初识 Synchronized
- 使用
synchronized修饰方法:这相当于为方法中的全部代码加上 this 对象锁,也就是将该类的实例作为锁对象。这表示,如果一个类中有多个方法被synchronized修饰,那么对于该类的同一实例,同一时间最多有一个方法被执行,对于不同实例则不受影响。这是比较粗粒度的锁控制。 - 使用
synchronized修饰静态方法:对于静态方法,JVM 直接通过类名找到对应的 Klass 结构,定位到方法字节码然后执行,没有 this 这个参数的传递。反映到 Java 层面就是我们无法在静态方法内部访问 this 引用,也就是经常提到的,静态方法属于 Class 对象而不属于 Object 对象。所以,synchronized修饰静态方法实际上是使用了 Class 对象作为监视锁。这意味着,同一时刻一个 JVM 进程内只能有一个线程访问该方法,这是非常粗粒度的锁控制。 - 使用
synchronized同步块:这是更为推荐的做法。使用synchronized修饰代码块可以更细粒度地控制并发,从而避免不必要的竞态。诸如读写锁、分段锁都是这种思想的体现。
Synchronized 锁升级
-
无锁状态:
当一个对象从未被当做锁来使用,或者说从未有线程来竞争这个对象,那么该对象处于无锁状态。无锁状态下 MarkWord 中保存的是该对象的 hashcode。我们知道只有调用对象的
hashcode()方法时才会计算其 hashcode,因此更好的说法是,无锁状态下对象的 MarkWord 中预留了其 hashcode 的位置。 -
偏向锁:
在同一时刻只有一个线程尝试占有资源时,进入偏向锁状态。偏向锁状态下的 MarkWord 保存抢到锁的线程 ID。刚刚提到,这个位置本来是为了保存对象的 hashcode 的,现在又要保存一个线程 ID,这就冲突了。因此,只有没计算过 hashcode 的对象才能进入偏向锁状态,否则其 hashcode 没有其他地方保存。
偏向锁状态下,是没有竞争的,线程加锁过程如下:线程 ID 为自己的 ID?是 -> 直接访问共享资源;不是 -> 是否为 NULL?是 -> CAS 将自己的 ID 加入 MarkWord;不是 -> 升级轻量级锁。
这表示,即使两个线程并不是同时,而是先后访问该资源,也会触发偏向锁的撤销,升级为轻量级锁。这是因为,线程在退出偏向锁时,不会撤销偏向锁,它的线程 ID 依然留在 MarkWord 中,不会置为 NULL,除非这个线程挂了,JVM 才会重新偏向。
这引来了一个问题,就是,当线程 A 发现 MarkWord 中的线程 ID 不是自己,而是线程 B,那 A 能不能知道 B 是正在访问共享资源呢?还是已经退出访问呢?其实它是不知道的。但这很重要,如果 B 正在访问资源,那么它应当直接进入轻量级锁,A 则参与竞争;如果 B 已经退出访问,那就跟它没关系了,A 参与竞争。
所以偏向锁是不能随便撤销的,必须等待一个全局安全点,在全局安全点,所有用户线程都处于挂起状态,这个过程如下:
JVM 要进行偏向锁的撤销,则设置一个
global safe point标志位,用户线程主动检查这个标志位,若为 true,则将自己挂起。那么用户线程在什么时候检查这个标志位呢?在到达安全点的时候。每个用户线程都有自己的安全点,这是 JVM 在代码的特定位置插入的,通常在方法的返回处和跳出循环后。在全局安全点,所有用户线程都挂起了,被称为 Stop The World。这个时候,JVM 才能保证 native 线程对堆的独占式访问,从而进行 GC、偏向锁撤销等操作。
偏向锁撤销需要等待全局安全点,如果频繁发生撤销,可能导致严重的性能问题。因此 JDK 18 开始,偏向锁相关代码已经被彻底删除了,不再支持偏向锁。
-
轻量级锁
线程竞争轻量级锁时,JVM 首先在抢锁线程的栈帧中建立一个锁记录。之后第一件事,线程要在锁记录中保存当前锁对象 MarkWord 的拷贝,也就是对象哈希码等信息。这是因为对象在轻量级锁状态下的 MarkWord 要保存指向线程锁记录的指针,没有空间保存哈希码了。待线程释放锁,还要将这些信息还原回 MarkWord。
下一步是 CAS,这里的 CAS 涉及到这几个操作数:对象 MarkWord 的内存地址,期望的处于无锁状态的 MarkWord,指向自身锁记录的指针。因此这步操作就是:当且仅当 MarkWord 处于无锁状态时,将 MarkWord 设为自己的锁记录地址。如果操作成功,线程要将锁记录中的 owner 指针指向锁对象,这样在释放锁的时候才能找得到锁对象,接下来线程就可以访问共享资源了。
如果操作失败,这个时候线程就要让锁对象关联一个
ObjectMonitor。 -
重量级锁
为锁对象关联
ObjectMonitor的源码如下:javaObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) { for (;;) { // 分支1:对象正被其他线程轻量级锁定 if (mark->has_locker()) { // 调用 omAlloc 函数,获得一个空闲的 ObjectMonitor 对象 ObjectMonitor * m = omAlloc (Self) ; // 对新分配的 ObjectMonitor 进行初始化 m->Recycle(); m->_Responsible = NULL ; m->OwnerIsThread = 0 ; m->_recursions = 0 ; // 设置自旋等待策略的持续时间,这体现了适应性自旋 m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // 这是一步 CAS,尝试将 MarkWord 的标记状态 // 从轻量级锁原子性地置换为一个特殊的 INFLATING(膨胀中)状态 // 这一步非常重要,它确保了同一时刻只有一个线程能成功地将某个对象的锁状态 // 标记为 INFLATING,从而防止多个线程为同一个对象创建多个 ObjectMonitor。 markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ; if (cmp != mark) { omRelease (Self, m, true) ; continue ; } // 从线程栈的锁记录中,取出并返回轻量级锁创建时保存的无锁状态标记字。 markOop dmw = mark->displaced_mark_helper() ; assert (dmw->is_neutral(), "invariant") ; // 将上面取出的无锁状态标记字设置到 ObjectMonitor 的 _header 字段, // 为了未来锁释放后能恢复对象为无锁状态。 m->set_header(dmw) ; // 将当前持有轻量级锁的线程设置为 Moniter 的持有者 m->set_owner(mark->locker()); // 将锁对象与 Moniter 相关联 m->set_object(object); // 将锁对象的 MarkWord 指向 Monitor object->release_set_mark(markOopDesc::encode(m)); // 经历了上面三步则表示锁膨胀成功 return m ; } // 分支2:对象处于无锁状态 // 这是 JVM 决定跳过轻量级锁直接使用重量级锁的特殊情况 assert (mark->is_neutral(), "invariant"); // 依然获取一个空闲 Moniter ObjectMonitor * m = omAlloc (Self) ; // 保存无锁状态标记字 m->set_header(mark); return m ; } }

现在锁对象已经关联了一个 ObjectMonitor,可以认为其已经进入重量级锁状态。接下来线程会进入 ObjectMonitor::enter(TRAPS) 方法来抢锁,这一部分的详细源码解读可以看我的另一篇博客:
[Java 并发编程\] 源码层面解读 ObjectMonitor-CSDN博客](https://blog.csdn.net/Steve_Albini/article/details/155351545?spm=1011.2415.3001.5331) 所以在这里我就简述了,线程进入这个方法后会先尝试将 `_owner` 指向自己,如果失败,则进入适应性自旋。自旋一段时间后无果,为该线程关联一个 `ParkEvent`,封装进 `cxq`,此时线程真正被挂起在 OS 内核。 这里的适应性自旋就是指,如果一个锁对象经常被自旋抢到,就说明这个锁适合自旋,JVM 会适当增加其自旋时长,反之则会减少其自旋时长。 看到这可能有些人会有疑问,就是自旋锁难道不是属于轻量级锁吗,怎么需要关联 `ObjectMonitor` 呢?其实你怎么说都可以,因为看问题的角度不同。自旋锁的源码的确存在于 `ObjectMonitor` 中,这个时候锁对象的状态也的确是 10,可以认为已经处于重量级锁状态;但是自旋锁从逻辑上讲应当是一种轻量级锁,这也是没问题的。