java线程同步原理主要2个概念:互斥(mutual exclusion)和可见性。
其中互斥保证了在同一时刻只有一个线程可以访问临界区,可见性保证一个线程对共享变量的修改能够及时被其他线程看到。 第3节我们简单介绍了内存可见性相关内容,并在4、5分别介绍了因缓存一致性导致的伪共享问题和代码重排序。这一章节主要了解下互斥相关。 Java提供了2种锁机制来控制多个线程对共享资源的互斥访问。下文分别从可重入性、公平性了解这2种锁的实现方式。
-
- JVM实现的synchronized内置锁,在软件层面依赖JVM
-
- JDK实现的ReentrantLock显式锁,在硬件层面依赖特殊的CPU指令
互斥锁
锁的名字千千万,但首先锁分为内置锁/隐式锁/自动锁和显式锁。关于锁的共享与独占,是否可重入(避免死锁),公平非公平,可中断锁的概念会在之后进行介绍。本节主要了解说明内置锁和显式锁。
互斥锁关键属性
1. 锁标识
锁标识用于识别成功获得锁。
2. 锁等待队列
锁等待队列用于锁线程等待队列。
3. wait队列
wait队列用于wait()后的等待队列。
synchronized
以最简单的synchronized为例,synchronized(this)中的块,可以保证同时只有一个线程执行。
synchronized实现逻辑
synchronized重量级锁的逻辑在JVM的ObjectMonitor类,由c++实现。关键属性有:
-
_owner用来保存当前持有锁的线程
-
_recursions用于保存重入次数(支持可重入)每次重入+1,释放时-1
-
_cxq先进后出的队列和_EntryList都用于存放等待锁的线程,默认情况下一个线程抢锁失败就会进入_cxq
两者的区别在持有锁的线程释放锁时,会唤醒等待队列中的线程。首先会看_EntryList中有没有元素,如果有唤醒_EntryList的头节点,如果为空则会把_cxq复制给_EntryList,从_cxq头部唤醒。
用两个队列是因为线程获取和释放锁的时候,wait()notify()的时候都会涉及到出入_cxq的操作,如果使用一个队列会增加冲突的概率。加入_EntryList只有当前持有锁的线程操作,只要这个队列不为空,获取和释放锁没有竞争问题,提高锁的效率。
-
_WaitSet用于存放调用wait()方法的线程
锁等待队列_cxq和wait队列_WaitSet实现上的不同。_cxq为双向链表,前入队,FILO_WaitSet为回环链表,后入队,FIFO。
ObjectMonitor() {
// 多线程竞争锁进入时的单向链表
ObjectWaiter * volatile _cxq;
//处于等待锁block状态的线程,会被加入到该列表
ObjectWaiter * volatile _EntryList;
// _header是一个markOop类型,markOop就是对象头中的Mark Word
volatile markOop _header;
// 抢占该锁的线程数,约等于WaitSet.size + EntryList.size
volatile intptr_t _count;
// 等待线程数
volatile intptr_t _waiters;
// 锁的重入次数
volatile intptr_ _recursions;
// 监视器锁寄生的对象,锁是寄托存储于对象中
void* volatile _object;
// 指向持有ObjectMonitor对象的线程
void* volatile _owner;
// 处于wait状态的线程,会被加入到_WaitSet
ObjectWaiter * volatile _WaitSet;
// 操作WaitSet链表的锁
volatile int _WaitSetLock;
// 嵌套加锁次数,最外层锁的_recursions属性为0
volatile intptr_t _recursions;
}
synchronized加锁过程
- 一个线程尝试获取锁尝试通过CAS将_owner变成当前线程(null -> 当前thread),更新成功则抢锁成功。
- 更新失败,则会使用前入队方式在_cxq等待(后入队的在头部),队列中元素类型为ObjectWaiter。
因为步骤1中不管等待队列有没有在等待的元素,都会通过CAS来尝试获取锁,同时_cxq的先进后出逻辑后来的线程可能会先被唤醒,所以是非公平锁。
synchronized使用方法
可以修饰在不同层级:修饰实例方法、修饰静态方法、修饰代码块。通过在对象->对象头->mark word标记字段中修改锁状态标志,
- 修饰实例方法,常量池多了ACC_SYNCHRONIZED标示符,根据标示符实现方法同步。调用指令时,会检查标示是否存在,如果设置执行线程就先获取monitor,获取后才能执行方法体,执行完后释放monitor。
public synchronized void synchronisedCalculate() {
setSum(getSum() + 1);
}
- 修饰静态方法
public static synchronized void syncStaticCalculate() {
staticSum = staticSum + 1;
}
- 修饰代码块,修饰代码块时会通过monitorenter和monitorexit指令获取Monitor所有权和退出Monitor。
public void performSynchronisedTask() {
synchronized (this) {
setCount(getCount()+1);
}
}
锁升级
除了无锁状态,Synchronized还有偏向锁、轻量级锁、重量级锁概念。
Lock
Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在 Lock的实现中必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面可以有所不同。以最简单的Lock为例,lock()加锁unlock()释放锁。Lock中while(isLocked)为自旋锁,
public class Counter{
private Lock lock = new Lock();
private int count = 0;
public int inc(){
lock.lock();
int newCount = ++count;
lock.unlock();
return newCount;
}
}
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
Lock的实现逻辑
上面提到要实现互斥锁需要锁标识和锁等待队列。在AQS中分别对应state整数和exclusiveOwnerThread,存储当前持有锁的线程。使用一个双向链表存储当前等待锁的队列。双向链表的head节点是一个空节点,FIFO新加入的节点会加入队列尾部,唤醒时会唤醒头部的节点。
等待队列的节点封装在Node类中
/**
* The synchronization state.
* AbstractQueuedSynchronizer
*/
private volatile int state;
/**
* AbstractOwnableSynchronizer
*/
private transient Thread exclusiveOwnerThread;
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
Lock加锁过程
以ReentrantLock非公平锁为例。
- 修改锁标识,看state属性是不是为0,如果是0则使用CAS将state更新为1。更新成功1则表示获取锁成功。将当前线程设置为exclusiveOwnerThread。
- 如果state不为0,根据exclusiveOwnerThread判断是否可重入锁,如果锁重入,则将state值直接加为1,返回成功。
- 如果不是exclusiveOwnerThread不是当前线程,其他线程持有锁,则需要进入锁等待队列中。
- 入队检查head节点是否存在,如果为空说明等待队列还没有初始化,就会初始化一个dummy空节点,初始化好后,head跟tail都会指向这个节点。队列初始化结束。
- 初始化后,节点就可以入队了,入队的线程将自己的prev指针指向tail节点,通过CAS将tail更新为新入队的节点。
- 新入队的线程会再检查一次,是不是队列中的第一个可用节点,如果是则重新尝试获取一次锁,防止自己入队过程中,持有锁的线程已经把锁释放掉了。
- 入队之后,把前置节点的waitStatus属性设置为SIGNAL。告诉前置节点当前节点先park一下,等轮到当前节点了再unpark。这样就不用轮训等待。
释放锁过程
- 检查state是否大于1,如果大于1锁重入,那么state-1,返回成功。
- 如果state等于1,那么将exclusiveOwnerThread设置为空,state设置为0,释放成功,但还要检查等待队列中有没有线程需要被唤醒。
- 如果等待队列中有节点需要释放,那么从等待队列后面的节点开始查找,找到第一个waitStatue等于SIGNAL的节点,会唤醒第一个waitStatue等于SIGNAL节点的下一个节点,将对应的节点unpark一下。这样释放锁就结束了。
- 被唤醒的线程开始抢锁,如果抢锁成功,则开始出队。
- 出队时,head节点指向自己,把自己的Thread属性设置为空,最后断开与原head的双向指针。
Lock相关一些问题
-
锁等待队列什么时候初始化?
发生第一次锁竞争的时候,第一个线程要入队的时候才会初始化等待队列。
-
公平锁和非公平锁的区别是什么?
非公平锁调用lock()的时候会先尝试抢锁。如果这个时候刚好有持有锁的线程释放就抢到了。公平锁看到等待队列中已经有线程在等待了,那么就会入队。
-
为什么等待队列中的第一个节点是空dummy节点?
因为释放锁的线程在唤醒等待线程时,通过前一个线程的waitStatus字段来判断后续节点是否可以被唤醒。如果没有空dummy节点,那么第一个节点就没有前置状态可以判断了。
Synchronized和Lock区别
无法中断、无法实现非阻塞这些都是不满足于使用隐式锁synchronized的原因。Lock在加锁和内存上提供的语义与与内置锁相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁。
- Lock加解锁的顺序灵活
- Lock可通过lockInterruptibly中断,Synchronized不可中断
- Lock可以被多个线程持有例如读写锁ReadWriteLock中读锁/StampedLock实现乐观读(多个线程并发读,一个线程写),Synchronized没有并发读
- Lock可设置为公平锁或非公平锁
- Lock底层原理是AQS和CAS,Synchronized底层是互斥Mutex Lock
- Lock的等待和唤醒通过Condition的await和signal实现,Synchronized的等待和唤醒通过wait和notify实现。都为同步队列和等待队列的切换。
- Lock有和Synchronized都只有一个同步队列,但是Lock有多个等待队列,Synchronized只有一个等待队列。
- Lock的tryLock可以实现非阻塞,Synchronized只有阻塞
- Lock支持定时的锁等待,可以通过tryLock设置超时机制,synchronized无超时机制
- Lock可定时可轮训
相关概念
- 线程同步
同步是在互斥的基础上,通过其他机制实现访问者对资源的有序访问。
- 同步队列
等待竞争锁的队列
- 等待队列
等待被唤醒后进入同步队列以获取竞争锁的队列
参考: