Synchronized是如何实现的|七日打卡

646 阅读4分钟

Synchronized 的使用场景

Synchronized 可以用在方法上也可以用在代码块上。

细分下来 Synchronized 使用方法有三种:分别是普通方法,静态方法,同步代码块。

对于普通方法,锁是当前实例对象。
对于静态方法,锁是当前类的 Class 对象。
对于同步代码块,锁是括号里配置的对象。

Synchronized原理分析

我们常常说,Synchronized 具有可重入性不可中断性,那么这些性能特点又是如何实现的呢?这就需要我们深入源码查看。

我们都知道,Java 对象头都会关联一个监视器锁(monitor),当 monitor 被占用就处于锁定状态。用一句话来总结:Synchronized = Java 对象头 + Monitor 机制

在深入分析前,首先要弄清楚:如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一。而在执行 monitorexit 指令时会将锁计数器减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。因此:

  • 对于锁代码块,会在编译后的字节码的前后包含 monitorenter 和 monitorexit 字节。在执行 monitorenter 指令的时候,首先要去尝试获取对象的锁(获取对象锁的过程,其实是获取 monitor 对象的所有权的过程)。

  • 对于锁方法,则会在编译方法的时候 ACCESS_FLAGS 加一个 ACC_SYNCHRONIZED 标识位,代表的是当线程执行到方法后会检查是否有这个标识位,如果有的话执行线程将先持有 monitor ,然后隐式的去调用 monitorenter 和 monitorexit 两个命令来将方法锁住。执行线程持有了 monitor ,其他任何线程都无法再获得同一个monitor。常见的标志位还有 public,private,static 等

通过以上的介绍可以知道,无论是锁代码块还是锁方法,其本质都是争夺 monitor 对象的所有权

之所以采用计数器的方式,是为了允许同一个线程重复获取同一把锁。这就是Synchronized可重入性的实现原理,究其根本还是为了避免死锁的发生。

以上全是属于理论中的锁实现,接下来让我们看看HotSpot虚拟机中具体的锁实现。

Synchronized的优化

重量级锁:

上面提到了Synchronized的实现离不开与对象关联的 monitor,实际上这时候的 Synchronized 依赖于 Mutex Loc 机制,存在大量的性能开销,因此在Java 1.6 之前也被叫做重量级锁。

到了 Java 1.6 的时候,synchronized 做了大量优化,引入了轻量级锁和偏向锁。到这时候,锁会有四种状态,分别是无锁偏向锁轻量级锁重量级锁。它会随着竞争情况逐渐升级。锁只可以升级而不能降级。升级方向是 无锁->偏向锁->轻量级锁->重量级锁,而当升级到重量级锁的时候,就只能被挂起阻塞来等待被唤醒了。升级的过程实际上也就是在源码中调用不同的实现去争夺锁,如果失败了就调用更高级的实现,从而完成锁的升级。

偏向锁:

大部分情况下,锁不仅仅不存在多线程竞争,而是总是由同一个线程多次获得。为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头里记录锁偏向的线程 ID,下次该线程再次进入只需要判断线程 ID 就可以了。

轻量级锁:

获取锁:

jvm 利用 CAS 去竞争锁,竞争成功则将锁标志位变成00(表示此对象处于轻量级锁状态),否则说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态,等待唤起。

释放锁:

轻量级锁的释放也是通过 CAS 进行的。

总结

不难看出,Java中的锁优化就是依赖于 CAS 实现的,后面我们也会专门分析。

同样的还有lock,时间问题就先不说了,有机会再补上(逃~