使用synchronized进行同步,关键就是对对象的监视器Monitor进行获取。这涉及到的是JVM层级别的monitorenter与monitorexit指令实现。
在使用synchronized时必须保证锁定的对象是Object以及其子类对象。只有Object和它的子类才有对象监视器。
需要执行一个monitorenter和多个monitorexit指令。这是因为JVM要保证无论是在正常还是异常情况下都能被解锁。
执行同步代码块首先执行monitorenter,退出执行monitorexit指令。当线程获取Monitor后才能继续往下执行,否则只能等待。而则这个获取的过程是互斥的,即同一时刻只有一个线程能够获取Monitor。
会打上一个ACC_SYNCHRONIZED标记。该标记表示进入该方法时,JVM需要进行monitorenter操作。而在退出方法时,不管是正常返回,还是抛出异常,java虚拟机均需要进行monitorexit操作。
1. monitorenter:获取监视器
synchronized(o){
}
可重入锁:当执行monitorenter时,对象的Monitor计数器值不为0,但是持有线程恰好是当前线程,此时将Monitor计数器值再次+1,当前线程进入同步方法或代码块。
在JDK1.6之前,synchronized性能特别低。挂起线程和恢复线程的操作都需要转入内核实现。在JDK1.6后,对synchronized进行了优化,有自适应自旋,锁消除,锁粗话,轻量级锁,偏向锁等等。
使用synchronized的最大特征就是:在同一时刻只有一个线程能够获取对象的监视器(Monitor),从而进入到同步代码块或同步方法中,表现为互斥性。这种方式效率肯定低下,因为每次只能通过一个线程。所以针对这种形式,优化方法就是提高每次线程通过的速度。
之前synchronized获取锁失败,将线程挂起。----- 悲观锁策略。
CAS操作过程
CompareAndSwap(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");
}