在使用多线程的时候,我们可以使用一些工具来达到对资源的访问量控制
、线程之间的相互等待
、线程之间的通信唤醒
。
控制线程的合作并发,主要有四大工具;CountDownLatch,Semaphore,Condition以及CyclicBarrier。下面对这四个工具进行逐一介绍。
CountDownLatch
CountDownLatch主要是用于线程之间的等待协作
。可以实现多等一,也可以实现一等多。
例如,我们需要启动多个线程进行资源加载,等资源加载完成后主线程才能继续执行;这就可以用CountDownLatch的一等多
的机制来完成;或者我们想自实现并发情况可以通过多等一
的方式来使线程阻塞,等主线程经过一系列操作(休眠,或者准备其他资源)后,进行放行
。
使用
CountDownLatch在构造的时候需要传入一个参数,这个参数就是需要等待线程的个数,并且提供了一个阻塞的方法await()
来让线程进行阻塞,只有当等待数减为0的时候,被阻塞的线程才能往下执行
。
而等待数可以通过CountDownLatch的countdown()
来进行减一的操作。
代码演示
一等多代码示例:
可用于等待资源
public class CountDownLatchTest {
// 构造一个倒数器
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
// 启动两个线程来加载资源
new Thread(new TaskOne()).start();
new Thread(new TaskTwo()).start();
System.out.println("资源加载线程启动完成,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
countDownLatch.await();
System.out.println("主线程执行完成,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
}
}
// 任务二
class TaskOne implements Runnable{
@Override
public void run() {
// 模拟处理资源要5秒
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("TaskOne Finish");
// 完成工作,线程数减一
CountDownLatchTest.countDownLatch.countDown();
}
}
// 任务一
class TaskTwo implements Runnable{
@Override
public void run() {
// 模拟处理资源要10秒
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 完成工作,线程数减一
CountDownLatchTest.countDownLatch.countDown();
System.out.println("TaskTwo Finish");
}
}
结果演示:
可以看到,主线程确实等待了子线程完成了对应的工作才能继续走下去。否则,会一直阻塞在当前状态下等待。
多等一代码示例:
可用于模拟并发的场景
public class CountDownLatchMuch {
// 构造一个倒数器
static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
// 用newCachedThreadPool 开多个任务线程(这里可以先了解一下JDK提供的线程池)
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100 ; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程已到达 , Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
// 先让所有线程在这里阻塞
countDownLatch.await();
System.out.println("线程出发 , Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
//TODO 这里可以调用任意接口HttpClient来模拟高并发
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 休眠5秒,让所有线程都准备好
Thread.sleep(5000);
// 放行所有的线程
countDownLatch.countDown();
}
}
执行结果:
我们可以看到,子线程在我们休眠五秒后,主线程调用countdown()方法
,然后同一时间被放行的。
小结
CountDownLatch通过我们实现定义好的计数器,当我们达到某种条件后可让计数器-·;最终当计数器为0时,激活调用await()方法的线程进行下一步操作。
Semaphore
Semaphore可以简单的理解为许可证
。
只有拿到对应数量许可证的人才能通行,其他拿不到对应数量的人统统会被阻塞住;并且,还要等别人释放了许可证
,自己才有机会拿到。
这就好比,有一些大的资源要限制更少的人拿去,有一些较小的资源允许多人拿去,并且这两个资源共用一种许可证。
使用
Semaphore提供了许多方法可以使用:
常用的
- acquire():拿取一个许可证
- acquire(int permits):拿取permits个许可证
- release():释放一个许可证
- release(int permits):释放permits个许可证
- tryAcquire():尝试拿取一个许可证
- tryAcquire(int permits):尝试拿取permits个许可证
- tryAcquire(long timeout, TimeUnit unit):尝试在timeout unit 内拿取一个许可证;unit表示时间单位
- tryAcquire(int permits, long timeout, TimeUnit unit):尝试在timeout unit 内拿取permits个许可证
- 带有
try
的获取许可证的方法都是尝试去拿,会立马
返回布尔值类型的结果;而不带try
的则会阻塞
的等待别人释放,在去拿。
代码演示:
不释放许可证
public class SemaphoreOneTest {
// 允许开放两个人去操作
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
new Thread(new SemaTaskOne()).start();
new Thread(new SemaTaskTwo()).start();
Thread.sleep(500);
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到了许可证");
System.out.println("Main Finish");
}
}
class SemaTaskOne implements Runnable{
@Override
public void run() {
try {
SemaphoreOneTest.semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到了许可证");
} catch (Exception e) {
e.printStackTrace();
}
}
}
class SemaTaskTwo implements Runnable{
@Override
public void run() {
try {
SemaphoreOneTest.semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到了许可证");
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们看到如果不释放许可证的话,即使子线程执行完,主线程仍然拿不到许可证;如果主线程有机会先拿到许可证,即使主线程执行完了,程序仍然处于运行状态。因为拿到许可证的线程都没有释放,仍有子线程处于阻塞状态。
增加释放:
class SemaTaskOne implements Runnable{
@Override
public void run() {
try {
SemaphoreOneTest.semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"拿到了许可证");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}finally{
SemaphoreOneTest.semaphore.release();
}
}
}
信号的释放必须放在finally
块中,否则程序发生异常不会释放许可证。
正确释放许可证结果:
错误释放许可证结果:
带有参数的许可证演示:
public class SemaphoreOneTest {
// 允许开放两个人去操作
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) throws InterruptedException {
new Thread(new SemaTaskOne()).start();
new Thread(new SemaTaskTwo()).start();
System.out.println(Thread.currentThread().getName()+"等待中,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
semaphore.acquire(2);
System.out.println(Thread.currentThread().getName()+"拿到了许可证"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
Thread.sleep(4000);
semaphore.release(2);
System.out.println("Main Finish");
}
}
class SemaTaskOne implements Runnable{
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"等待中,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
SemaphoreOneTest.semaphore.acquire(2);
System.out.println(Thread.currentThread().getName()+"拿到了许可证"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
Thread.sleep(4000);
} catch (Exception e) {
e.printStackTrace();
}finally {
SemaphoreOneTest.semaphore.release(2);
}
}
}
class SemaTaskTwo implements Runnable{
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"等待中,Time:"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
SemaphoreOneTest.semaphore.acquire(2);
System.out.println(Thread.currentThread().getName()+"拿到了许可证"+new SimpleDateFormat("hh:mm:ss").format(new Date()));
Thread.sleep(4000);
} catch (Exception e) {
e.printStackTrace();
}finally {
SemaphoreOneTest.semaphore.release(2);
}
}
}
演示结果:
小结
非阻塞的尝试拿取就不做演示了,大体上差不多。只不过不会阻塞,也可以设置超时时间的拿,如果在这段时间拿不到的话线程就继续往下执行。
Semaphore在使用的过程中,一定要保证许可证能正确的释放
,否则会出现死锁的问题导致程序无法运行下去。
Condition
Condition本质上是一个接口,所以只能实例化其子类或者根据需求重写对应的方法。
使用
其实跟Object的wait和notify方法类似,Condition提供了await()
和signal()
。在await()中提供了可设置超时时间的重载方法。
代码
下面用Condition来演示一个生产者和消费者模式
public class ConditionTest {
static ReentrantLock reentrantLock = new ReentrantLock();
// 生产者锁
static Condition conditionProvier = reentrantLock.newCondition();
// 消费者锁
static Condition conditionConsumer = reentrantLock.newCondition();
static Queue arrayList = new ArrayBlockingQueue(10);
public static void main(String[] args) {
new Thread(new ConditionTask()).start();
new Thread(new ConditionTaskTwo()).start();
}
}
// 消费者
class ConditionTask implements Runnable{
@Override
public void run() {
while(true) {
try {
ConditionTest.reentrantLock.lock();
if (ConditionTest.arrayList.size() <= 0) {
System.out.println("无了,先不拿");
ConditionTest.conditionConsumer.await();
}
int poll = (int) ConditionTest.arrayList.poll();
System.out.println("弹出元素," + poll);
ConditionTest.conditionProvier.signalAll();
} catch (Exception e) {
} finally {
ConditionTest.reentrantLock.unlock();
}
}
}
}
// 生产者
class ConditionTaskTwo implements Runnable{
int i = 0;
@Override
public void run() {
while(true) {
try {
ConditionTest.reentrantLock.lock();
//Thread.sleep((long) (Math.random() * 10000));
if (ConditionTest.arrayList.size() >= 10) {
System.out.println("满了,暂停一下");
ConditionTest.conditionProvier.await();
}
System.out.println("插入元素");
ConditionTest.arrayList.offer(i++);
ConditionTest.conditionConsumer.signalAll();
} catch (Exception e) {
} finally {
ConditionTest.reentrantLock.unlock();
}
}
}
}
结果演示:
CyclicBarrier
CyclicBarrier比较容易理解,凑够了一拨人就出发。
使用:
- 只需要要在构造参数中传入要凑够多少人(线程),具体线程调用
await()
方法,如果凑够了指定数的线程就一起出发。 - 也可以传入凑够的人数和执行线程;这个执行线程是凑够人数后,await()线程会
继续往下执行
,当前线程开启执行线程
,相当于多一个线程用于提醒或执行其他操作。 - 后面凑不够指定的也会全部出发了
代码演示
无执行线程
public class CyclicBarrierOneTask {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
// 一次凑够5个人出发
CyclicBarrier cyclicBarrierWithOutRunnable = new CyclicBarrier(5);
// 开10个线程
for (int i = 0; i < 10; i++) {
new Thread(new CyclicBarrierSmailTask(cyclicBarrierWithOutRunnable)).start();
}
}
}
class CyclicBarrierSmailTask implements Runnable{
private CyclicBarrier cyclicBarrier;
public CyclicBarrierSmailTask(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
// 让线程随机休眠,显示出先来后到的凑够一拨人
long time = (long) (Math.random() * 10000);
Thread.sleep(time);
System.out.println(Thread.currentThread().getName()+"等待中");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+"出发了");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
运行结果:
增加执行线程
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,new Runnable(){
@Override
public void run() {
System.out.println("凑够了一拨人,出发");
}
});
for (int i = 0; i < 10; i++) {
new Thread(new CyclicBarrierSmailTask(cyclicBarrier)).start();
}
}
结果演示:
当调用await()
的线程数达到指定数时,就会出发执行线程。并且之前调用await()
的线程会一起运行,不在阻塞。
小结
合理运用好线程控制工具可以很好的帮助我们使用线程的合作,如果需要搞懂内部原理的话需要对AQS
进一步掌握。
因为里面的控制数就是通过AQS中的state类变量
来计数。