想起同步器,可能大家想到的就是AQS,但他毕竟是个抽象类,不能直接使用,所以juc还提供了好多现成的同步器,几乎都是基于这个AQS来实现的,下面一起来看看吧。
CountDownLatch
想必大家最熟悉的同步器就是这个了吧,所以先来介绍一下,他的作用场景是在当需要一个或多个线程全部完成自己的任务之后(计数器减为0)才能进行后续操作。举个通俗易懂的例子,三个运动员参加田径比赛,发令员需要等大家准备好之后才会发令,然后开始计时
@Test
public void TestCountDownLatch() throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println(t.getName() + "准备的时候出问题了" + e.getStackTrace()[0].toString());
});
// 固定的参赛人员数量
CountDownLatch latch = new CountDownLatch(3);
System.out.println("比赛准备开始" + System.currentTimeMillis());
new Thread(() -> {
System.out.println("t1 正在穿鞋子");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 穿鞋子花了10秒");
latch.countDown();
}, "t1").start();
new Thread(() -> {
System.out.println("t2 正在刷牙");
try {
TimeUnit.SECONDS.sleep(8);
System.out.println("t2 刷牙花了8秒");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
new Thread(() -> {
System.out.println("t3 正在喝水");
try {
TimeUnit.SECONDS.sleep(4);
System.out.println("t3 喝水花了4秒");
// int a = 1/0;
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t3").start();
latch.await();
System.out.println("比赛开始" + System.currentTimeMillis());
}
用法很简单,就是先创建固定数量的门栓数量,每个线程完成自己的任务之后减去一个门栓,即调用countDown()方法,然后需要开门的那个线程(这里就是主线程)调用await()方法进行等待,当所有门栓都被减去了,门自然而然地打开了,就能干自己想干的事了,我觉得这个解释应该很好懂了吧。但是这里需要注意的是,无论如何一定要保证门栓能被减去,所以一般是放到finally块去执行,所以我上面这个例子其实不严谨,同时在t3这里我注视了一行错误代码,如果放开再执行的话会发现程序无法结束,原因其实已经很明了了,就是因为出现异常之后门栓没有减去,所以这个门永远都开不了,主线程就一直等着。当然他还有另外一个带超时时间的方法await(time:long)
然后再来看一看另外一个使用场景,当1个或多个线程需要同时开始执行时(理想情况下),典型场景就是多个运动员在听到发令枪响后同时起跑,来看一下例子
@Test
public void TestCountDownLatchMoreAwait() throws InterruptedException {
// 固定的参赛人员数量
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
try {
latch.await();
System.out.println("发令员发令");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
latch.await();
System.out.println("裁判开始记时");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
latch.await();
System.out.println("拉拉队开始呼喊");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("比赛准备开始" + System.currentTimeMillis());
new Thread(() -> {
System.out.println("t1 正在穿鞋子");
try {
TimeUnit.SECONDS.sleep(10);
System.out.println("t1 穿鞋子花了10秒");
latch.countDown();
latch.await();
System.out.println("t1 起跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
System.out.println("t2 正在刷牙");
try {
TimeUnit.SECONDS.sleep(8);
System.out.println("t2 刷牙花了8秒");
latch.countDown();
latch.await();
System.out.println("t2 起跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
new Thread(() -> {
System.out.println("t3 正在喝水");
try {
TimeUnit.SECONDS.sleep(4);
System.out.println("t3 喝水花了4秒");
latch.countDown();
latch.await();
System.out.println("t3 起跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4").start();
// TimeUnit.SECONDS.sleep(15);
}
我在上面一个例子上加了一点东西,当三位运动员准备好后,裁判员开始计时,发令员发令,拉拉队开始呼喊,运动员同时起跑,后面这几个动作要求同时执行,实现也很简单,只需要在自己的线程里面主动去调用lantch.await()方法即可,注意一定要是同一个lantch,但是如果看到这里的你直接搬这段代码去运行的话你会发现程序只会几乎同时输出三个运动员各自正在准备什么,然后就结束了,不妨思考一下为什么呢?其实开始我写这个例子的时候也遇到这个问题了,想了半天总觉得没错,但其实有一点被我们疏忽了,那就是jvm会在没有至少一个非守护进程正在运行的话,会自动退出,再回过头来看上面的例子,我们的方法在主线程之中运行,虽然在各个子线程里面进行了sleep,但是主线程并不care,他的左右就是启动了多个线程,然后结束,所以子线程睡不睡都没关系,所以最后那一行注释放开就是为了阻塞主线程,让整个程序跑完,才能看到效果。
同时需要注意的是这个门拴只能用一次,只要减到0之后就不能再用了(不能再加回去),所以有时候我们会声明多个门拴来完成一系列动作。
CyclicBarrier
这个比起上面的门拴来说,他可以重用这个门拴,先来看个例子,还是上面的运动员比赛
@Test
public void testCyclicBarrier() throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(3,()->{
System.out.println("发令员发令");
});
new Thread(() -> {
System.out.println("t1 正在准备!");
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("t1 准备好了");
barrier.await();
System.out.println("t1 起跑");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("t2 正在准备!");
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 准备好了");
barrier.await();
System.out.println("t2 起跑");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("t3 正在准备!");
try {
TimeUnit.SECONDS.sleep(6);
System.out.println("t3 准备好了");
barrier.await();
System.out.println("t3 起跑");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(10);
}
通过这个例子能看出来,在构造CyclicBarrier的时候同时可以指定需要多少个线程同时达到某个点,和此时想要做的事,而每个线程在忙完自己的事情之后,调用await方法就是声明自己已经到达临界点了,现在是在等别的线程,和上面countDownLatch的countDown方法有点类似,然后等所有线程都到临界点了,先执行声明的额外方法,然后各线程再同时继续向下执行,同样这个await方法也可以带上超时时间,然后再看看她可以重用的例子
@Test
public void testCyclicBarrier() throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
CyclicBarrier barrier = new CyclicBarrier(3,()->{
if (num.get()==0){
System.out.println("发令员发令");
}else {
System.out.println("开始颁奖");
}
});
new Thread(() -> {
System.out.println("t1 正在准备!");
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("t1 准备好了");
barrier.await();
System.out.println("t1 起跑");
TimeUnit.SECONDS.sleep(10);
System.out.println("t1 到达终点");
num.incrementAndGet();
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("t2 正在准备!");
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 准备好了");
barrier.await();
System.out.println("t2 起跑");
TimeUnit.SECONDS.sleep(13);
System.out.println("t2 到达终点");
barrier.await();
num.incrementAndGet();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("t3 正在准备!");
try {
TimeUnit.SECONDS.sleep(6);
System.out.println("t3 准备好了");
barrier.await();
System.out.println("t3 起跑");
TimeUnit.SECONDS.sleep(12);
System.out.println("t3 到达终点");
barrier.await();
num.incrementAndGet();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(30);
}
所以可以重用表示当这个临界点在所有线程到达之后会立刻重置,然后每个线程又可以在后续重新使用。其实和CountDownLatch比起来,我觉得两者在功能上并没有太大差别,看各位各取所需了。
Semaphore
Semaphore实际上算是一个限流器,他能控制最多n个线程同时访问某一资源,但是要注意的是,这里只是允许你去访问,但并没有保证对该资源的读写的原子性,可见性和禁止重排序,所以需要自己单独去实现线程安全,举个例子但我觉得很容易理解,车站只有5个售票窗口,所以只允许同一时刻有5个线程去上买票,这就是Semaphore可以实现的场景,但是不管你有几个窗口正在卖票,有一点你必须的保证那就是票不能超卖,这就是要自己保证线程安全,Semaphore不管这个
@Test
public void SemaphoreTest() throws InterruptedException {
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "可以卖票了");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t" + i).start();
}
TimeUnit.SECONDS.sleep(10);
}
这里一共开启了10个线程去抢着卖票,但是最终只有5个能抢到这个资格,当然抢到资格的线程可以通过semaphore.release()去让出自己的资格,然后其他因为acquire阻塞住的线程就有机会去获取资格,但是最终数量还是不会超过5
Phaser
这玩意儿是jdk7引入的,其实功能类似CountDownLatch,也是一个门栓,但是CountDownLatch和CyclicBarrier都是在构造的时候制定的限制的线程数量,然后就不能再修改了,有些场景下,我们可能需要修改,所以就可以用这东西来代替,先看看方法签名
register()//添加一个新的注册者
bulkRegister(int parties)//添加指定数量的多个注册者
arrive()// 到达栅栏点直接执行,无须等待其他的线程
arriveAndAwaitAdvance()//到达栅栏点,必须等待其他所有注册者到达
arriveAndDeregister()//到达栅栏点,注销自己无须等待其他的注册者到达
onAdvance(int phase, int registeredParties)//多个线程达到注册点之后,会调用该方法。
其中arriveAndAwaitAdvance()方法就和前面的CyclicBarrier.await()方法一样的作用,还是前面赛跑的例子,用Pahser来实现
@Test
public void phaserTest() throws InterruptedException {
Phaser phaser = new Phaser(3);
System.out.println("比赛准备开始" + System.currentTimeMillis());
new Thread(() -> {
System.out.println("t1 正在穿鞋子");
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("t1 穿鞋子花了5秒");
phaser.arriveAndAwaitAdvance();
System.out.println("t1 起跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
System.out.println("t2 正在穿鞋子");
try {
TimeUnit.SECONDS.sleep(10);
System.out.println("t2 穿鞋子花了10秒");
phaser.arriveAndAwaitAdvance();
System.out.println("t2 起跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
System.out.println("t3 正在穿鞋子");
try {
TimeUnit.SECONDS.sleep(7);
System.out.println("t3 穿鞋子花了7秒");
phaser.arriveAndAwaitAdvance();
System.out.println("t3 起跑");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
TimeUnit.SECONDS.sleep(20);
}
这个还能控制任务执行的轮数,通过重写他的onAdvance方法,这个方法也是指定数量线程每一次到达临界点就会执行,参考CyclicBarrie的barrierAction参数
Phaser phaser = new Phaser() {
// return true 表示需要终止
@Override
protected boolean onAdvance(int phase, int registeredParties) {
return phase == 3 || registeredParties == 0;
}
};
其中phase表示指定数量线程(每一批次可以动态修改数量)第几次到达这个临界点,可能不太好理解,举个例子就是我规定第一阶段要5个线程到达这个临界点进行等待,然后大家开始执行进入下一阶段,但是下一阶段我的线程数量可以动态修改,不一定跟上一阶段一样,所以这里这个pahse跟几个线程没关系,他表示的是大家第几次到临界点,所以我这里把他设置为3,表示我只需要一共到达三次临界点就不用了,registeredParties表示的就是这一次到达临界点的线程数量
@Test
public void Phaser2() throws InterruptedException {
Phaser phaser = new Phaser() {
@Override
protected boolean onAdvance(int phase, int registeredParties) {
int stage = phase + 1;
System.out.println("第" + stage + "阶段开始");
return phase + 1 == 3 || registeredParties == 0;
}
};
for (int i = 0; i < 5; i++) {
phaser.register();
new Thread(() -> {
while (!phaser.isTerminated()) {
phaser.arriveAndAwaitAdvance();
System.out.println(Thread.currentThread().getName() + "执行新一阶段的任务了");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t" + i).start();
}
TimeUnit.SECONDS.sleep(30);
}
Exchanger
这个东西就是用来协助交换两个线程的数据的,实际应用场景我觉得很少吧,我倒是觉得可以用来让一个线程拿到另一个线程交换过来的数据做个组合,比较什么的还有意义一些。
@Test
public void ExchangerTest() throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
String s1 = "abc";
try {
String s2 = exchanger.exchange(s1);
System.out.println(Thread.currentThread().getName() + s1 + "-->" + s2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
String s2 = "def";
try {
String s1 = exchanger.exchange(s2);
System.out.println(Thread.currentThread().getName() + s2 + "-->" + s1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
TimeUnit.SECONDS.sleep(2);
}
SynchronousQueue
就是一个没有存储功能的阻塞队列,适合在生产速率和消费速率相当的场景,就不解释了