通过多线程交替打印,彻底理解Java通知机制、thread与Runnable的区别

5,468 阅读5分钟

题目一:两个线程顺序打印1~100

题解:实现Runnable接口,利用wait()和notify()来交替打印

public class PrintThread implements Runnable {

    private  int count = 0;

    @Override
    public void run() {
        synchronized (this) {
            while (count <= 100) {
                //唤醒其他需要this锁的进程来竞争锁,当前进程等到临界区代码执行完毕才释放锁
                this.notify();//代码一处
                System.out.println(Thread.currentThread().getName() + "当前数字是:" + count++);

                //休息一段时间,放大差异
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                //立即阻塞当前线程
                try {
                    this.wait();//代码二处
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试代码

public class PrintTest {
    public static void main(String[] args){
        PrintThread printThread = new PrintThread(); //代码三处
        new Thread(printThread, "Thread-A").start();
        new Thread(printThread, "Thread-B").start();
    }
}

做的过程中发现有趣的点:这个循环打印0~100的也可以使用继承Thread来实现,只需要将PrintThread类后面的Implements Runnable换成Extends Thread即可,原因:因为Thread其实也是继承Runnable的,这里只是将Thread当作Runnable用(代码三处),不过不建议这么用。

分析:

先说两个注意的点:
(1)调用了notify()或者notigyAll()并不是立马就释放锁的,需要等待当前线程把临界区的代码执行完才释放。
(2)调用wait()之后当前线程会立即阻塞,JVM会在等待wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。

  1. 为什么实现Runnable接口,而不是继承Thread类?

    答:因为这道题让我们两个线程顺序打印0~100。Thread不适合资源共享,而Runnable很适合资源共享。提一下为什么Runnable适合资源共享,因为实现Runnable和Callable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。可以理解为任务是通过线程驱动从而执行的。

  2. Runnable相比与Thread具有的优势

    • 适合多个相同的程序代码的线程去处理同一个资源
    • 可以避免java中的单继承的限制
    • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
    • 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
  3. 等待通知机制的定义

    先来看一下等待 / 通知的定义(摘抄自《Java并发编程的艺术》):等待 / 通知的相关方法是任何一个Java对象都具有的。等待 / 通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另外一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作

  4. 代码一处的this.notify()调用后,会等待synchronized(this){}的所有代码执行完毕之后才释放,并不是立马释放的

  5. 代码二处的this.wait()

    当前线程调用了this.wait()方法之后,阻塞的是当前线程,然后唤醒等待this锁的线程

题目二:三个线程交替打印10次ABC

题解:使用同步块和wait()、notify()的方法控制三个线程的执行次序。具体方法如下:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能进行打印操作。一个对象是prev,即前个对象的锁,另外一个是自身对象的锁self,为了控制打印顺序,必须要先持有prev锁的情况下才能进行打印

public class PrintABC {

    public static class Printer implements Runnable {

        private String name;
        private Object prev;
        private Object self;

        public Printer(String name, Object prev, Object self) {
            this.name = name;
            this.prev = prev;
            this.self = self;
        }

        @Override
        public void run() {
            int count = 10;
            //循环10次
            while (count > 0){
                //先获取prev锁
                synchronized (prev) {
                    //接着获取self锁
                    synchronized (self) {
                        //打印
                        System.out.println(name);
                        count--;
                        //唤醒其他需要用到self锁的线程来竞争
                        //注意此时self锁还未释放
                        self.notify();
                    }
                    //现在才释放self锁,因为需要等到临界区代码执行完毕

                    //立即释放prev锁,并当前线程阻塞,等待其他线程唤醒
                    try {
                        prev.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();

        Printer pa = new Printer("A", c, a);
        Printer pb = new Printer("B", a, b);
        Printer pc = new Printer("C", b, c);

        new Thread(pa).start();
        Thread.sleep(10);//保证初始ABC的启动顺序
        new Thread(pb).start();
        Thread.sleep(10);
        new Thread(pc).start();
        Thread.sleep(10);
    }
}

更进一步理解等待 / 通知机制,以线程A举例

  • prev.wait()会释放锁,释放谁的锁?因为是prev.wait(),所以按照定义,释放的是prev锁即c锁,阻塞当前线程即线程A
  • self.notify()是释放哪个锁,唤醒哪个线程?因为是self对象,在线程A中是a,所以释放的是a锁,持有a锁的还有线程B,所以唤醒的是线程B
  • 怎么唤醒线程A?是使用a.nofity()还是c.notify()?因为前面使用的是c.wait()阻塞当前线程,所以使用c.notify()来唤醒,即线程C里面的方法

这样一来就可以保证ABC顺序打印,除了这种方法之外,这道题还可以使用Lock锁方法来实现。