Semaphore、CountDownLatch、CyclicBarrier
Semaphore
- Semaphore是信号箱。作用是控制访问特定资源的线程书名。底层依赖AQS的状态state。
- 一般用于多个线程对多个资源的抢夺;
- 如果资源数码为1,则退化为ReentrantLock;
代码示例
private static void test2() {
Semaphore semaphore=new Semaphore(2);
for (int i=0;i<5;i++){
new Thread(()->{
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"获得车位");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"离开车位");
semaphore.release();
},String.valueOf(i)).start();
}
}
会有两个资源,然后开启5个线程去争抢这两个资源。
构造方法
//参数permit为资源数量,默认使用非公平方式争抢
public Semaphore(int permits)
//fair表示创建的公平争抢还是非公平争抢;
public Semaphore(int permits, boolean fair)
源码:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
//传入为true,则创建公平锁,传入false,创建非公平锁;
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
重要方法
//请求资源
public void acquire() throws InterruptedException
//释放资源
public void release()
//在时间内请求资源,获取不到就退出
tryAcquire(long timeout, TimeUnit unit)
Semaphore.acquire()
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//Semaphore.acquire()会调用下面的
//AbstractQueuedSynchronizer.acquireSharedInterruptibly方法;
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//判断线程是否发生中断,发生中断就抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
Semaphore类里定义了两个内部类NonfairSync,FairSync代表公平和非公平模式。
tryAcquireShared(arg)
实际调用的是Semaphore.NonfairSync.tryAcquireShared方法;再调用内部类Sync的nonfairTryAcquireShared方法;
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取可使用的资源数据
int available = getState();
//减去当前线程请求的资源数据
int remaining = available - acquires;
//因为多个线程可能同时获取线程,所以修改state资源数码使用CAS方式。
//最后返回剩余资源数量;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
返回后,做判断,如果返回值也就是剩余资源数量小于0,则执行下一个方法doAcquireSharedInterruptibly;
doAcquireSharedInterruptibly()
该方法在AbstractQueuedSynchronizer定义了;用来阻塞将获取不到资源的线程给阻塞住。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//将获取不到资源的线程封装成Node结点,放进CLH同步队列;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获取当前线程结点的上一个结点。
//不同线程结点在队列中的位置不同。
final Node p = node.predecessor();
//如果当前线程结点的上一个结点是头结点(傀儡结点),则条件成立
if (p == head) {
//条件成立,尝试去获取资源。
//返回值r是剩余资源数量
int r = tryAcquireShared(arg);
//如果剩余资源数量大于0,则唤醒CLH中当前结点后面的结点。
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//如果当前结点不是头结点,或者是头结点,但是剩余资源数量r小于0
//则首先将当前线程结点的上一个结点的等待状态waitStatus变为-1,即Signal,返回false后;
//再进行一次自旋,调用parkAndCheckInterrupt阻塞将当前线程阻塞;
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//如果线程发生了中断,则抛出异常;
throw new InterruptedException();
}
} finally {
if (failed)
//如果发生中断,会将线程结点的等待状态waitStatus改为cancel,然后调用下面方法移除这些结点。
cancelAcquire(node);
}
}
addWaiter(Thread)
这个方法就是将线程封装成结点,然后放进CLH同步队列;
private Node addWaiter(Node mode) {
//1、 将当前结点封装为一个Node结点
//mode为共享Share模式;
Node node = new Node(Thread.currentThread(), mode);
//获得同步队列的尾结点tail;
//如果已经有队列中已经有Node结点了,则tail不为空;
//由于我们线程A获得锁、其他线程里的第一个线程运行到这里的时,同步队列是空的,所以tail为null; 先执行enq方法;
Node pred = tail;
//pred指向tail指针指向的结点。
if (pred != null) {
//如果tail尾指针不为空,则将当前结点的前向指针指向尾指针所指向的结点。
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//再通过CAS将尾指针指向当前结点。
//将结点的前一个结点的next指针指向当前结点。
pred.next = node;
return node;
}
}
//创建傀儡结点,这个方法可能会有多个线程同时进入。
enq(node);
//返回当前结点。
return node;
}
//首个进入队列的结点才会执行这个方法;
//可能会有多个线程进入这个方法,所以需要自旋和CAS
//即自旋锁来保证原子性;
private Node enq(final Node node) {
//自旋操作,可能多个线程同时进来
for (;;) {
//先获得队列尾结点tail;
Node t = tail;
//刚开始tail为空,判断成立
if (t == null) { // Must initialize
//多个线程可能同时进来,只有一个线程使用CAS创建一个傀儡结点;
//其他线程CAS设置失败,返回false,跳出if方法体;
//new Node(),是一个Thread为null的结点,也称为傀儡结点,使用CAS设置Head头结点;
if (compareAndSetHead(new Node()))
//然后再将头结点的引用设置为tail尾结点。
//即使tail结点指向傀儡结点。
tail = head;
} else {
//因为可能多个线程同时进入了这个方法,由于第一个线程创建了傀儡结点,并通过CAS将tail和head结点指向了傀儡结点。
//所以tail不为空,其他的线程会将Node结点接入傀儡结点的后面
node.prev = t;
//将结点的前向指针指向队列的尾指针所指向的结点。
if (compareAndSetTail(t, node)) {
//再通过CAS将尾指针指向当前结点。
//将结点的前一个结点的next指针指向当前结点。
t.next = node;
//返回当前结点。
return t;
}
}
}
}
//判断头结点是否为null,如果为null,就把Update对象引用设置为头结点。
//这里的Update对象引用是傀儡结点的引用
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
这样addWaiter执行结束,所有线程都封装成Node结点,入队了;
node.predecessor()
获取当前结点的上一个结点;
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
shouldParkAfterFailedAcquire(p,node)
将上一个结点的等待状态waitStatus改为Signal,即可唤醒状态;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前结点的上一个结点的waitStatus;
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果是-1,即Signal状态,则是可被唤醒装填。可以获得锁;
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//这里通过CAS将上一个结点的waitStatus改为Signal装填;
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false;
return false;
}
parkAndCheckInterrupt()
使用LockSupport的park方法将线程阻塞住。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
到这里,线程才算是真正的阻塞,在等待资源了。
Semaphore.release()
释放资源,让其他线程去占有;
public void release() {
sync.releaseShared(1);
}
调用AQS的releaseShared方法释放1个资源;
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(arg)
protected final boolean tryReleaseShared(int releases) {
//可能多个线程同时释放资源,所以需要自旋CAS操作;
for (;;) {
//获取剩余资源数量;
int current = getState();
//释放当前线程持有的资源数量;
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//多个线程可能会同时修改state资源数量;避免覆盖,使用CAS来保证数据正确性;
if (compareAndSetState(current, next))
//返回true;
return true;
}
}
执行该方法后,返回true,条件判断成立,调用doReleaseShared方法;
doReleaseShared()
private void doReleaseShared() {
for (;;) {
//获取头指针指向的结点。(傀儡结点)
Node h = head;
//如果队列不为空,则成立
if (h != null && h != tail) {
//获取头指针指向的结点的等待状态
//是否为Signal,可唤醒状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果是Signal,则通过CAS修改该节点的等待状态变为0;修改成功,返回true,条件判断不成立
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//将该节点的下一个结点使用LockSupport的unpark唤醒;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//唤醒后,判断h结点是否还是头结点,如果是跳出循环。
if (h == head) // loop if head changed
break;
}
}
unparkSuccessor(h)
唤醒傀儡结点的下一个结点。
//node:传入的是队列头指针所指向的结点对象、即傀儡对象;
private void unparkSuccessor(Node node) {
//获取傀儡对象的等待状态waitStatuS
int ws = node.waitStatus;
//如果waitStatus小于0,则说明是Signal状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//将其waitStatus通过CAS改为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);
//如果傀儡结点的下一个结点存在,不为Null,则将这个结点里面封装的线程唤醒
}
唤醒线程后,就回到线程线程阻塞的地方
parkAndCheckInterrupt;
setHeadAndPropagate(node, r)
设置头指针指向的结点,以及传播唤醒其他线程结点。
private void setHeadAndPropagate(Node node, int propagate) {
//传入的被唤醒的结点。
//获取头指针指向的线程结点。
Node h = head; // Record old head for check below
//使头指针指向传入的node结点。
setHead(node);
//propagate为资源数量,肯定大于0;才会调用当前这个方法;
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//获取被唤醒的线程结点node结点的下一个结点。
Node s = node.next;
if (s == null || s.isShared())
//唤醒线程结点的方法,上面有讲。
doReleaseShared();
}
}
Semaphore的源码解析基本完成,基本可以理解其工作流程;底层还是依赖AQS框架。
CountDownLatch
- 让一些线程阻塞,知道另一些线程完成操作后,才被唤醒;
- CountDownLatch维护了一个计数器,有两个方法:countDown()和await();
A:调用countDown()方法,计数器-1;
B:当计数器不为零,线程调用await方法时,会被阻塞
C:当计数值为0时,因调用await方法被阻塞的线程会被唤醒;
实例代码
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
countDown();
}
private static void countDown() throws InterruptedException {
CountDownLatch latch=new CountDownLatch(5);
for (int i=0;i<5;i++){
new Thread(()->{
System.out.println("线程"+Thread.currentThread().getName()+"come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
},String.valueOf(i)).start();
}
latch.await();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"exit");
}).start();
}
}
结果:
线程1come in
线程3come in
线程4come in
线程2come in
线程0come in
Thread-0exit
底层也是依靠AQS完成。
CyclicBarrier
- 字面意思为可循环使用的屏障,他要做的事情是:让一组线程到到一个屏障时被阻塞,知道最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续干活。
- 通过await方法使线程进入屏障;
- CountDownLatch是减,这个是增加。
实例代码
public class BarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier=new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final int index=i;
new Thread(()->{
System.out.println("收集了第"+index+"龙珠");
try {
TimeUnit.SECONDS.sleep(3);
barrier.await() ;
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
结果:
收集了第0龙珠
收集了第4龙珠
收集了第3龙珠
收集了第2龙珠
收集了第1龙珠
收集了第6龙珠
收集了第5龙珠
召唤神龙