介绍
提供了一个基于FIFO队列,可以用于构建锁或其他相关同步装置的基础框架。
核心成员变量如下:
- 该同步器使用了一个state(int类型)来表示状态,期望它能成为大部分同步需求的基础。
- 提供了一个FIFO队列(双向链表),Node元素保存着线程引用和线程状态的容器,每个线程对同步器的访问,都可以看做是队列中的一个节点。
//等待队列的头,延迟初始化。除了初始化,只能通过setHead方法进行修改。
//如果head存在,则保证其waitStatus不会取消
Node head;
//等待队列的尾,延迟初始化。只能通过enq添加新的等待节点
Node tail;
//同步状态
int state;
Node解析
节点成为sync队列和condition队列构建的基础,在同步器中就包含了sync队列。同步器拥有三个成员变量:sync队列的头结点head、sync队列的尾节点tail和状态state。对于锁的获取,请求形成节点,将其挂载在尾部,而锁资源的转移(释放再获取)是从头部开始向后进行。对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。
class final Node{
//指示节点正在共享模式下等待的标记
Node SHARED = new Node();
//指示节点正在以独占模式等待的标记
Node EXCLUSIVE = null;
/**
当前节点状态,包括如下状态:
SIGNAL:值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
CANCELLED:值为1,表示当前的线程被取消
CONDITION:值为-2,表示当前节点在等待condition,也就是在condition队列中
PROPAGATE:值为-3,表示当前场景下后续的acquireShared能够得以执行
值为0,表示当前节点在sync队列中,等待着获取锁。
**/
int waitStatus;
//前一个节点
Node prev;
//后一节点
Node next;
//当前阶段绑定的线程
Thread thread;
// 存储condition队列中的后序节点
Node nextWaiter;
}
使用点
- ReentrantLock、ReentrantReadWriteLock中的Sync
- Semaphore中的Sync
- CountDownLatch中的Sync,释放时,有序释放所有正在等待的线程
- ThreadPoolExecutor中的Worker,Worker是工作线程,为了避免可重入,单独实现锁控制
核心
AbstractQueuedSynchronizer只维护队列,具体的锁定逻辑有各自的子类实现来定,.
比如下面的tryAcquire等方法,注意其中的参数arg,这个参数一般用来指定一定申请几个,一般是1,这个变量对应于AQS中state变量的变化,一般是state=state+arg,
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock传递arg=1,那么state也会大于等于1,这样也就保证state==0时,肯定可以获得锁,大于0时不一定,重入时state会继续加一,也就会存在state>1的情况。所以ReentrantLock多次使用时,需要多次释放
Semaphore中的public boolean tryAcquire(int permits) {},此时方法参数就变成了需要申请的许可量,那么底层的state就对应于总的许可量,每次申请时,state会减去相应的许可量
ReentrantReadWriteLock读写锁,state中,高16位为读锁,低16位为写锁,高低位记录各自的锁个数,比如当前高位不等于0时,就说明当前有多少个数的读锁正在进行。
通过上面的分析,底层的state变量很关键,基本上锁个数的控制都是由它来控制的
总结
- AQS的共享模式和排他模式,共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。参考:https://www.cnblogs.com/panxuejun/p/8874321.html
- CountDownLatch是通过默认count设置给state,这样state>1时代表被占有了锁,等调用countDown(),state==0时,会唤醒第一个Node等待节点,发现是共享模式,会继续传播唤醒后续共享模式的Node节点,直到没有共享模式节点或遇到排他模式节点时停止唤醒
- Lock的lockInterruptibly支持锁中断lock不支持锁线程终端,lockInterruptibly是发现线程中断的标识,直接当前线程抛出异常,lock是发现线程中断标识后,忽略继续执行https://blog.csdn.net/fangzuo/article/details/71080469
其他总结
ReentrantReadWriteLock
可重复读写锁,
关键点:
1、state中,高16位为读锁,低16位为写锁,高低位记录各自的锁个数,比如当前高位不等于0时,就说明当前有多少个数的读锁正在进行。
2、底层也是采用AQS使用,读和写都维护在一个链表中,抢占锁时,公平锁会依次塞入链表,非公平会先判断是否可获得锁
3、当一个读锁或写锁释放时,会拿到链表中一条要求读或者写的数据,继续抢占读锁或写锁
4、读锁的获取和释放:除了state记录总重入锁个数,在每个获取到读锁的线程中,还分别通过threadLocal绑定了各自线程读锁重入的次数(计数器),记录目的:这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数)
线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
综上:
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。
Condition
怎么实现等待?
白话:如同下面的awaitNanos方法,基本上是通过while循环不断的计算剩余时间,来实现指定时间的等待。其中里面,如果时间剩余大于1秒则会通过LockSupport先暂停线程执行,当小于1秒时才用while来精确计算等待。
// 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
//nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;
//若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
long savedState = fullyRelease(node);
final long deadline = System.nanoTime() + nanosTimeout;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
if (nanosTimeout <= 0L) {
transferAfterCancelledWait(node);
break;
}
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return deadline - System.nanoTime();
}
//使当前线程加入 await() 等待队列中,并释放当锁,当其他线程调用signal()会重新请求锁。与Object.wait()类似。
void await() throws InterruptedException;
//调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
//调用该方法后,结束等待的唯一方法是其它线程调用该条件对象的signal()或signalALL()方法。等待过程中如果当前线程被中断,该方法仍然会继续等待,同时保留该线程的中断状态。
void awaitUninterruptibly();
// 调用该方法的前提是,当前线程已经成功获得与该条件对象绑定的重入锁,否则调用该方法时会抛出IllegalMonitorStateException。
//nanosTimeout指定该方法等待信号的的最大时间(单位为纳秒)。若指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;
//若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。
long awaitNanos(long nanosTimeout) throws InterruptedException;
//与await()基本一致,唯一不同点在于,指定时间之内没有收到signal()或signalALL()信号或者线程中断时该方法会返回false;其它情况返回true。
boolean await(long time, TimeUnit unit) throws InterruptedException;
//适用条件与行为与awaitNanos(long nanosTimeout)完全一样,唯一不同点在于它不是等待指定时间,而是等待由参数指定的某一时刻。
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个在 await()等待队列中的线程。与Object.notify()相似
void signal();
//唤醒 await()等待队列中所有的线程。与object.notifyAll()相似
void signalAll();
}
ConditionObject
通过ConditionObject我们可以实现和synchronized下的wait同样的效果
package com.quzf.thread.orderlyThread;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description 多线程有序依赖执行
* @Author quzf
* @Create 2018-12-20 11:07 AM
*/
@Slf4j
public class OrderlyThreadWithLockTest {
public static ReentrantLock lock = new ReentrantLock();
//通过condition,我们可以实现和synchronized下的wait同样的效果
public static Condition condition = lock.newCondition();
@Data
@NoArgsConstructor
static class ThreadTest extends Thread {
private ThreadTest dependThread;
private String out;
private volatile boolean isComplete = false;
public ThreadTest(String out) {
this.out = out;
}
@Override
public void run() {
lock.lock();
while (dependThread != null && !dependThread.isComplete()) {
// lock.unlock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info(out);
this.isComplete = true;
condition.signalAll();
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadTest thread1 = new ThreadTest("Thread1 run");
ThreadTest thread2 = new ThreadTest("Thread2 run");
ThreadTest thread3 = new ThreadTest("Thread3 run");
thread3.setDependThread(thread2);
thread2.setDependThread(thread1);
// thread1.setDependThread(thread2);
thread3.start();
Thread.sleep(1000);
thread2.start();
Thread.sleep(1000);
thread1.start();
// System.out.println(thread3.dependThread);
}
}
总结:
其实看到这么多,也算基本了解底层锁的实现原理,所以此种锁的方式和java内置的syncized、wait是不一样的
上面的锁都是基于CAS和
LockSupport来保障原子性、实现线程等待的
FutureTask
白话:可等待式的线程,默认的runable是没有返回值的,如果想要阻塞式的获取异步线程的返回值,就要用到FutureTask类
CyclicBarrier
白话:它允许一组线程互相等待,直到到达某个公共屏障点,然后释放这些线程,重置屏障点继续等待,直到所有要执行的线程都执行完毕
CyclicBarrier是CountDownLatch的升级版,功能更复杂强大
Cyclic意为循环,就是说可以反复使用。比如指定计数器为10,当凑齐第一批10个线程后,计数器自动归0;接着下一批…
应用场景:10个人一组一组的完成某个任务。
Exchanger
白话:Exchanger类源于java.util.concurrent包,它可以在两个线程之间传输数据,Exchanger中的public V exchange(V x)方法被调用后等待另一个线程到达交换点(如果当前线程没有被中断),然后将已知的对象传给它,返回接收的对象。
如果另外一个线程已经在交换点等待,那么恢复线程计划并接收通过当前线程传给的对象
ArrayBlockingQueue
白话:阻塞队列,底层主要使用ReentrantLock、Condition notEmpty、Condition notFull来实现阻塞队列,基本上所有的方法先来者占用了锁,后来的基本上都需要等待。
CountDownLatch
countDownLatch.await()方法执行时,执行acquireSharedInterruptibly方法,先tryAcquireShared,此时state!=0,所以返回-1(获取共享锁失败),然后执行doAcquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
doAcquireSharedInterruptibly方法,所有线程都在此处等待
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//新建一个共享模式的节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获取上一个节点
final Node p = node.predecessor();
//判断上节点是否是head节点
if (p == head) {
int r = tryAcquireShared(arg);
//第一次进入时,此时state!=0,r<0继续后面执行
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//第一次进入时,获取共享锁失败,把线程进行park锁定,当前线程在for循环处停止
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
countDownLatch.countDown()方法执行时,执行releaseShared,先tryReleaseShared释放共享锁,当state=0时,tryReleaseShared获取共享锁成功,返回true,才执行doReleaseShared方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
doReleaseShared方法执行
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//开始释放head下一个等待节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
关键点:当前doReleaseShared执行后,unpark释放了head下一个等待节点。会触发上方doAcquireSharedInterruptibly方法中的for循环其中一个线程的执行,获取到共享锁时,触发setHeadAndPropagate方法执行,setHeadAndPropagate方法会先修改head后续节点,然后继续调用doReleaseShared方法。就这样通过不断的doReleaseShared方法,通过转播机制把所有共享模式的节点线程都进行释放,直到没有共享模式的节点为止。
doReleaseShared释放线程-》触发doAcquireSharedInterruptibly线程执行-》触发doReleaseShared-》触发doAcquireSharedInterruptibly线程执行-》直到没有共享模式的节点停止