当前位置: 首页 > news >正文

synchronized 修饰符的使用

在黑马点评项目实战项目中使用了synchronized(),本文主要是为了对在学习过程中遇到的一些问题和疑问以及解决方案进行一个终结。

在Java并发编程中,synchronized是最基础且常用的锁机制,它像一把“隐形的锁”,守护着多线程环境下的数据安全。但你是否遇到过这样的困惑:单机场景下运行良好的synchronized,在集群部署时却突然“失效”,导致超卖或重复下单?本文将从原理到实践,带你彻底搞懂synchronized的底层逻辑、使用场景,以及在集群模式下的局限性。


一、synchronized基础:锁的本质与使用场景

1.1 什么是synchronized?

synchronized是Java的​​内置同步关键字​​,用于控制多线程对共享资源的访问,确保同一时间只有一个线程能执行被修饰的代码块或方法。它的核心是通过​​对象锁(Monitor Lock)​​实现线程互斥,解决多线程并发访问时的线程安全问题(如数据不一致、超卖、重复下单等)。

1.2 三种修饰形式:锁的“绑定对象”

synchronized可以修饰三种代码结构,锁的绑定对象不同:

  • ​实例方法​​:锁是当前对象实例(this)。例如:
    public synchronized void updateUser() { ... } // 锁是this(当前User对象)
  • ​静态方法​​:锁是类的Class对象(如UserService.class)。例如:
    public static synchronized void updateCount() { ... } // 锁是UserService.class
  • ​代码块​​:锁是括号内指定的对象(如lockObj)。例如:
    synchronized (lockObj) { ... } // 锁是显式指定的lockObj对象

1.3 核心作用:解决哪些问题?

synchronized的核心价值是​​保证原子性​​,典型场景包括:

  • ​共享变量修改​​:如库存扣减(stock--)、订单数量更新。
  • ​复合操作​​:如“查询库存→扣减库存→创建订单”的原子性组合(避免中间状态被其他线程干扰)。
  • ​防重复操作​​:如“一人一单”场景(同一用户只能创建一个订单)。

二、代码中的synchronized:为什么用userId.intern()?

2.1 问题背景:集群下的“一人一单”

在秒杀场景中,用户可能通过快速点击发起多个请求。若同一用户的多个线程同时执行“扣库存→创建订单”逻辑,会导致:

  • 库存被重复扣减(超卖);
  • 同一用户创建多个订单(重复下单)。

因此,需要用synchronized锁定同一用户的线程,确保操作串行执行。但直接使用userId作为锁对象会遇到问题。

2.2 关键细节:userId.toString().intern()的作用

用户代码中,锁对象通常写成synchronized (userId.toString().intern()),这是为了​​确保同一用户ID的不同String对象共享同一个锁引用​​。

(1)String的“陷阱”:相同内容≠同一对象

userIdLong类型,调用toString()会生成新的String对象(即使内容相同)。例如:

Long userId1 = 123L;
Long userId2 = 123L;
String s1 = userId1.toString(); // 内存地址A的新String对象
String s2 = userId2.toString(); // 内存地址B的新String对象

此时s1s2内容相同但内存地址不同。若直接用s1s2作为锁对象,同一用户的不同线程会锁定不同的对象,导致锁失效。

(2)intern():字符串常量池的“去重魔法”

String.intern()是Java的字符串池化机制,作用是将字符串添加到​​字符串常量池​​(String Pool)中,并返回池中已存在的引用(若字符串已存在)。

  • ​JDK 7前​​:常量池位于方法区(永久代),可能引发内存溢出;
  • ​JDK 7及以后​​:常量池移至堆(Heap),避免内存限制。

关键逻辑:

String s1 = userId.toString(); // 生成新String对象(堆中)
String s = s1.intern();        // 检查常量池:// - 存在:返回常量池中的引用(与s1内容相同)// - 不存在:将s1加入常量池,返回s1的引用

通过intern(),无论userId.toString()生成多少次String对象,只要内容相同,最终都会返回常量池中的​​同一个引用​​。因此,同一用户的不同线程锁定的是​​同一对象​​,确保同步。

2.3 不加intern()的后果

若省略intern()userId.toString()每次生成新的String对象(即使内容相同),会导致:

  • 同一用户的不同线程锁定不同的String对象(锁对象不一致);
  • synchronized失效,引发超卖或重复下单。

2.4 为什么锁用户ID?

锁用户ID的核心目的是​​隔离不同用户的并发操作​​:

  • 同一用户的多个线程必须串行执行(防止重复抢购);
  • 不同用户的线程使用不同的锁对象(互不影响),保证系统并发效率(不同用户可同时抢购)。

三、synchronized的底层原理:Monitor与对象头

3.1 锁的本质:Monitor监视器

synchronized的底层依赖Java对象的​​对象头(Mark Word)​​中的Monitor(监视器)。Monitor是JVM实现锁的核心数据结构,记录锁的状态(如是否被占用、持有线程ID、等待线程队列等)。

(1)对象头的内存布局

Java对象在内存中的布局分为三部分:

  • ​对象头​​:包含类型指针(指向类元数据)和Mark Word(存储运行时元数据);
  • ​实例数据​​:对象的实际属性值;
  • ​对齐填充​​:JVM要求对象内存地址为8字节的倍数,不足部分填充。

其中,Mark Word是锁机制的核心,存储锁状态标志(无锁/偏向锁/轻量级锁/重量级锁)、线程ID、等待队列等信息。

3.2 锁的获取与释放流程

synchronized的加锁与释放本质是​​Monitor的获取与释放​​,流程如下:

(1)加锁阶段

线程尝试进入synchronized代码块时,检查对象的Mark Word:

  • ​无锁(标志位01)​​:通过CAS操作将锁状态改为“偏向锁”(标志位01),记录当前线程ID;
  • ​偏向锁(标志位01)​​:若线程ID匹配,直接获取锁;若不匹配,升级为轻量级锁;
  • ​轻量级锁(标志位00)​​:通过CAS竞争锁,失败则升级为重量级锁;
  • ​重量级锁(标志位10)​​:线程被阻塞,加入Monitor的等待队列(EntryList)。
(2)释放锁阶段

线程执行完synchronized代码块后,释放Monitor锁:

  • ​偏向锁​​:清除线程ID,锁状态回退为无锁;
  • ​轻量级锁​​:通过CAS释放锁,唤醒等待线程(可能升级为重量级锁);
  • ​重量级锁​​:从EntryList唤醒一个等待线程,使其获取锁。

3.3 锁升级:性能优化的关键

JVM对synchronized做了​​锁升级​​优化,避免不必要的性能开销:

  • ​偏向锁​​:适用于单线程重复访问场景(如单例模式),减少无竞争时的锁获取开销;
  • ​轻量级锁​​:适用于短时间内的多线程竞争(如方法调用),通过自旋减少线程阻塞;
  • ​重量级锁​​:适用于长时间竞争场景(如高并发请求),通过操作系统线程阻塞/唤醒机制降低CPU消耗。

四、集群模式下的困境:synchronized的“进程内锁”局限

4.1 集群模式的核心特征

集群模式指系统部署在多台服务器(多JVM实例)上,通过负载均衡(如Nginx)将请求分发到不同实例。每个JVM实例有独立的​​内存空间​​和​​对象实例​​,线程仅存在于单个JVM中。

4.2 为什么synchronized失效?

synchronized是​​JVM层面的进程内锁​​,其锁的生效范围仅限于​​同一个JVM实例内的对象​​。具体表现为:

  • 锁的标识(Monitor)与对象的内存地址绑定;
  • 不同JVM实例中的同一类对象(如UserService)是​​不同的内存对象​​,拥有独立的Monitor;
  • 线程只能在所属JVM内竞争Monitor锁,无法感知其他JVM实例中的锁状态。

4.3 典型问题:同一用户多把锁

假设用户在集群环境中访问,请求被负载均衡到不同JVM实例(如Server A和Server B):

  • 用户在Server A中创建订单,Server A的synchronized锁保护该操作;
  • 用户的第二个请求被负载均衡到Server B,Server B的UserService对象是独立的,其Monitor未被锁定;
  • 结果:Server B的线程正常获取锁,导致同一用户创建多个订单(重复下单)。

​总结​​:synchronized的锁仅在同一JVM内有效,无法跨实例同步,集群模式下无法保证“同一用户只有一把锁”。


五、集群模式下的替代方案:分布式锁

为解决集群环境下的并发问题,需使用​​分布式锁​​,其核心是​​全局唯一的锁标识​​,确保不同JVM实例中的线程竞争同一把锁。常见实现包括:

5.1 Redis分布式锁(RedLock)

利用Redis的SETNX(原子性设置键值)命令实现锁:

// 获取锁(设置过期时间防止死锁)
String lockKey = "user_lock:" + userId;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);if (locked) {try {// 执行原子操作(查询、扣库存、创建订单)} finally {// 释放锁(Lua脚本保证原子性)redisTemplate.execute(new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class), Collections.singletonList(lockKey), "1");}
}

​优点​​:性能高,支持集群部署;
​缺点​​:依赖Redis可用性,需处理锁续期(如使用Redisson的WatchDog机制)。

5.2 ZooKeeper分布式锁

利用ZooKeeper的​​临时有序节点​​实现锁:

  • 客户端在/locks/user_${userId}路径下创建临时有序节点(如node_12345);
  • 客户端监听比自己序号小的前一个节点,若前一个节点删除(释放锁),则当前节点获取锁。
    ​优点​​:强一致性,自动失效(客户端断开则节点删除);
    ​缺点​​:性能低于Redis,适合对一致性要求高的场景。

5.3 数据库分布式锁(不推荐)

通过数据库的唯一索引或FOR UPDATE行锁实现,但性能较差且易引发死锁,仅适用于小规模场景。


六、总结:synchronized的适用边界与最佳实践

场景synchronized是否适用原因
单机多线程锁作用于同一JVM内的对象Monitor,保证线程互斥
集群多JVM实例不同JVM的Monitor独立,锁无法跨进程同步
分布式系统(跨机器)需分布式锁(如Redis、ZooKeeper)实现全局互斥

​核心结论​​:
synchronized是单机环境下的高效锁机制,但其“进程内锁”的本质决定了无法在集群模式下跨JVM同步。集群环境下,需使用分布式锁(如Redis、ZooKeeper)解决跨实例的并发问题。

下次遇到集群下的线程安全问题时,记得:​​synchronized管单机,分布式锁管集群​​!

http://www.dtcms.com/a/291319.html

相关文章:

  • (7)ROS2-MUJOCO联合仿真环境迁移优化
  • MVCC 多版本并发控制 详解
  • C语言(20250721)
  • 【PTA数据结构 | C语言版】验证六度空间理论
  • day20-sed-find
  • 【学习路线】C#企业级开发之路:从基础语法到云原生应用
  • 感知机-梯度下降法
  • 代码随想录day41dp8
  • 教资科三【信息技术】— 学科知识: 第三章(多媒体技术)
  • Java I/O模型深度解析:BIO、NIO与AIO的演进之路
  • CDN和DNS 在分布式系统中的作用
  • JAVA+AI教程-第三天
  • 数据库mysql是一个软件吗?
  • 主流 MQ 的关键性能指标
  • 瑶池数据库Data+AI驱动的全栈智能实践开放日回顾
  • 5.Java的4个权限修饰符
  • 如何用 LUKS 和 cryptsetup 为 Linux 配置加密
  • 3.4 递归函数
  • GUI简介
  • CMake变量和环境变量之间的关系和区别CMAKE_EXPORT_COMPILE_COMMANDS环境变量作用
  • Weex 知识点
  • SymPy 中抽象函数求导与具体函数代入的深度解析
  • C多线程下的fwrite与write:深入挖掘与实战指南
  • 每日算法刷题Day51:7.21:leetcode 栈6道题,用时1h40min
  • 【项目实战】——深度学习.全连接神经网络
  • PostgreSQL SysCache RelCache
  • Java API (二):从 Object 类到正则表达式的核心详解
  • DevOps是什么?
  • Flutter中 Provider 的基础用法超详细讲解(一)
  • C++的“链”珠妙笔:list的编程艺术