关于多线程的一些同步器
这次梳理下关于多线程中的三个同步器。
1、信号量(Semaphore)
概念上讲, 一个信号量管理许多的许可证 ( permit ) 。 为了通过信号量。线程通过调用acquire 请求许可。其实没有实际的许可对象 , 信号量仅维护一个计数。许可的数目是固定的,由此限制了通过的线程数量。其他线程可以通过调用release释放许可。
看如下示例代码:
public class MySemaphore {
//构造函数中的参数表示,控制同时访问特定资源的线程数量
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
//创建线程池执行任务
ExecutorService executorService = Executors.newCachedThreadPool();
//创建10个人开始购票
for (int i = 0; i < 10; i++) {
executorService.execute(new TicketBuyer(i+1));
}
//关闭线程池
executorService.shutdown();
}
static class TicketBuyer implements Runnable{
//人员编号
private int i = 0;
public TicketBuyer(int i){
this.i = i;
}
@Override
public void run() {
try {
//单个线程调用semaphore.acquire()获取信号量
semaphore.acquire();
System.out.println("第"+i+"个人开始购票");
Thread.sleep(2000);
System.out.println("第"+i+"个人购票完成");
//释放信号量
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
执行结果:
第4个人开始购票
第8个人开始购票
第8个人购票完成
第4个人购票完成
第2个人开始购票
第6个人开始购票
第2个人购票完成
第10个人开始购票
第6个人购票完成
第1个人开始购票
第10个人购票完成
第5个人开始购票
第1个人购票完成
第9个人开始购票
第9个人购票完成
第5个人购票完成
第3个人开始购票
第7个人开始购票
第3个人购票完成
第7个人购票完成
有结果可以看出,限定信号量为2,即限定每次访问的线程数量为2,每次有两个人进行购票,直到十个人全都购票完毕。
2、倒计时门栓(CountDownLatch)
一个倒计时门栓 ( CountDownLatch ) 让一个线程集等待直到计数变为0
。 倒计时门栓是一次性的。一旦计数为0,就不能再重用了。线程数初始化门栓。 每个工作器线程在结束前将门栓计数减1。 另一个获取工作结果的线程在门外等待 , 一旦所有工作器线程终止该线程继续运行。
查看如下示例代码:
public class MyCountDownLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(()->{
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
}).start();
new Thread(()->{
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
}).start();
System.out.println("等待2个子线程执行完毕...");
countDownLatch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
}
}
执行结果如下:
等待2个子线程执行完毕...
子线程Thread-0正在执行
子线程Thread-1正在执行
子线程Thread-0执行完毕
子线程Thread-1执行完毕
2个子线程已经执行完毕
继续执行主线程
有上述结果可以看出,程序首先走到主线程的"等待2个子线程执行完毕"的countDownLatch.await();处等待子线程执行任务并执行完成,然后子线程执行任务触发countDownLatch.countDown();门栓减一,知道两个子线程执行完毕,门栓为0后继续执行主线程。
3、障栅(CyclicBarrier)
CyclicBarrier 类实现了一个集结点 ( rendezvous ) 称为障栅 (barrier) 。 考虑大量线程运行在一次计算的不同部分的情形。 当所有部分都准备好时,需要把结果组合在一起 。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。 一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
看如下示例代码:
public class MeCyclicBarrier {
public static void main(String[] args) {
int N =4;
CyclicBarrier cyclicBarrier = new CyclicBarrier(N);
for (int i = 0; i < N ; i++) {
new Thread(new WriteMe(cyclicBarrier,i+1)).start();
}
}
static class WriteMe implements Runnable{
private CyclicBarrier cyclicBarrier;
private int i = 0;
public WriteMe(CyclicBarrier cyclicBarrier,int i){
this.cyclicBarrier = cyclicBarrier;
this.i= i;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000+i*1000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
代码执行结果如下:
线程Thread-0正在写入数据...
线程Thread-3正在写入数据...
线程Thread-2正在写入数据...
线程Thread-1正在写入数据...
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
由上述代码执行结果我们可以得出,我们首先设置了障栏数为4,程序执行线程任务,任务执行完毕后出发障栏cyclicBarrier.await();,障栏数减一,线程都会等在障栏这里,等待所有线程执行完毕后,障栏数为0,继续执行线程的其他任务"所有线程写入完毕,继续处理其他任务..."。
障栏这个形容非常的贴切,就是设置障栏,线程执行到障栏处需等待,直到所有线程执行到障栏处,达到设置的障栏数,才会放行,继续执行其他部分。