一、synchronized实现原理
为了解决线程安全问题,java提供了synchronized关键字来保证线程的同步互斥,使得同一时间只有一个线程来访问共享资源,而这个同步互斥就来自于监视锁Monitor。
1.Java对象结构
说到监视锁Monitor我们不得不说一下Java对象的构成,如下图所示:
关于对象头更细一步的介绍可以参考:https://blog.csdn.net/scdn_cp/article/details/86491792.
在每一个对象的Mark Word中都有一个指向对象监视器Monitor的指针,synchronized关键词也是基于这个实现的。
2.synchronized实现原理
当一个线程访问同步代码块时,线程进入时需要获取到对象的监视器Monitor锁,在结束或者抛出异常时释放监视器Monitor锁。
monitor锁机制:在Java代码编译为字节码时,字节码文件开始的位置生成monitorenter指令,结束生成monitorexit指令。
- monitorenter: 当线程进入同步代码块时,monitor被占用并会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权;简单理解就是:同步代码块进入时,执行monitorenter指令就获取到了对象的监视器monitor锁。
- monitorexit: 执行monitorexit的线程必须是对象所对应的monitor的所有者,在synchronized代码块结束时,执行monitorexit指令来释放锁。
我们来看一段代码:
第一种:synchronized代码块:
public class SynchronizedTest {
public void test() {
synchronized (this) {
System.out.println("hahha");
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest=new SynchronizedTest();
synchronizedTest.test();
}
}
通过" javap -c 文件名 "命令反编译成汇编代码:
我们可以看出在进入synchronized代码块时进入monitorenter指令获得锁,离开代码块时执行monitorexit指令并释放对象锁。
总结:
- JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的。
- 通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
- JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。
- 字节码中包含一个monitorenter指令以及多个monitorexit指令。这是因为Java虚拟机需要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁。
我们将代码改为sychronized加在普通实例方法上:
第二种:synchronized普通实例方法:
public class SynchronizedTest {
public synchronized void test() {
System.out.println("hahha");
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest=new SynchronizedTest();
synchronizedTest.test();
}
}
反编译结果图下:
synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。(见博客 https://www.cnblogs.com/javaminer/p/3889023.html.)
二、synchronized优化
在JDK1.5之前synchronized是一个重量级锁,效率比较低。随着JDK1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。
在jdk1.6后,synchronized具体被优化包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁 这些优化策略。
1、自适应的CAS自旋锁
自适应自旋锁能够智能地根据它得到的信息判断是否要自旋。自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
优点:这样能够减少自旋的次数提高效率。
2、偏向锁
对象的代码如果一直被同一线程执行,不存在多个线程竞争,这种情况下,该线程在后续的执行中就直接自动获取锁,这样就降低获取锁带来的性能开销。因此就引进了偏向锁。
偏向锁: 指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销:如果在某个时间点上没有字节码正在执行,就先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。
3、轻量级锁
轻量级锁: 是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁(A线程没有执行完同步代码块时,线程B就来竞争锁),线程 B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
轻量级锁转为重量级锁:
当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
偏向锁、轻量级锁、重量级锁之间的转换:
1)一开始如果偏向锁可用,就成为偏向锁(一个线程持有锁);也就是没有人竞争的情况下就为偏向锁。
2)如果有其他线程来竞争,偏向锁就转为轻量级锁
3)如果轻量级锁处理不了就会转为重量级锁
4、锁消除
很多应用程序的代码,用到了synchronized锁,但其实没在多线程环境下。这些加锁——释放锁的操作统统变得没有意义了。优化的时候就把锁消除掉!
锁消除:锁消除即删除不必要的加锁操作。一般使用在方法内部的局部变量,不存在多个线程共享问题。
下面来看一个例子:
变量sb是使用在自己内部,为main线程里的一个局部变量,所以该局部变量不可能被其他线程使用,所以三个append操作不需要加锁都是安全的。
上面代码中并不涉及多线程的情况下,使用了StringBuffer,我们知道StringBuffer是线程安全的,使用了synchronized关键字,所以此时并没有提高效率调用一次append方法获取释放锁,降低了效率。在这种情况下,就不需要使用锁,就采用了锁消除,将锁干掉。
5、锁粗化
锁粗化:锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。一般是用在类变量上因为可能有好几个方法调用(线程)
这里涉及到方法区里,类里边有共享变量(sb为类变量,是共享的),所以不能保证该变量有可能被其他线程在使用,因此,将三个append方法的加锁解锁合并为一个加锁解锁操作,即使有其他线程在使用,合并成为一个加锁解锁操作后也需要竞争锁,保证不受影响。
synchronized 优化总结:
1.编译器+JVM 判断锁是否可以被消除,如果可以,直接消除;
2.第一个线程,优先进入偏向锁状态,该线程的加锁-释放锁操作,非常快;
3.随着其他线程参与竞争,偏向锁状态被消除,进入轻量级锁(用户态锁),用户态锁中加入了智能的自旋状态(空转);
4.如果一定条件满足不了轻量级锁就会膨胀为重量级锁(OS: :mutex),重量级锁是操作系统实现的,效率比较低,成本较高。