Lock底层原理—ReentrantLock、AQS、Condition
文章目录
先来看看J.U.C包下的结构
- juc-locks 锁框架
- juc-atomic 原子类框架
- juc-sync 同步器框架
- juc-collections 集合框架
- juc-executors 执行器框架
而我们今天的主角ReentrantLock
,就是juc-locks包下的。上一篇刚算了解一下synchronized
的底层原理,所以就想看看ReentrantLock
和它的区别到底是什么,改进在哪里,适用于什么场景。
1. Lock
Lock
和ReadWriteLock
是两大锁的根接口,下面看一下JDK 1.8 API中如何描述的
通过API的介绍,sychronized
和Lock
的区别可以分为如下:
Lock
增加了灵活性,最主要的就是支持多个Condition。Lock
提供了非阻塞获得锁的方式,synchronized
有自旋锁。Lock
更适合使用在复杂的同步,而synchronized
更简单、简杰。Lock
可以设置公平锁和非公平锁,synchronized
只有非公平锁。
浏览一下Lock
源码
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
//获得锁
void lock();
//获取锁定,除非当前线程中断
void lockInterruptibly() throws InterruptedException;
//只有在调用时才可以获得锁
boolean tryLock();
//如果在给定的等待时间内是空闲的,并且当前的线程尚未得到 interrupted,则获取该锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁。
void unlock();
//返回一个新Condition绑定到该实例Lock实例
Condition newCondition();
}
2. ReentrantLock
Lock
了解之后,看一下实现类是如何实现接口的方法的。最常用的实现类就是ReentrantLock
。
公平锁和非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从构造函数上可以看出来ReentrantLock
实现了公平锁和非公平锁两种,默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多。
Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
NonfairSync
非公平锁实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
看一下最主要的lock方法
通过CAS判断当前状态是否为空闲状态(0),如果是则将状态设置为独占线程(1),这样获取锁成功。CAS操作只保证一个线程获取锁,如果多个线程竞争,失败的就需要排队。
所以else就是失败者走的,古有名言:失败是成功之母。那就看一下失败者的经历吧!!!
acquire
是父类AbstractQueuedSynchronizer
的方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire
方法Nonfair
重写了
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//我们走下来 acquires = 1
final Thread current = Thread.currentThread();
//获取锁状态
int c = getState();
//如果是0,表示没有线程拥有
if (c == 0) {
//获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程已经占用了此锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//更新state值
setState(nextc);
return true;
}
//尝试获取失败
return false;
}
第一个吃螃蟹的当不上,只能当下一个了。再次尝试获取,如果此时状态为空闲,则获取;如果有线程占用,判断是不是自己;如果是,更新状态值;如果不是,则失败。
!tryAcquire(arg)
-> acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
慢慢往下撸
private Node addWaiter(Node mode) {
//包装成一个结点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//无头节点,进入enq创建队列也就是头节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//CAS操作设置头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//设置成尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取前驱结点
final Node p = node.predecessor();
//作为第二节点,可以尝试获取锁
if (p == head && tryAcquire(arg)) {
//获取锁成功
setHead(node);//将当前节点设置为头节点
p.next = null;// help GC
failed = false;//更新failed
return interrupted;//返回是否被中断过
}
//锁未获取后,判断是否可以被挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
到现在有点走晕了,先捋一捋
- 通过
lock
获取锁,如果获取失败,则进入acquire
方法尝试再次获取。 acquire
中通过!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
进入两个方法,第一个是尝试第二次尝试获取锁,如果失败再次看第二个方法。addWaiter
将当前线程包装成结点加入同步队列。acquireQueued
方法addWaiter
返回、已入队的结点作为参数,然后第三次尝试获取锁(获取的条件是成为二号结点)。- 最后通过
shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
判断是否需要被挂起。
此时我们再看shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
如何实现的
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱结点的状态
int ws = pred.waitStatus;
//如果前驱结点可以符合条件(可以唤醒当前线程)
if (ws == Node.SIGNAL)
return true;
//如果不符合条件,则一直向前遍历,找到可以唤醒自己的结点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果都没有符合条件的结点,那就通过CAS条件将前驱结点的设置为signal
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//挂起当前线程,返回线程中断状态并重置
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这两个方法执行完成后,就代表着线程已经被挂起了,只能等待signal标志的线程来唤醒它。
static void selfInterrupt() {
//中断当前线程
Thread.currentThread().interrupt();
}
最后回到selfInterrupt
方法。
Node
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
waitstatus
查看状态值
- CANCELLED:当线程等待超时或者被中断,则取消等待,设等待状态为-1,进入取消状态则不再变化
- SIGNAL:后继节点处于等待状态,当前节点被取消或中断时会通知后继节点,使后继节点的线程得以运行
- CONDITION:当前节点处于等待队列,节点线程等待在Condition上,当其他线程对condition执行signall方法时,等待队列转移到同步队列,加入到对同步状态的获取
- PROPAGATE:与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态
- 0状态:值为0,代表初始化状态。
那么SHARED
和EXCLUSIVE
是干什么的呢?
SHARED
标识的是共享,而EXCLUSIVE
是独占。但是ReentrantLock
是排他锁。
FairSync
说完了非公平锁,那就不得不提公平锁的实现了。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
//非公平锁调用的是父类sync的nonfairTryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
有了非公平锁做铺垫,了解公平锁就方便多了。公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态。
公平锁和非公平锁最主要的区别在于lock()
,一个是先排队,一个是先抢锁。但是非公平锁的吞吐量更大。
3. AQS—AbstractQueuedSynchronizer
在之前看Java并发编程这本书时,就了解过AQS,但是只停留在表层。整好通过这次学习来好好深入的理解一下AQS,这也是面试会经常问到的东西。
Sync
就是AbstractQueuedSynchronizer
的子类,AQS采用了模板方法模式,提高了扩展性有保证了规范性。
JDK 1.8 API中的介绍如下
从上述介绍中可以看出:
- 同步器依赖于每个Node的状态,状态都是通过CAS来设置的。
- AQS支持独占模式和共享模式 。
还有一些关于
Condition
,下面再去研究。
首先AQS是一个抽象的队列同步器,AQS定义多线程情况下访问共享资源的同步器框架。底层使用了Unsafe类进行CAS操作。
MyLock
仿照ReentrantLock
来实现一个MyLock
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
* @author gyh
* @csdn https://blog.csdn.net/qq_40788718
* @date 2020/6/4 10:15
*/
public class MyLock extends AbstractQueuedSynchronizer {
public void lock() {
this.acquire(1);
}
public void unlock() {
this.release(1);
}
@Override
protected boolean tryAcquire(int arg) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0){
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()){
setState(1);
return true ;
}
return false ;
}
@Override
protected boolean tryRelease(int arg) {
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException() ;
setExclusiveOwnerThread(null);
setState(0);
return true ;
}
}
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author gyh
* @csdn https://blog.csdn.net/qq_40788718
* @date 2020/6/4 13:59
*/
public class MyLockDemo {
static int count = 0 ;
public static void main(String[] args) throws InterruptedException {
final MyLock lock = new MyLock() ;
ExecutorService executorsService = Executors.newCachedThreadPool() ;
CountDownLatch countDownLatch = new CountDownLatch(1000) ;
for (int i=0 ; i<1000 ; i++){
executorsService.execute(()->{
try{
lock.lock();
count++ ;
System.out.println("线程:"+Thread.currentThread().getName()+" 数值:"+count);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorsService.shutdown();
System.out.println(count);
}
}
测试了几次,感觉成功了,如果有问题请指出来。
4. Condition
Condition
是一个接口,从1.5的时候出现的,是用来替代Object
的wait
、notify
。所以显而易见,Condition
的await
和signal
肯定效率更高、安全性更好。Condition
是依赖于lock
实现的。并且await
和signal
只能在lock
的保护之内使用。
sychronized + Object.wait = Lock + Condition
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Date;
public interface Condition {
//导致当前线程等到发信号或 interrupted 。
void await() throws InterruptedException;
//使当前线程等待直到发出信号
void awaitUninterruptibly();
//使当前线程等待直到发出信号或中断,或指定的等待时间过去。
long awaitNanos(long nanosTimeout) throws InterruptedException;
//使当前线程等待直到发出信号或中断,或指定的等待时间过去。
boolean await(long time, TimeUnit unit) throws InterruptedException;
//使当前线程等待直到发出信号或中断,或者指定的最后期限过去。
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待线程。
void signal();
//唤醒所有等待线程。
void signalAll();
}
(网图)
从图片中可以看出AQS的整体框架的大致轮廓,ConditionObject
是Condition
的实现类。
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
可以看出这是队列的头和尾。
看一下两个最常用的方法
ConditionObject.await
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//1
Node node = addConditionWaiter();
//2
int savedState = fullyRelease(node);
int interruptMode = 0;
//3
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//4
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
- 把当前线程的节点加入到等待队列中
- 由于调用await()方法的线程是已经获取了锁的,所以在加入到等待队列之后,需要去释放锁,并且唤醒后继节点线程
- 挂起当前线程,当别的线程调用了signal(),并且是当前线程被唤醒的时候才从park()方法返回
- 当被唤醒后,该线程会尝试去获取锁,只有获取到了才会从await()方法返回,否则的话,会挂起自己
ConditionObject.signal
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
从first开始遍历等待队列,把第一个非空、没取消的节点transfer到同步队列
5. 同步队列与等待队列
AQS中存有同步队列,而Condition中存有等待队列。他们二者有什么区别?
同步队列存放着竞争同步资源的线程的引用;等待队列存放着待唤醒的线程的引用。
同步队列中存放着一个个节点,当线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,首节点表示的获取同步状态成功的线程节点。
Condition维护着一个等待队列,与同步队列相似。主要针对await和signal的操作。
借助博客:https://blog.csdn.net/MakeContral/article/details/78135531 的例子可能更好理解
public class CondtionTest {
public static class ThreadA extends Thread{
@Override
public void run(){
try{
lock.lock();
for (int i=0 ;i<3;i++){
System.out.println("A进程输出" + " : " + ++index);
conditionB.signal();
conditionA.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static class ThreadB extends Thread{
@Override
public void run(){
try{
lock.lock();
for (int i=0 ;i<3;i++){
System.out.println("B进程输出" + " : " + ++index);
conditionC.signal();
conditionB.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static class ThreadC extends Thread{
@Override
public void run(){
try{
lock.lock();
for (int i=0 ;i<3;i++){
System.out.println("C进程输出" + " : " + ++index);
conditionA.signal();
conditionC.await();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static ReentrantLock lock = new ReentrantLock();
public static Condition conditionA = lock.newCondition();
public static Condition conditionB = lock.newCondition();
public static Condition conditionC = lock.newCondition();
public static int index = 0;
public static void main(String[] args){
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
ThreadC threadC = new ThreadC();
threadA.start();//(1)
threadB.start();//(2)
threadC.start();//(3)
}
}
输出结果为:
这种情况是B线程先占到了锁了。
参考:
https://blog.csdn.net/m47838704/article/details/80013056
https://blog.csdn.net/zhangdong2012/article/details/81151661
https://blog.csdn.net/qq_28605513/article/details/84194624
https://blog.csdn.net/fuyuwei2015/article/details/83719444
https://www.cnblogs.com/yanlong300/p/9772271.html
https://blog.csdn.net/andyzhaojianhui/article/details/79361454