并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理
在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并分析了CountDownLatch工具类的使用方式和源码实现,接下来我们继续学习CyclicBarrier,Semaphore,Exchanger的源码实现。
1. CyclicBarrier(栅栏)
1.1 CyclicBarrier的使用方式
在实际的并发中,我们可能需要等待所有线程到达一个数量点之后,才会去继续执行线程,这个点就是栅栏,比如开会的场景,我们应该要做的是应该等到所有与会人员都到齐之后,我们才开始开会,使用CyclicBarrier就可以很容易的实现这么一个场景,示例源码:
package com.company.thread.t4; import java.util.Random; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public void meeting(CyclicBarrier cyclicBarrier) { Random random = new Random(); try { Thread.sleep(random.nextInt(4000)); } catch (InterruptedException e) { e.printStackTrace(); } // if (Thread.currentThread().getName().equals("Thread-1")) { // throw new RuntimeException("异常了"); // } System.out.println(Thread.currentThread().getName() + "进入会议室"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "开始汇报...."); } public static void main(String[] args) { final CyclicBarrierDemo cyclicBarrierDemo = new CyclicBarrierDemo(); final CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始开会..."); } }); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { cyclicBarrierDemo.meeting(cyclicBarrier); } }).start(); } } }
meeting()方法传入CyclicBarrier对象,调用await()方法等待,直到达到初始化CyclicBarrier对象是传入的值时才会唤醒所有线程并发执行剩下的任务。
main方法中构建等待的线程数为10个,并传入一个任务,该任务由最后进入的线程来执行,将cyclicBarrier传入到meetting中,此时执行结果为:
Thread-9进入会议室 Thread-6进入会议室 Thread-5进入会议室 Thread-4进入会议室 Thread-3进入会议室 Thread-1进入会议室 Thread-8进入会议室 Thread-7进入会议室 Thread-2进入会议室 Thread-0进入会议室 Thread-0开始开会... Thread-0开始汇报.... Thread-9开始汇报.... Thread-6开始汇报.... Thread-4开始汇报.... Thread-5开始汇报.... Thread-3开始汇报.... Thread-1开始汇报.... Thread-7开始汇报.... Thread-2开始汇报.... Thread-8开始汇报....
执行结果显示,只有当是个线程都进入等待时,才会执行我们在构造CyclicBarrier对象时传入的任务,此任务就相当于是一个栅栏点,达到线程等待数量后,唤醒所有线程去继续执行,CyclicBarrier的使用方式就如上代码所以,需要注意的是,如果执行任务期间有一个线程出现异常,导致无法达到栅栏的点,那么所有线程都加会一直阻塞。
1.2 CyclicBarrier源码实现
首先看CyclicBarrier的构造方法:
public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; }
构造函数就是执行了一个赋值的操作,将我们需要等待的线程个数进行初始化,然后看核心的方法await(),
await():
public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen; } }
直接放回了dowait()方法,继续进入dowait()方法,
dowait():
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; lock.lock(); try { final Generation g = generation; if (g.broken) throw new BrokenBarrierException(); if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } int index = --count; if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; if (command != null) command.run(); ranAction = true; nextGeneration(); return 0; } finally { if (!ranAction) breakBarrier(); } } // loop until tripped, broken, interrupted, or timed out for (;;) { try { if (!timed) trip.await(); else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { if (g == generation && ! g.broken) { breakBarrier(); throw ie; } else { // We're about to finish waiting even if we had not // been interrupted, so this interrupt is deemed to // "belong" to subsequent execution. Thread.currentThread().interrupt(); } } if (g.broken) throw new BrokenBarrierException(); if (g != generation) return index; if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } }
它是CyclicBarrier的核心方法,我们来一步步分析:
第一步:首先获取可重入锁,判断线程是否被中断,如果被中断,调用beakBarrier()方法,generation.broken = true,还原count为初始值,并唤醒所有等待的线程。并抛出中断异常
beakBarrier():
private void breakBarrier() { generation.broken = true; count = parties; trip.signalAll(); }
否则将count减减,count减到最后一个线程进入时。使用ranAction判断是否执行,判断初始化时是否传入任务,传入则执行该任务的run方法,这里就是我们前面说得栅栏任务由最后一个进入的线程执行的实现,将ranAction置为true,nextGeneration()调用唤醒所有线程,并将所有参数重置为初始化状态。表示这一组任务已经等待完成并开始执行,
nextGeneration()方法:
private void nextGeneration() { // signal completion of last generation trip.signalAll(); // set up next generation count = parties; generation = new Generation(); }
当index不等于0,表示不是最后一个线程进入时,自旋使线程等待,当g.broken = true时,表示线程出现过错误,执行breakBarrier();方法,否则就中断线程,接着就是校验线程是否正确执行和是否超时等的一些校验,dowait()方法源码说完了。
主要的就是初始化一个状态,每次调用自减,跟直到状态为0时,唤醒所有的线程,这点于CountDownLatch()类似,但是CyclicBarrier是可重用的,当到达栅栏的所有线程被唤醒之后,它会将CyclicBarrier实例还原成初始状态,下一次可以继续使用。
2. Semaphore(信号量)
信号量指当前空闲有三个资源,但是有一个栏杆一样的东西在拦截线程,当前面三个线程进来获取资源时,因为有资源所以允许三个线程进入,当资源被前面三个线程占用时,其他的线程就只能等待着正在使用资源的线程释放资源,栏杆才会随机的放一个线程进入,这样就达到了控制并发线程数量的一个目的;
2.1 使用方式
直接看代码:
package com.company.thread.t4; import java.util.concurrent.Semaphore; public class SemaphoreDemo { public void method (Semaphore semaphore) { try { semaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "is running "); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } semaphore.release(); } public static void main(String[] args) { final SemaphoreDemo semaphoreDemo = new SemaphoreDemo(); final Semaphore semaphore = new Semaphore(10); for (int i = 0; i < 20; i++){ new Thread(new Runnable() { @Override public void run() { semaphoreDemo.method(semaphore); } }).start(); } } }
method()方法中调用 semaphore.acquire();获取资源,当能获取到资源后继续执行,执行完成后调用 semaphore.release()方法释放资源,main方法中可以创建很多个线程,但是我们通过观察输出可以看到,每次都只能有是个线程在执行,这就是在初始化的时候控制了线程并发执行的数量只能是是个,所有多余的将会等待前面执行的线程释放资源,然后拿到资源,使用都是非常简单的,看示例源码来理解完全没有问题,接下来我们看看信号量的实现原理
2.2 源码实现
首先还是看构造方法:
public Semaphore(int permits) { sync = new NonfairSync(permits); }
可以看到它是实例化非公平内部同步器,并将信号量个数初始化,这里就不再深入,主要的方法是acquire()/release()方法,
acquire()方法:
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
这里调用的AQS的共享模式的acquireSharedInterruptibly(1)对线程获取资源,由于AQS的模式主要是看自己实现的tryAcquireShared()方法,所以我们主要看tryAcquireShared()源码:
protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); }
返回nonfairTryAcquireShared()的执行结果,
nonfairTryAcquireShared()方法是操作AQS状态。
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
返回资源数量,我们知道在AQS共享模式下,如果返回大于0时,它就可以去等待队列中唤醒线程,这里是自旋直到返回状态减acquires后的能拿到资源的状态,即是自旋一直等待其他线程释放资源,此时当前线程就可以拿到资源执行。
release()方法:
public void release() { sync.releaseShared(1); }
也是使用AQS的releaseShared()方法,所以我们这里依然只看自己实现的tryReleaseShared()方法:
protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } }
它的实现就是,当释放资源时,不断修改AQS的stats增加releases资源,这样当释放成功后,等待中的线程就有机会获取线程去执行了。
理解了AQS的情况下,CyclicBarrier和信号量的实现其实也就不难理解了,都是继续AQS提供的模式重写tryXX方法来实现自己想要的功能。
3. Exchanger(数据交换)
Exchanger是可以线程之间通信的,它可以使数据在两个线程直接交换,也就是A线程可以获取到B线程中的某些数据,B线程也可能可以获取到B线程的数据。
3.1 使用方式
直接上代码:
package com.company.thread.t5; import java.util.concurrent.Exchanger; public class Demo { public void a (Exchanger<String> exchanger) { System.out.println("a 方法执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } String res = "12345"; try { String value = exchanger.exchange(res); } catch (InterruptedException e) { e.printStackTrace(); } } public void b(Exchanger<String> exchanger) { System.out.println("b 方法开始执行"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } String res = "123456"; try { String value = exchanger.exchange(res); System.out.println("开始进行比对"); System.out.println("比对结果为:" + value.equals(res)); } catch (InterruptedException e) { e.printStackTrace(); } } }
Exchanger是一个泛型类,他管理的是传入的类型的变量,示例中使用一个Exchanger实例,在a方法中调用exchange()将需要交换的数据出入,b也一样,创建两个线程,分别调用a,b方法,传入同一个Exchanger实例,这样当两个线程执行到一个零界点时,就会将两个线程中的传入的数据交换的两外的线程中,这样就可以在另外的方法或线程中拿到其他线程的某些我们需要的数据。jdk文档中介绍,它最常用的就是缓冲这一块,有兴趣可以去了解一下。Exchanger的使用也是非常简单的。
3.2 Exchanger 源码
exchange()核心方法:
public V exchange(V x) throws InterruptedException { if (!Thread.interrupted()) { Object v = doExchange((x == null) ? NULL_ITEM : x, false, 0); if (v == NULL_ITEM) return null; if (v != CANCEL) return (V)v; Thread.interrupted(); // Clear interrupt status on IE throw } throw new InterruptedException(); }
简单明了的看到,它调用了doExchange()方法去执行了具体的改变操作,所以进入doExchange():
private Object doExchange(Object item, boolean timed, long nanos) { Node me = new Node(item); // Create in case occupying int index = hashIndex(); // Index of current slot int fails = 0; // Number of CAS failures for (;;) { Object y; // Contents of current slot Slot slot = arena[index]; if (slot == null) // Lazily initialize slots createSlot(index); // Continue loop to reread else if ((y = slot.get()) != null && // Try to fulfill slot.compareAndSet(y, null)) { Node you = (Node)y; // Transfer item if (you.compareAndSet(null, item)) { LockSupport.unpark(you.waiter); return you.item; } // Else cancelled; continue } else if (y == null && // Try to occupy slot.compareAndSet(null, me)) { if (index == 0) // Blocking wait for slot 0 return timed ? awaitNanos(me, slot, nanos) : await(me, slot); Object v = spinWait(me, slot); // Spin wait for non-0 if (v != CANCEL) return v; me = new Node(item); // Throw away cancelled node int m = max.get(); if (m > (index >>>= 1)) // Decrease index max.compareAndSet(m, m - 1); // Maybe shrink table } else if (++fails > 1) { // Allow 2 fails on 1st slot int m = max.get(); if (fails > 3 && m < FULL && max.compareAndSet(m, m + 1)) index = m + 1; // Grow on 3rd failed slot else if (--index < 0) index = m; // Circularly traverse } } }
它的核心算法是for(;;)中的内容,当slot为空时,创建一个Slot,不为空,则获取slot的value不为空,则表示已经有线程进来了,使用cas将当前线程的value与前一个进入的value进行交换,成功后唤醒前面进入之后等待的线程。当前y为空,则表示当前线程是第一个进来的线程,设置创建一个节点,将当前需要保存的value设置到slot中,以node的形式保存,当被唤醒时,则执行await()方法返回修改后的值,因为在上面代码中第二个进来的线程已经交换了两个需要交换的数据,所以当等待线程被叫醒后返回的被后一个线程交换后的数据。
Java的并发工具类,到这里就算是学习了,理解不是很深刻,望各位各位多多指点,共同进步,谢谢!