ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。
ReentrantLock内部实现依赖于AQS。
AQS(AbstractQueuedSynchronizer)是抽象队列同步器,在线程访问共享资源时候,会先判断资源是否上锁了,
如果上锁了,那么把该线程放入CLH队列中进行等待;
如果没上锁,那么把该线程设置为工作线程。
AQS的主要原理图:
AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。
ReentrantLock有三个内部类:
ReentrantLock默认实例化的非公平锁NonfairSync。可以通过传递false,来实例化公平锁FairSync
FairSync加锁过程:
调用lock()方法即可加锁。
加锁时先tryAcquire(1)尝试获取锁:
FairSync解锁过程:
调用unlock()方法即可解锁。注意,该方法一定要放在finally块中进行调用!
解锁时调用tryRelease(arg)方法
NonFairSync加锁解锁过程:
加锁过程不同于FairSync,NonFairSync会在调用lock()时,直接尝试把state设置为1:
解锁过程和FairSync一致。
FairSync加锁的主要方法:
lock():
public void lock() {
sync.lock();
}
看到lock()方法其实就是调用Sync的lock()方法,而Sync的Lock有NonFairSync和FairSync两种实现,这边先看FairSync的实现:
final void lock() {
acquire(1);
}
该方法的参数1,实际上即是state的值,1表示加锁。继续看acquire()方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire()调用了三个方法,分别是:
先看tryAcquire()
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state的值
int c = getState();
// state等于0,即共享资源没有被加锁
if (c == 0) {
/*
先看队列中有没有线程在等待获取锁,
如果没有那就设置当前线程就是工作线程
如果有就返回false,此处体现了公平锁特性
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置当前线程为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前工作线程是该线程,那么可以重复获取锁,并将state的值+1,此处体现了ReentrantLock是可重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// int有最大值,超过最大值后,state会变成负数,因此加了这个判断
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 共享资源被锁定,返回false
return false;
}
这个方法的第一个if判断,提现了获取锁是公平的;第二个则体现是ReentrantLock是可重入锁。
看一下hasQueuedPredecessors()方法,
hasQueuedPredecessors():
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 头结点不等于尾节点,并且,(头结点的next不为null,或者next的线程不是当前线程)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
该方法的解析:https://blog.csdn.net/weixin_38106322/article/details/107154961
addWaiter():
private Node addWaiter(Node mode) {
// 把当前线程作为入参创建一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 如果队列的尾节点不为null,把node添加到队尾
if (pred != null) {
node.prev = pred;
// cas设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 将节点插入队列,如果队列为空则初始化队列
enq(node);
return node;
}
addWaiter(),就是把当前线程添加到队列中。 其中的enq()是在快速添加node到队尾失败才会调用,而这个方法也是添加node到队列中。
acquireQueued():
final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到资源
boolean failed = true;
try {
// 标记等待过程中是否中断过
boolean interrupted = false;
// 开始自旋,要么获取锁,要么中断
for (;;) {
// 获取当前节点的前节点
final Node p = node.predecessor();
// 如果前节点是头结点(工作节点)并且tryAcquire()成功
if (p == head && tryAcquire(arg)) {
// 把node设置为头结点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 靠前驱节点判断当前线程是否应该被阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞当前节点
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果执行失败,则把这个节点状态设置为取消状态
if (failed)
cancelAcquire(node);
}
}
这边多了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()方法。
shouldParkAfterFailedAcquire():靠前驱节点判断当前线程是否应该被阻塞
parkAndCheckInterrupt():阻塞当前节点
// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取头结点的节点状态
int ws = pred.waitStatus;
// 说明头结点处于唤醒状态
if (ws == Node.SIGNAL)
return true;
// 通过枚举值我们知道waitStatus>0是取消状态
if (ws > 0) {
do {
// 循环向前查找取消节点,把取消节点从队列中剔除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 设置前任节点等待状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 当前线程被唤醒后,返回线程的中断状态,然后重置中断状态
return Thread.interrupted();
}
FairSync解锁的主要方法:
在调用lock.unlock()时候,内部调用的是Sync的release()方法,源码:
public void unlock() {
sync.release(1);
}
release():
public final boolean release(int arg) {
// 释放锁
if (tryRelease(arg)) {
Node h = head;
// 如果当前头结点不为null,并且waitStatus不是0(无锁状态)
if (h != null && h.waitStatus != 0)
// 唤醒下一个线程开展业务
unparkSuccessor(h);
return true;
}
return false;
}
release()会调用tryRelease()方法:
tryRelease():
protected final boolean tryRelease(int releases) {
// 获取state的值,并且减去1(releases等于1)
int c = getState() - releases;
// 判断当前线程是不是工作线程,如果不是,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 共享资源是否空闲的标记
boolean free = false;
// 如果state为0(空闲)
if (c == 0) {
// 设置空闲标记为true,并把当前工作线程设置为null
free = true;
setExclusiveOwnerThread(null);
}
// 设置state等于0
setState(c);
return free;
}
tryRelease()会释放一次锁,不过ReentrantLock是可重入锁,因此释放一次后,共享资源不一定会处于空闲状态。
unparkSuccessor():
private void unparkSuccessor(Node node) {
// 获取头结点waitStatus
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取当前节点的下一个节点
Node s = node.next;
// 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
if (s == null || s.waitStatus > 0) {
s = null;
// 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果当前节点的下个节点不为空,而且状态<=0,就把当前节点unpark
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor()就是唤醒线程进行工作。只有当唤醒下一个线程后,release()方法才会返回true。
NonFairSync加锁的主要方法:
对比FairSync的源码,会发现两者的区别就是在于lock方法:
lock():
final void lock() {
// 第一步直接尝试设置state的值(不公平啊)
if (compareAndSetState(0, 1))
// 设置成功,设置当前线程为工作线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 设置失败,再去尝试获取锁
acquire(1);
}
不公平锁加锁时,会直接尝试设置state的值。
再看acquire()方法,
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到和FairSync的acquire()方法一样,唯一不同的是tryAcquire()的不同;
tryAcquire():
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
tryAcquire()调用的是nonfairTryAcquire(),
nonfairTryAcquire():
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
// 如果共享资源处于空闲状态
if (c == 0) {
// 尝试设置直接设置state(不管队列中是否有排队的线程直接就插队,不公平啊)
if (compareAndSetState(0, acquires)) {
// 设置state成功,就把当前线程设置为工作线程
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;
}
对比FairSync的tryAcquire()方法和nonfairTryAcquire()方法,可以看出唯一的区别就是nonfairTryAcquire()方法少了一个hasQueuedPredecessors()的判断,这个判断是判断CLH队列中有没有线程在等待获取资源。而非公平锁就不管等待队列是否有等待线程,直接去尝试获取资源(不公平)。
1.lock.lock()是怎么锁定资源的?
通过AQS中变量state的值来判断这个资源有没有被锁定。
当代码执行到lock.lock()时,会判断state的值,
如果state等于0,那么获取锁并将state设置为1。
如果state不等于0,但是当前线程就是加锁线程,那么获取锁定资源,并把state+1
否则,调用LockSupport.park(this),把当前线程挂起。获取锁定资源时候,会把当前线程设置为独占线程。
2.lock.unlock()是怎么释放锁的?
先把state-1,并判断当前线程是不是独占线程,如果不是,抛出IllegalMonitorStateException异常
然后判断state是否为0?
如果等于0,把独占线程设置null,并返回true
如果不等于0,把state值设置为新的state,并返回false
3.锁被释放后,如何通知队列中的线程过来工作的?
在调用unlock()时候,如果tryRelease()成功,并且head节点不为null和head节点的waitStatus不等于0(0是初始值)
这时会调用LockSupport.unpark(s.thread)唤醒等待节点过来工作
4.等待线程被唤醒后工作后,会不会从CLH队列中移除
在等待线程被唤醒后工作后,会把头结点设置这个等待节点,等待节点会被设置为头结点,并把之前头结点的next指针指向null
5.ReentrantLock中公平锁和不公平锁的区别?
ReentrantLock中有两个内部类,分别是:NonfairSync(不公平锁)、FairSync(公平锁),ReentrantLock默认实例化的是不公平锁。
FairSync:加锁的时候会进行先看state是否等于0,如果是,那么会再去看队列中有没有线程在等待执行,如果有,那么就加入队尾等待执行。如果没有,才执行线程
NonfairSync:加锁时,先尝试设置state为1,成功则获取锁,否则则再去获取state的值,如果此时state等于0,那么直接获取锁,不管队列中是否还有没有线程在等待执行。如果不等于0,那么该线程加入队尾排队执行。
因此:
两个锁的区别在于加锁的时候。除此之外,其他逻辑一样。
6.AQS原理
AQS是抽象队列同步器,在线程访问共享资源时候,会先判断资源是否上锁了,
如果上锁了,那么把该线程放入CLH队列中进行等待;
如果没上锁,那么把该线程设置为工作线程。
7.Reentrantlock中的condition的使用方法和原理?
使用lock.newCondition();可以构造一个condition对象。
condition主要作用于线程之间通讯,功能类似Object的wait()和notify()
每个condition内部维护了队列,队列存放等待执行的线程,每调用一次condition.await(),队列都会增加,调用conditionA.signal();队列会移除元素。
condition.await();使当前线程等待,直到它被通知或中断。该方法会释放当前锁,并将当前线程放入CLH队列队尾中进行等待,然后唤醒condition中队列的第一个元素。
conditionA.signal();唤醒一个等待的线程。唤醒condition中队列的第一个元素。
8.独占锁和共享锁的区别?
独占锁:独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
ReentrantLock 和 synchronized 都是独占锁
共享锁:共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,能加排它锁。获得共享锁的线程只能读数据,能修改数据。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
ReentrantReadWriteLock:读锁是共享锁,写锁是独占锁。读锁的共享可以保证并发读是高效的,读写,写读,写写是互斥的