synchronized的实现原理以及JDK1.6之后的优化

synchronized的底层实现

使用synchronized进行同步,关键就是对对象的监视器Monitor进行获取。这涉及到的是JVM层级别的monitorenter与monitorexit指令实现。

在使用synchronized时必须保证锁定的对象Object以及其子类对象。只有Object和它的子类才有对象监视器。

  1. 执行同步代码块的时候

需要执行一个monitorenter多个monitorexit指令。这是因为JVM要保证无论是在正常还是异常情况下都能被解锁。

执行同步代码块首先执行monitorenter,退出执行monitorexit指令。当线程获取Monitor后才能继续往下执行,否则只能等待。而则这个获取的过程是互斥的,即同一时刻只有一个线程能够获取Monitor。

  1. 执行同步方法

会打上一个ACC_SYNCHRONIZED标记。该标记表示进入该方法时,JVM需要进行monitorenter操作。而在退出方法时,不管是正常返回,还是抛出异常,java虚拟机均需要进行monitorexit操作。


Monitor机制

1. monitorenter:获取监视器

synchronized(o){

}
  • 检查o对象的Monitor计数器值是否为0,为0表示此监视器还未被任意一个线程获取,此线程可以进入同步代码块并且将Monitor值+1,将Monitor的持有线程标记为当前线程
  • 当Monitor计数器值不为0,且持有线程不是当前线程,表示Monitor已经被别的线程占用,当前线程只能阻塞等待
  • 当Monitor计数器值不为0但是持有线程是当前线程时,那么java虚拟机可将其计数器值+1.

可重入锁:当执行monitorenter时,对象的Monitor计数器值不为0,但是持有线程恰好是当前线程,此时将Monitor计数器值再次+1,当前线程进入同步方法或代码块。

  1. monitorexit:释放监视器
  • 当执行monitorexit时,java虚拟机需将锁对象的计数器减1、当计数器减为0时,表示该锁已经被释放了。

JDK1.6后对于内建锁synchronized的优化

在JDK1.6之前,synchronized性能特别低。挂起线程和恢复线程的操作都需要转入内核实现。在JDK1.6后,对synchronized进行了优化,有自适应自旋,锁消除,锁粗话,轻量级锁,偏向锁等等。

使用synchronized的最大特征就是:在同一时刻只有一个线程能够获取对象的监视器(Monitor),从而进入到同步代码块或同步方法中,表现为互斥性。这种方式效率肯定低下,因为每次只能通过一个线程。所以针对这种形式,优化方法就是提高每次线程通过的速度


CAS操作(无锁实现的同步-乐观锁)---- 自旋

之前synchronized获取锁失败,将线程挂起。----- 悲观锁策略。

CAS操作过程

CompareAndSwap(O,V,N):

  • O:当前线程存储的变量值
  • V:内存中该变量的具体值
  • N:希望修改后的变量值

当O==V,此时还没有线程修改共享变量的值,此时可以成功的将内存的值修改为N;
当O!=V,表示此时内存中的共享变量值已被其他线程修改,返回内存中的最新值V,再次尝试修改变量。

当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程

CAS并不是武断的将线程挂起,当CAS操作失败后,会进行一定的尝试,而非进行耗时的挂起唤醒操作,因此也叫非阻塞同步。

之前的线程挂起/阻塞:车熄火
自旋:脚踩刹车,车不熄火

CAS的问题

1)ABA问题
比如原来的i=0;

线程1 线程2 线程3
i=1 i=0 i=0

此时线程3认为此时内存的值没有修改,因此没有返回。
解决以上问题,添加版本号(1.A 2.B 3.A)

2)自旋在CPU上跑无用指令,会浪费CPU资源
这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它期望在运行无用指令的过程中,锁能够被释放出来。

自适应自旋:JVM尝试自旋一段时间,若在此时间内,线程成功获取到锁,在下次获取锁时,适当延长自旋时间。即增强循环时间。
若再次时间内,线程没有获取到锁,在下次获取锁时,适当缩短自旋时间

3)公平性问题
处于阻塞态的线程:可能一直无法获取到
处于自旋态线程:速度快
解决以上问题:synchronized无法实现公平锁,可以由Lock实现公平性


偏向锁

最乐观的锁:进入同步块或者同步方法中始终是一个线程
这就相当于在自家庄园里装了个红绿灯,只有自己在开车。当碰到自己的车牌,直接亮绿灯。

当使用偏向锁时,会在对象头和栈帧中的锁记录例存储锁偏向的线程ID。在线程进入和退出同步块时,不需要进行CAS操作来加锁和解锁,只需要简单测试一下对象头是否含有该线程偏向锁,如果有,表示线程已经获取了锁。如果没有,查看对象头中的偏向锁的标识是否为1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁,否则直接将对象头中的偏向锁指向当前线程。

当出现另一个线程也尝试获取锁时(不同时刻),偏向锁会升级为轻量级锁


轻量级锁

多个线程在不同时间段请求同一把锁,也就是没有锁竞争。使用轻量级锁,来避免线程的阻塞以及唤醒。

当同一时刻有不同的线程尝试获取锁,会将偏向锁升级为重量级锁。


重量级锁

JDK1.6之前的锁都是重量级锁,将线程阻塞挂起。


锁粗化

当出现多次连续的加锁或解锁过程,会将多次加解锁化为一次的加锁与解锁的过程。


public class Test {
    private static StringBuffer stringBuffer = new StringBuffer();
    public static void main(String[] args) {

        //append被synchronized修饰
        //在第一次append方法进行加锁,最后一个append结束后进行解锁
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

锁消除

当对象不属于共享资源时,对象内部的同步方法或同步代码块会自动被解除。


public static void test(){
    StringBuffer stringBuffer = new StringBuffer();
    //append被synchronized修饰
    stringBuffer.append("a");
    stringBuffer.append("b");
    stringBuffer.append("c");
}

你可能感兴趣的:(Java)