synchronized关键字在Java语言层面提供了同步功能。
synchronized同步的形式
Java中的任何对象都可以被锁。
有以下三种形式:
- 对于普通同步方法,锁住的是当前实例对象
- 对于静态同步方法,锁住的是当前类的Class对象
- 对于同步方法块,锁住的是synchronized括号里配置的对象
这三种形式在jvm里都是由管程来支撑的。
同步代码块在字节码层面插入了monitorenter和monitorexit指令,用于实现对管程的调用。
方法级的同步是隐式的,不需要在字节码中插入指令,它实现在方法调用和返回之中。JVM从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志位来确定一个方法是否为同步方法。如果调用指令检查到方法为同步方法,则JVM要求当前执行线程必须先成功持有管程。
Java对象头
在hotspot虚拟机中,synchronized用的锁存在Java对象头里。非数组对象头分为两部分:
- Mark Word,存储对象自身的运行时数据,如hashcode,GC分代年龄,锁标志位等。
- 存储指向方法去对象类型数据的指针
ps:数组对象额外一部分用于存储数组的长度。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
锁状态 | 存储内容 | 锁标志位 |
---|---|---|
无锁 | 对象hashcode,GC分代年龄,偏向锁标志位:0 | 01 |
偏向锁 | ThreadID,Epoch,对象分代年龄,偏向锁标志位:1 | 01 |
轻量级锁 | 指向栈中锁记录的指针/ | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针/ | 10 |
GC标记 | 空/ | 11 |
JVM线程状态转化机制
线程阻塞/等待队列图:
工作原理:当多个线程同时请求某个管程时,管程会设置几种状态来区分和组织协调请求的线程
- Contention List:所有请求锁的线程首先添加至竞争队列
- Entry List:Contention List中有资格成为候选线程的添加至Entry List
- Wait Set:调用wait()方法阻塞的线程添加至wait set
- OnDeck:任何时候最多只能有一个线程正在竞争锁,即OnDeck
- Owner:获得锁的线程
- !Owner;释放锁的线程
具体过程:
Contention List:
当有新线程竞争管程时,先添加至Contention List。Contention List是一个LIFO队列,新线程通过CAS操作将队列头节点设置为自己,再将next引用指向之前的头结点。Owner线程从队列尾取元素。
Entry List:
Entry List 和Contention List逻辑上同属等待队列,因为Contention List会被多个线程并发访问,就涉及到等待或者阻塞,为了尽可能利用CPU时间,建立Entry List。Owner线程在unlock时会从Contention List中迁移线程至Entry List,并指定EntryList的head节点为OnDeck。Owner并不是直接将锁传递给OnDeck,而是将竞争锁的权利交给OnDeck,OnDeck需要重新竞争锁,也就是由OnDeck主动获取。正如前面所说,Owner线程会与其他线程并发访问Contention List,存在等待或者阻塞的情况,如果让Owner线程直接交给OnDeck锁,就有可能出现Owner线程阻塞在Contention List上而不能及时将锁交付,浪费CPU时间。
Wait Set:
如果Owner线程被wait方法阻塞,则转移到WaitSet队列,当在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。
自旋
在Contention List和Entry List中的线程均处于BLOCKED状态,Wait Set中的线程处于WAITING状态,所以直接进入等待队列会引起线程上下文切换。实际上,很多时候共享数据的锁定状态只会持续很短的时间,为了这段时间去阻塞和恢复线程并不值得。当发生竞争时,竞争线程在队列外自旋一段时间,在Owner线程释放锁之后,竞争线程可能立即得到锁,从而避免了线程阻塞带来的开销。
锁升级机制
Java 1.6引入了锁升级机制,提高了synchronized的同步性能。锁一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,这几个状态随着竞争情况的越来越激烈而逐步升级。锁只能升级而不能降级。
线程每次尝试竞争锁时,会首先在当前线程栈帧中建立一个锁记录(Lock Record),用于存储该对象Mark Word的拷贝(Displaced Mark Word)。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,由此背景引入了偏向锁。
加锁
当一个对象Mark Word中的锁标志位为01(无锁/可偏向)时,
- 当偏向锁标志位为0时,说明不是偏向锁,该对象管程未被任何线程获取,当前线程通过CAS操作尝试在对象Mark Word中记录当前线程ID,同时将偏向锁标志位置为1,当前线程即持有该对象的偏向锁。从此以后,持有偏向锁的线程进入该对象管程时,虚拟机将不再做任何同步操作。
- 当偏向锁标志位为1时,说明该对象已经有所偏向,如果偏向线程ID并不是当前线程,则尝试用CAS操作在Mark Word中记录自己的线程ID,如果成功,则该对象重偏向至当前线程。
解锁
如果重偏向失败,即已经出现竞争,偏向锁才会解锁。根据锁对象是否处于被锁定的状态,撤销偏向后对象要么重偏向,要么恢复到无锁状态,要么升级为轻量级锁–将锁标志位置为00。
轻量级锁
加锁
当偏向锁膨胀为轻量级锁后,线程再次竞争锁时,将使用CAS尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功,则当前线程进入管程。如果失败,当前线程自旋获取锁。如果自旋获取锁失败,则竞争情况变得更加激烈了,锁膨胀为重量级锁,锁标志位置为10,同时当前线程进入阻塞态。
解锁
轻量级锁解锁时,会使用CAS操作尝试将Displaced Mark Word替换为对象头,如果成功,则表示在执行同步代码期间没有竞争发生或者竞争线程自旋没有结束。如果失败,说明其他线程在同步期间竞争失败了,在释放锁的同时,需要唤醒阻塞线程。
重量级锁
重量级锁的加锁和解锁由状态转化机制协调。
对比
锁 | 背景 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
偏向锁 | 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得 | 加锁和解锁过程没有额外的开销 | 如果存在竞争,将带来额外的撤销开销 | 竞争不激烈的情况 |
轻量级锁 | 在许多应用中,共享数据的锁定状态只会持续很短的一段时间 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果自旋竞争失败,会浪费CPU时间,还额外带来了CAS操作的开销(重量级锁没有CAS操作) | 同步块执行速度非常快的情况 |
重量级锁 | 竞争激烈情况下必须程序正确同步 | 线程竞争不使用自旋,不会浪费CPU | 线程阻塞,用户态内核态的切换,线程上下文切换等开销 | 竞争激烈,同步块执行时间长 |