zoukankan      html  css  js  c++  java
  • 面试题:线程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则是利用了计数器。

  • 相关阅读:
    缓存与清除缓存
    PHP文件缓存与memcached缓存 相比 优缺点是什么呢
    memcached的基本命令(安装、卸载、启动、配置相关)
    54点提高PHP编程效率 引入缓存机制提升性能
    登陆类
    格式化金额数与自动四舍五入
    如何用Ajax传一个数组数据
    CodeIgniter的缓存机制与使用方法
    CI框架缓存的实现原理
    PHP导出数据库方法
  • 原文地址:https://www.cnblogs.com/enjoyitlife/p/12093960.html
Copyright © 2011-2022 走看看