1 Synchronized
1.1 synchronized 关键字是什么?
synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
同步:是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者一些,当使用信号量的时候)线程使用。
临界区:指的是某⼀块代码区域,它同 ⼀时刻只能由⼀个线程执⾏。
最主要的三种使用方式:
-
修饰实例方法,锁为当前实例。
public synchronized void instanceLock(){ // code }
-
修饰静态方法,锁为当前Class对象。
public static synchronized void classLock() { // code }
-
修改代码块,锁为括号里面的对象。
public void blockLock() { Object o = new Object(); synchronized (o) { // code } }
总结:
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是给Class类上锁。synchronized
关键字加到实例方法上是给对象实例上锁。- 尽量不要使用
synchronized(String a)
因为JVM中,字符串常量池具有缓存功能!(要保证加锁对象的唯一性
1.2 synchronized 原理
synchronized 关键字底层原理属于JVM层面。
-
synchronized 同步语句块情况
public class TestSynchronized { public void method(){ synchronized (this){ System.out.println("test synchronized"); } } }
执行
javap -v -p TestSynchronized.class
反编译。
-
synchronized 修饰方法的情况
public class TestSynchronized { public synchronized void method(){ System.out.println("test synchronized"); } }
总结
通过对.class文件反编译可以发现:
- 同步方法通过
ACC_SYNCHRONIZED
修饰。 - 代码块同步使用
monitorenter
和monitorexit
两个指令实现。
虽然两者实现细节不同,但其实本质上都是JVM基于进入和退出Monitor对象来实现同步,JVM的要求如下:
monitorenter
指令会在编译后插入到同步代码块的开始位置,而monitorexit
则会插入到方法结束和异常处。- 每个对象都有一个
monitor
与之关联,且当一个monitor
被持有之后,他会处于锁定状态。 - 线程执行到
monitorenter
时,会尝试获取对象对应monitor
的所有权。 - 在获取锁时,如果对象没被锁定,或者当前线程已经拥有了该对象的锁(可重进入,不会锁死自己),将锁计数器加一,执行
monitorexit
时,锁计数器减一,计数为零则锁释放。 - 获取对象锁失败,则当前线程陷入阻塞,直到对象锁被另外一个线程释放。
wait/notify 等方法也依赖于monitor对象,这就是为什么只有在同步代码块或者方法中才能调用wait/notify 等方法,否则会抛出java.lang.IllegalMonitorStateException 的异常的原因。
1.3 synchronized JDK1.6 优化
早期的synchronized
JDK1.6之前属于重量级锁,依赖于操作系统的Mutex Lock,Java的线程映射到操作系统的原生线程之上,需要操作系统申请互斥量,操作系统对线程的切换,需要从用户态切换到内核态,比较耗时,效率低。
JDK1.6 以后
synchronized锁特性由JVM负责实现。在JDK的不断优化迭代中,synchronized锁的性能得到极大提升,包括锁粗化,锁消除,自适应自旋锁,偏向锁,轻量级锁。特别是偏向锁的实现。JVM底层是通过监视锁来实现synchronized同步的。
-
监视锁:即monitor,是每一个对象与生俱来的一个隐藏字段。
-
锁粗化:如果
虚拟机探测到有一串的操作都对同一个对象加锁
,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。public String concatString(String s1,String s2){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(s1); stringBuffer.append(s2); return stringBuffer.toString(); }
以上述代码为例,就扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可以了。(锁粗化不适合循环的场景!
-
锁消除:是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到
不可能存在共享数据竞争的锁
进行消除。锁消除的主要判断依据来源于逃逸分析
的数据支持。 -
自适应自旋锁:自适应意味着
自旋的时间不再固定
,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。 -
偏向锁:为了在资源
没有被多线程竞争的情况下
尽量减少锁带来的性能开销。在锁对象头中有一个ThreadId字段
;当第一个线程访问锁时,如果该锁没有被其他线程访问过,即ThreadId字段为空,那么JVM让其持有偏向锁,并将ThreadId字段的值设置为该线程的ID。当下一次获取锁时,会判断当前线程的ID是否与锁对象的ThreadId一致。如果一致,那么该线程不会再重复获取锁,从而提高了程序的运行效率。 -
轻量级锁:指当锁原来是偏向锁的时候,被另一个线程来竞争偏向锁时,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过
自旋(CAS)
的方式尝试获取锁,不会阻塞。
1.4 锁升级的过程
一个对象其实有四种锁状态,它们级别由低到高依次是:
- 无锁状态:没有对资源进行锁定,任何线程都可以尝试去修改它。
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
⼏种锁会随着竞争情况逐渐升级,锁的升级很容易发⽣,但是锁降级发⽣的条件会⽐较苛刻,锁降级发⽣在Stop The World期间
,当JVM进⼊安全点
的时候,会检查否有闲置的锁,然后进⾏降级。
Java对象头的组成
锁存在于Java对象头里,对象头的组成部分:
- Mark Word : 存储对象的hashCode或锁信息等。
- Class MetaData Address: 存储到对象类型数据的指针。
- Array length:数组的长度(如果当前对象是数组
Java 对象头又存于Java 堆中,堆内存分为三部分:对象头、实例数据和对齐填充。
Java对象头的Mark Word 中记录了对象和锁的相关信息。
可以看到,当对象状态为偏向锁时, Mark Word 存储的是偏向的线程ID
;当状态为 轻量级锁时, Mark Word 存储的是指向线程栈中 Lock Record 的指针
;当状态为重 量级锁时, Mark Word 为指向堆中的monitor对象的指针
。
锁升级的过程