【深入浅出Java多线程】线程间同步

565 阅读4分钟

随着互联网数据呈指数增长,多线程编程已成为新时代码农的必备技能之一。在特定业务场景下,我们还需要让线程之间进行同步,以最大限度提高程序的吞吐率。我们可以通过以下几种方式来进行线程之间的同步:

  1. Notify,NotifyAll和Wait
  2. Suspend与Resume
  3. CountDownLatch
  4. 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方法的使用和原理: image.png

  1. wait方法会将当前运行的线程挂起(进入阻塞状态),等待notify或notifyAll的唤醒。JDK也提供有时间参数的wait方法,在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
  2. wait的底层是通过对象的监视器来完成,这意味着wait只能在同步代码块中调用,否则会抛IllegalMonitorStateException,在Java中,同步代码块一般是通过synchronized关键字实现,不了解synchronized的同学可以参考另一篇文章。Synchronized
  3. 通过wai方法挂起线程,线程会释放掉自己所持有的锁,当线程被唤醒后,还需要争抢到相应的锁之后才能继续执行。

Notify,NotifyAll

image.png 通过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的使用

未命名文件.png

一般来说,我们在看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 不太相同:

  1. 创建 CyclicBarrier 的时候,我们还需要传入了一个回调函数,当计数器减到 0 的时候,会调用这个回调函数。
  2. 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");
    }
}