随着互联网数据呈指数增长,多线程编程已成为新时代码农的必备技能之一。在特定业务场景下,我们还需要让线程之间进行同步,以最大限度提高程序的吞吐率。我们可以通过以下几种方式来进行线程之间的同步:
- Notify,NotifyAll和Wait
- Suspend与Resume
- CountDownLatch
- CyclicBarrier
Notify,NotifyAll和Wait
我们可以通过网上流传的一道阿里面试题来看看notify与wait是如何搭配使用的,题目是这样的:使用wait notify 让两个线程交替打印出0到100的奇偶数。
public class Test {
private volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
Thread evenThread = new Thread(()->{test.printEven();});
Thread oddThread = new Thread(()->{test.printOdd();});
evenThread.start();
oddThread.start();
}
private synchronized void printOdd(){
while (true){
if(num%2 != 0){
System.out.println("odd thread " + num);
if(num == 99){
num++;
notify();
break;
}
num++;
}else {
notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private synchronized void printEven(){
while (true){
if(num%2 == 0){
System.out.println("even thread " + num);
if(num == 100){
break;
}
num++;
}else {
notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Wait
我们先来看一下wait方法的使用和原理:
- wait方法会将当前运行的线程挂起(进入阻塞状态),等待notify或notifyAll的唤醒。JDK也提供有时间参数的wait方法,在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
- wait的底层是通过对象的监视器来完成,这意味着wait只能在同步代码块中调用,否则会抛IllegalMonitorStateException,在Java中,同步代码块一般是通过synchronized关键字实现,不了解synchronized的同学可以参考另一篇文章。Synchronized
- 通过wai方法挂起线程,线程会释放掉自己所持有的锁,当线程被唤醒后,还需要争抢到相应的锁之后才能继续执行。
Notify,NotifyAll
通过wait方法阻塞的线程,我们可以通过 Notify或NotifyAll来唤醒阻塞在对象moniter上的线程,不同的是,如果有多个线程阻塞在同一个监视器上,notify只唤醒其中一个,而notifyAll则唤醒所有的线程。
Suspend与Resume
我们也可以通过Suspend与Resume来进行线程之间的等待和唤醒
public class Test {
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
Thread thread = new Thread(test::waitFun);
thread.start();
thread.suspend();
System.out.println("gg");
thread.resume();
}
private void waitFun() {
System.out.println("hello");
System.out.println("hello this is waitFun");
}
}
与wait方法不同的是,通过Suspend方法挂起的线程,不会释放自己所持有的锁,且在JDK1.8中,Suspend与Resume已标记为“已废弃”,因为这两个方法非常容易产生死锁,比如线程A调用了Suspend,线程B在调用Resume之前尝试去获取线程A的锁,这是线程A与线程B会相互等待形成死锁。
CountDownLatch
我们可以通过我们在B站白嫖视频的过程来直观地感受一下CountDownLatch的使用
一般来说,我们在看UP主吹nb的同时,会发发弹幕,遇到知识点可能还会记一记笔记,这几件事是可以同时进行的,只有等这几件事做完了,我们才能算是成功白嫖。对应到代码中,我们首先初始化了CountDownLatch并设置了计数器,在不同的线程中,每完成一个任务,我们就将计数器减1,countDownLatch的wait()方法会阻塞主线程直到计数器为0。
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(()->{
System.out.println("看UP吹nb");
countDownLatch.countDown();
}).start();
new Thread(()->{
System.out.println("发弹幕");
countDownLatch.countDown();
}).start();
new Thread(()->{
System.out.println("记笔记");
countDownLatch.countDown();
}).start();
countDownLatch.wait();
System.out.println("成功白嫖");
}
CyclicBarrier
CyclicBarrier也是通过计数器来控制线程间的同步,但是在使用上与 CountDownLatch 不太相同:
- 创建 CyclicBarrier 的时候,我们还需要传入了一个回调函数,当计数器减到 0 的时候,会调用这个回调函数。
- CyclicBarrier 的计数器有自动重置的功能,当减到 0 的时候,会自动重置你设置的初始值。 从某种角度看,如果CountDownLatch是白嫖一次视频,那么CyclicBarrier就是多次白嫖,不断地等待计数器减到0,然后执行同一个回调。
public class Test {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2,()->{doSomeThing();});
new Thread(()->{
while (true){
System.out.println("看UP吹nb");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while (true){
System.out.println("发弹幕");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private static void doSomeThing(){
System.out.println("白嫖视频\n");
}
}