synchronized实现原理及优化
1. synchronized实现原理
sychronized的使用场景:
对象锁monitor机制
1. synchronized修饰同步代码块
先来看一段简单的代码
为了解到synchronized的底层实现原理,我们来对这段代码进行反编译
先通过cd
命令进入到src下的package目录
先编译javac Test.java
,生成class文件
反编译命令:
javap -c -v Test.class
然后我们来看生成的字节码:
执行同步代码块后首先要先执行monitorenter
指令,退出的时候执行monitorexit
指令。通过分析之后可以看出,使用synchronized进行同步,其关键就是要获取对象的监视器monitor
,当线程获取monitor
后才能继续往下执行,否则就只能等待。
而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor
。
上述字节码中包含一个monitorenter
指令以及两个monitorexit
指令。这是因为JVM需要确保获得的锁在正常执行路径、异常执行路径上都能够被解锁。
2. synchronized修饰同步方法
当用synchronized标记方法时,字节码中方法的访问标记包括 ACC_SYNCHRONIZED
。
- 进入该方法时,JVM需要进行
monitorenter
操作 - 退出该方法时,不管是正常返回,还是向调用者抛异常,JVM均需要进行
monitorexit
操作
monitorenter—monitorexit
-
关于monitorenter 和 monitorexit 的作用,我们可以抽象地理解为:每个锁对象拥有一个锁计数器和一个 “指向持有该锁的线程” 的指针
-
当执行monitorenter时,如果目标锁对象的计数器为0,那么说明它没有被其他线程所持有。在这个情况下JVM会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
-
在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么JVM可以将其计数器再加1;否则需要等待,直至持有线程释放该锁
-
当执行monitorexit时,JVM则需将锁对象的计数器减1。当计数器减为0时,便代表该锁已经被释放掉了
可重入锁
当执行monitorenter
时,对象的monitor计数器值不为0,但是持有锁的线程恰好是当前线程。此时将monitor计数器值再次+1
,当前线程再次进入同步方法或代码块
之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁
【证明锁的可重入与互斥】
synchronized修饰的test1
方法中调用test2
方法
class Sync implements Runnable {
@Override
public void run() {
test1();
test2();
}
public synchronized void test1() {
if (Thread.currentThread().getName().equals("A")) {
test2();
}
}
public synchronized void test2() {
if (Thread.currentThread().getName().equals("B")) {
System.out.println("B线程进入该同步方法test2()...");
}else {
//此时B线程还没有启动
System.out.println(Thread.currentThread().getName() + "线程--->进入test2()方法");
}
}
}
public class Reentrant {
public static void main(String[] args) throws InterruptedException {
Sync run = new Sync();
new Thread(run,"A").start();
//有时间差,保证A先启动
Thread.sleep(2000);
new Thread(run,"B").start();
}
}
如果一个类中拥有多个synchronized方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作
2. JDK1.6后对synchronized的优化
synchronized的操作都是互斥的,效率低。当一个线程拿到了锁资源之后,因为要保证同步,所以其他线程只能等待该线程释放锁,效率自然降低。
优化的思想:让每个线程通过同步代码快时的速度提高
CAS自旋
CAS自旋,无锁实现的同步----乐观锁Compare And Swap
Compare And Swap(O, V, N)
-
O:当前线程存储的变量值
-
V:内存中该变量的具体值
-
N:希望修改后的变量值
线程1先进入同步代码块:
-
O ----> i = 0
-
V ----> i = 0
-
O == V
-
N ----> i = 10
线程2再进入同步代码块:
-
O ----> i = 0
-
V ----> i = 10
-
O != V
-
N ----> 修改失败
-
当 O == V 时,此时表示还没有线程修改共享变量的值,此时可以成功的将内存中的值修改为N
-
当 O != V 时,表示此时内存中的共享变量值已经被其他线程修改,此时返回内存中的最新值V,再此尝试修改
自旋问题可以理解为开车过红绿灯:
-
线程挂起阻塞:车熄火
-
自旋: 脚踩刹车,车不熄火
自旋带来的问题: ABA问题
- 解决方法: 添加版本号 A.1----> B.2
自旋在CPU上跑无用指令,虽然减少了操作系统的开销,但是浪费了CPU资源
JVM自适应自旋 : 根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)
- JVM尝试自旋一段时间,若在此时间内线程成功获取到锁,在下次获取锁时,适当延长自旋时间;
- 若在此时间内线程没有获取到锁,在下次获取锁时,适当缩短自旋时间
公平性问题
-
处于阻塞状态的线程可能一直无法获取到锁,自旋状态的线程更容易获取到锁
-
Lock锁可以实现公平性,synchronized无法实现公平锁
偏向锁
JDK 1.6 之后默认synchronized
最乐观的锁:进入同步块或同步方法始终是一个线程
在不同时刻时,当出现另一个线程也尝试获取锁,偏向锁会升级为轻量级锁
轻量级锁
- 不同时刻有不同的线程获取锁,“亮黄灯策略”,基本不存在锁竞争
- 同一时刻,如果不同线程尝试获取锁,会将偏向锁自动升级为重量级锁
重量级锁
-
JDK 1.6 之前的锁都是重量级锁,将线程阻塞挂起
-
锁只有升级过程,没有降级
锁粗化
将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁
比如使用StringBuffer中的apperd方法来添加字符串
sb.append("a");
sb.append("b");
sb.append("c");
这里每次调用append方法都需要加锁和解锁操作
如果虚拟机检测到有一系列连串对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append
方法时进行加锁,最后一次append
方法结束后进行解锁
锁消除
当对象不属于共享资源时,对象内部的同步方法或同步代码块的锁会被自动解除