synchronized的实现原理以及锁优化

版权声明:转载请标明附带连接标明出处 https://blog.csdn.net/Hollake/article/details/90711538

前言

为了巩固帮助自己记忆锁优化的相关知识,今天对其做一些总结。

synchronized在多线程并发编程中一直是元老级人物,而且在JDK1.6之前由于实现同步所带来的性能消耗过大,因而被称为重量级锁,随着JDK1.6对synchronized的各种优化,产生了偏向锁轻量级锁,自适应锁等,它现在也就没有那么重量级了。

实现原理

synchronize是如何实现同步操作的呢?它通过以下三种方式实现同步:

  1. 对于普通同步方法,锁是当前实例对象。
  2. 对于静态同步方法,锁是当前类的class对象。
  3. 对于同步代码块,锁是synchronized{}中所配置的对象

那么它具体是怎么实现在同步方法或者同步代码块中保证最多只会有一个线程来访问呢?每个对象都是一个监视器锁(Monitor),其实JVM是通过进入和退出Monitor对象来实现方法和代码块的同步。

具体来说就是JVM对synchronized修饰的方法或者代码块在编译后会插入monitorenter和monitorexit两条指令,即在进入同步方法或者同步代码块之前插入monitorenter,在退出时插入monitorexit,这样就能保证每次只有一个线程能够访问同步方法或者同步代码块,下面就是一个例子来说明:

public class SynchronizedTest {
    public void test(){
        synchronized (SynchronizedTest.class) {
            System.out.println("code ");
        }
    }
}

javac SynchronizedTest.java 进行编译,接着通过javap -c SynchronizedTest.class进行反编译得到如下结果:

synchronized是一个可重入锁,线程在执行到monitorenter指令时尝试获取monitor的所有权,判断monitor的进入数是否为0,如果是0,说明当前锁可以获取,获取后将进入数设置为1,该线程拥有锁,如果判断该锁的进入数大于0,而且发现拥有该锁的线程是自己,那么进入数+1,每执行monitorexit退出一次进入数-1。

锁优化

为了减少获取锁和释放锁带来的性能消耗,在JDK1.6后引入了偏向锁和轻量级锁,在JDK1.6以后,锁一共有四种状态,级别从低到高:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以从低到高升级,但是不能降级,就是偏向锁可以升级到轻量级锁,但是轻量级锁不能降级到偏向锁。在阅读接下的部分之前,建议对Java对象头进行简单了解。

偏向锁

引入偏向锁的作者发现,在大多数情况下是不存在竞争的,都是由一个线程多次获得,为了使获得锁的代价更低,于是引入了偏向锁。

获得锁,当一个线程要进入同步代码块获得锁的时候,会检查对象头中是否存储当前线程ID,如果没有,判断一下偏向锁标志位是否为1,如果不是,说明这个锁还没有被任何线程获取,于是采用CAS的方式替换Mark Word,如果替换成功,将对象Mark Word中的线程ID指向自己,并且将是否偏向的标志置为1,如果发现偏向锁标志位以及为1,则尝试CAS的方式将当前对象头的偏向锁指向当前线程。在下次线程要获得锁的时候直接判断对象头中存储的是否是自己的线程ID,如果是就直接进入,不用重新获取。

锁释放,偏向锁只有在其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。偏向锁的撤销必须等待全局安全点(无正在执行的字节码)。它首先会暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不是,那么就将对象头设置为无锁状态;如果仍然活着,继续执行完成,最后会将对象头设置为无锁状态或者标记已经不适合作为偏向锁,这是有虚拟机来决定,最后唤醒等待线程。

可以看出偏向锁的优势在于一个线程多次获得同一个锁,在已经持有偏向锁的情况下,只需要判断当前对象头的线程ID是否是当前线程,这和不加锁只存在纳秒级的差距。

轻量级锁

获得锁,当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录区域,同时将锁对象的对象头中Mark Word拷贝到锁记录中,再尝试使用CAS将Mark Word更新为指向锁记录的指针。

如果更新成功,表示获得锁成功,如果失败,说明当前锁存在竞争,当前线程尝试使用自旋来获得锁。

锁释放,会使用CAS操作将锁记录区域的记录的信息替换回对象头,如果成功,则说明没有发生竞争如果失败,那么就导致锁膨胀为重量级锁。

因为CAS自旋会消耗CPU资源,在线程没有竞争的情况下相比互斥来说会好很多,但是在线程竞争激烈的情况下,不仅有互斥的开销,还有CAS的开销,甚至比重量锁更慢。

自旋锁与自适应锁

在一台物理主机上拥有2个以上的CPU时,能让2个以上同时并行执行,那么在获得同一个锁发生竞争时,让没有或得到锁的线程不放弃CPU的执行时间,看看持有锁的线程是否会很快释放锁,为了让线程等待,我们只需让线程执行一个忙循环(自旋),这种技术就叫做自旋锁。一直等待自旋会浪费CPU资源,不可能让线程一直自旋等待,自旋等待的次数默认值为10次。

JDK1.6引入了自适应自选锁,它的等待时间由前一次等待这个锁的时间和锁的拥有者来决定,例如在同一个锁对象上,自旋刚刚成功获得过锁,那么JVM会认为这次自旋也很有可能成功,那么等待的时间会相应增加,反之,如果对于某个锁自旋很少获得成功,那么有可能直接省略自旋,以避免浪费CPU资源。

参考文献:

《Java并发编程的艺术》

《深入理解Java虚拟机》

猜你喜欢

转载自blog.csdn.net/Hollake/article/details/90711538