学习过程中参考了:
JUC锁: ReentrantLock详解 | Java 全栈知识体系 (pdai.tech)
1LockSupport
1.1 LockSupport简介
LockSupport用来创建锁和其他同步类的基本线程阻塞原语。
1.2 核心方法
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);
park:阻塞线程,并且线程在发生以下几种情况之前都会被阻塞:
- 调用unpark方法,释放线程许可。
- 该线程被中断了。
- 设置时间到了。time为绝对时间时,isAbsolute为true。否则isAbsolute为false。当time等于0时,无限等待直到unpark发生。
unpark:释放线程许可,激活调用park后阻塞的线程。这个方法不安全,调用时需要确保线程依旧存活。
park方法和unpark方法均由sun.misc.Unsafe的实现。
sun.misc.Unsafe的介绍可参考:JUC原子类: CAS, Unsafe和原子类详解 | Java 全栈知识体系 (pdai.tech)
图片来自于参考的文章中
1.3 CAS
在这里提一下CAS(Compare And Swap):通过比较两个值是否相等然后原子的更新某个位置的值(通过比较期望值和当前是否相等来决定是否更新新的值)。而Unsafe在Java的CAS操作中有广泛应用。各个原子类的实现也依赖于Unsafe类。
CAS有一个问题ABA问题:
因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
1.4 wait/notify与park/unpark的区别
wait方法会释放掉当前线程拥有的锁,并让线程陷入阻塞状态,从而给其他线程争夺锁的机会。notify会唤醒被wait阻塞的线程,重新去争夺锁。所以一定要先wait,再notify,否则陷入阻塞的线程不能继续执行下去。
而park/unpark方法不会因为调用顺序的先后而导致阻塞,所以park比wait/notify更加的灵活。另外wait方法必须搭配锁使用,而park不需要。当一个线程被park之后,调用该线程的interrupt方法,可以让该线程继续执行。
2 AQS
2.1 简介
AQS(AbstractQueuedSynchronizer)是一个用来构建锁和同步器的框架,使用AQS能够简单高效地构造出应用广泛的大量同步器。
2.2 AQS核心思想
如果请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制AQS使用CLH队列锁实现,即将暂时获取不到锁的线程加入到队列中去。
CLH队列是一个虚拟的双向队列(虚拟是指不存在队列实例,仅存在节点之间的关联关系)。AQS将每一个请求共享资源的线程封装成一个CLH锁队列的一个节点来实现锁的分配。
AQS使用int成员变量来标识同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现值的修改。
2.3AQS对资源的共享方式
AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLocak。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的顺序,先到者先拿到锁。
- 非公平锁:当线程要获取锁的时候,无视队列顺序直接去抢锁,谁抢到就是谁的。
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。
2.4 AQS使用了模板方法模式
自定义同步器的时候需要实现以下几个方法:
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
3 Lock
3.1 简介
Lock
实现提供比使用synchronized
方法和语句更广泛的锁操作。 Loc允许更灵活的结构化。
3.2 ReentrantLock源码分析
ReentrantLock是一个可重入锁(当一个线程已经拥有了所资源后可以再次申请所资源且不会发生阻塞),所以lock次数要和unlock次数对得上。
ReentrantLock是Lock的一个具体实现。其内部有三个类:
图片来自于参考的文章中
释放锁和获取锁的具体实现依赖于这三个类。
ReentrantLock实现了Lock接口公布了两个方法:
lock:获取锁
unlock:释放锁
上图是ReentrantLock对于lock和unlock方法的实现。可以看出获取锁和释放锁操作是由syn对象实现的。
首先分析Lock方法。Lock方法具体有NonfairSync(非公平的)和FairSync(公平的)实现,两者代码实现差不多,公平是指当锁资源被解放后,需要抢夺锁的线程是有序的,CLH队列中先进先出的获取锁资源,非公平是指争夺锁资源的时候可以不按照顺序。以公平锁为例:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);//调用了父类AbstractQueuedSynchronizer的acquire方法,独占锁资源
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
//这个方法在父类的acquire方法中被调用,方法作用是尝试获取锁资源
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取锁状态
if (c == 0) {
/**hasQueuedPredecessors()方法通过判断当前节点之前是否
存在待执行的节点判断当前节点时候可被加锁**/
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;
}
}
AbstractQueuedSynchronizer的acquire方法:
//获取独占锁
public final void acquire(int arg) {
if (!tryAcquire(arg) && //调用子类的具体实现,尝试获取锁资源
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //创建等待独占资源的节点并不停获取锁资源
selfInterrupt();
}
AbstractQueuedSynchronizer的addWaiter方法:
//在队列的尾部新增等待节点并返回尾结点
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;
}
}
//尾结点为null(未被初始化过)或者设置尾结点失败
enq(node);
return node;
}
AbstractQueuedSynchronizer的enq方法:
//在队列尾部增加当前节点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 尾结点为空(没有被初始化)这个if中其实就是初始化操作
if (compareAndSetHead(new Node()))//设置头结点为新的节点
tail = head;//尾结点指向头结点
} else {
node.prev = t;//将当前节点放到尾结点后
if (compareAndSetTail(t, node)) {//当前节点成为尾节点
t.next = node;
return t;
}
}
}
}
AbstractQueuedSynchronizer的acquireQueued方法:
//这个方法以自旋的方式去获取锁资源
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;//获取所资源成功
return interrupted;
}
//当前节点的前置节点不是头结点或者设置头结点失败
if (shouldParkAfterFailedAcquire(p, node) && //判断当前节点是否应该阻塞
parkAndCheckInterrupt()) //阻塞当前节点
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
AbstractQueuedSynchronizer的parkAndCheckInterrupt方法:
//通过LockSupport的park方法阻塞当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//通过sun.misc.Unsafe的park方法实现
return Thread.interrupted();
}
以上是lock方法的实现。接下来分析unlock方法:
sync对象并没覆盖release方法所以调用的是AbstractQueuedSynchronizer的release方法:
//释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放锁调用Sync方法的具体实现全
Node h = head;
if (h != null && h.waitStatus != 0)//头结点不为空且头结点不是无状态的
unparkSuccessor(h);//设置新的头结点并使下一个节点取消阻塞状态
return true;
}
return false;
}
Sync中的tryRelease方法:
//尝试释放锁资源,返回所资源是否被完全释放
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;
}
AbstractQueuedSynchronizer中的unparkSuccessor方法:
//
private void unparkSuccessor(Node node) {
//头结点状态
int ws = node.waitStatus;
if (ws < 0)//头结点不是无状态的
compareAndSetWaitStatus(node, ws, 0);//头结点无状态
//头结点的后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {//后继节点不为空或者状态为取消
s = null;//后继结点为空节点
//从后往前找到最前方状态不为取消或者无状态的节点待,即待执行的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;//
}
if (s != null)//存在待执行节点
LockSupport.unpark(s.thread);//取消待执行节点的线程的阻塞状态
}
总结一下类与类之间的关系: