面试题:线程A打印1-10数字,打印到第5个数字时,通知线程B

此题考查的是线程间的通信方式。

  • 可以利用park/unpark实现
  • 可以利用volatile关键字实现
  • 可以利用synchronized结合wait notify实现
  • 可以利用JUC中的CountDownLatch实现
  • 可以利用Condition中的await signal 实现

代码示例

利用Park/Unpak实现线程通信

private void notifyThreadWithParkUnpark(){

        Thread thb  = new Thread("线程B"){
            @Override
            public void run() {
                LockSupport.park();
                System.out.println(Thread.currentThread().getName()+"启动了");
            }
        };
        Thread tha =new Thread("线程A"){
            @Override
            public void run() {
                for(int i=1;i<11;i++){
                    System.out.println(Thread.currentThread().getName()+i);
                    if(i==5){
                        LockSupport.unpark(thb);
                    }
                }
            }
        };
        thb.start();
        tha.start();
    }

park与unpark可以看做一个令牌,park就是等待令牌,unpark就是颁发一个令牌,另外需要注意的是park与unpark的调用次数不用一一对应,而且假如在同步代码块中调用park方法,线程会进入阻塞状态,但是不会释放已经占用的锁。

本例使用park使线程B进入阻塞等待状态,在线程A调用unpark并传入线程B的名称使线程B可以继续运行。

使用Volatile关键字实现线程通信

private static volatile boolean flag = false;

private void notifyThreadWithVolatile(){
        Thread thc= new Thread("线程C"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if(i==5){
                        flag=true;
                        try {
                            Thread.sleep(500L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        };

        Thread thd= new Thread("线程D"){
            @Override
            public void run() {
                while (true){
                    // 防止伪唤醒 所以使用了while
                    while(flag){
                        System.out.println(Thread.currentThread().getName()+"收到通知");
                        break;
                    }
                }
            }
        };

        thd.start();
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            e.printStackTrace();
        }
        thc.start();

    }

volatile表示的禁用CPU缓存,用volatile修饰的变量,会强制从主内存中读取变量的值。java内存模型中关于volatile也是有说明的,volatile只能保证可见性,但不能保证原子性。

本例通过在volatile来修饰一个标志位,线程C修改了该标志位,然后线程D就可以“看到”标志位的修改,从而实现互相通信。

使用Synchronized 集合wait notify实现线程间通信

private static final Object lock = new Object();

private void notifyThreadWithSynchronized(){
        Thread the = new Thread("线程E"){
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i <10 ; i++) {
                        System.out.println(Thread.currentThread().getName()+i);
                        if(i==5){
                            lock.notify();
                        }
                    }
                }
            }
        };


        Thread thf = new Thread("线程F"){
            @Override
            public void run() {
                while(true){
                    synchronized (lock){
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"启动了");
                    }
                }
            }
        };
        thf.start();
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        the.start();

    }

synchronized修饰同步代码块,而wait notify notify必须是在synchronized修饰代码块中使用,否则会抛出监视器异常。

本实例定义一个对象锁,而线程F首先获取到互斥锁,在执行wait()方法时,释放已经持有的互斥锁,进入等待队列。而线程E执行获取到互斥锁开始执行,当1==5时,调用notify方法,就会通知lock的等待队列,然后线程E会继续执行,由于线程F此时还是获取不到互斥锁(因为被线程E占用),所以会在线程E执行完毕后,才能获取到执行权。

利用CountDonwLatch实现线程间通信

//      倒计时器
    private CountDownLatch cdl = new CountDownLatch(1);

private void notifyThreadWithCountDownLatch(){
        Thread thg = new Thread("线程G"){
            @Override
            public void run() {
                try {
                    cdl.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"启动了");
            }
        };

        thg.start();

        Thread thh = new Thread("线程H"){
            @Override
            public void run() {
                for (int i = 1; i < 11; i++) {
                    System.out.println(Thread.currentThread().getName()+i);
                    if(i==5){
                        cdl.countDown();
                    }
                }

            }
        };

        thh.start();
    }

本示例中使用了CountDownLatch倒计时器,利用了倒计时器的阻塞特性来实现等待。具体就是声明一个计数器为1的倒计时器,线程G调用await()方法进入等待,直到计数器为0的时候才能够进入执行,而线程H在i==5会将计数器减一,使其为0,此时线程G就会继续执行了。

利用Condition中的await和signal来实现

//      ReentrantLock+ condition
    private Lock rtl=new ReentrantLock();
    private Condition condition = rtl.newCondition();

private void notifyThreadWithCondition(){

        Thread thi = new Thread("线程I"){
            @Override
            public void run() {

                while (true){
                    rtl.lock();
                    try {
                        condition.await();
                        System.out.println(Thread.currentThread().getName()+"启动了");
                        break;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        rtl.unlock();
                    }
                }
            }
        };


        Thread thj = new Thread("线程J"){
            @Override
            public void run() {
                rtl.lock();
                try {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName()+i);
                        if(i==5){
                            condition.signal();
                        }
                    }
                } finally {
                    rtl.unlock();
                }

            }
        };

        thi.start();
        try {
            Thread.sleep(500L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thj.start();
    }

本示例是结合ReentrantLock和Condition来进行控制线程间的执行顺序,Condition的await()和signal(),他们的语义和wait notify是一样的。区别是在synchronized代码块里调用wait notify。通过示例可以看到这中方法实现会不断的加锁与解锁,所以看起来稍微复杂些。

总结

通过以上代码看到通过volatile的方式是最简洁方便,用park与unpark方式是比较灵活,不用加锁或解锁,剩下的synchronized与Conditon都是用了锁,而CountDownLatch则是利用了计数器。

posted @ 2019-12-24 22:33  进击的大兵  阅读(1037)  评论(0编辑  收藏  举报