zoukankan      html  css  js  c++  java
  • 假期充电: 一道并发java面试题的N种解法

    疫情居家隔离期间,在网上看了几个技术教学视频,意在查漏补缺,虽然网上这些视频的水平鱼龙混杂,但也有讲得相当不错的,这是昨晚看到的马老师讲的一道面试题,记录一下:

    如上图,有2个同时运行的线程,一个输出ABCDE,一个输出12345,要求交替输出,即:最终输出A1B2C3D4E5,而且要求thread-1先执行。

    主要考点:二个线程如何通信?通俗点讲,1个线程干到一半,怎么让另1个线程知道我在等他?

    方法1:利用LockSupport

    import java.util.concurrent.locks.LockSupport;
    
    public class Test01 {
    
        //这里一定要初始化成null,否则在线程内部无法引用,会提示未初始化
        static Thread t1 = null, t2 = null;
    
        public static void main(String[] args) {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            t1 = new Thread(() -> {
                for (char c : cA) {
                    System.out.print(c);
                    //解锁T2线程(注:unpark线程t2后,t2即使再调用LockSupport.park也锁不住)
                    LockSupport.unpark(t2);
                    //再把自己T1卡住(直到T2为它解锁)
                    LockSupport.park(t1);
                }
            }, "t1");
    
            t2 = new Thread(() -> {
                for (char c : cB) {
                    //先把T2自己卡住(直到T1为它解锁)
                    LockSupport.park(t2);
                    System.out.print(c);
                    //再把T1解锁
                    LockSupport.unpark(t1);
                }
    
            }, "t2");
    
            t1.start();
            t2.start();
        }
    }
    

    优点:逻辑清晰,代码简洁,可认为是最优解。 

    方法2:模拟自旋锁的做法,利用标志位不断尝试

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Test02a {
        
        public static void main(String[] args) {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            //AtomicInteger保证线程安全,值1表示t1可继续 ,值2表示t2可继续
            AtomicInteger flag = new AtomicInteger(1);
    
            new Thread(() -> {
                for (char c : cA) {
                    //不断"自旋"重试
                    while (flag.get() != 1) {
                    }
                    System.out.print(c);
                    //标志位指向t2
                    flag.set(2);
                }
            }, "t1").start();
    
            new Thread(() -> {
                for (char c : cB) {
                    while (flag.get() != 2) {
                    }
                    System.out.print(c);
                    //标志位指向t1
                    flag.set(1);
                }
            }, "t2").start();
        }
    }
    

    优点:思路纯朴无华,容易理解。缺点:自旋尝试比较占用cpu,如果有更多线程参与竞争,cpu可能会较高。

    这个方法还有一个变体,不借助并发包下的AtomicInteger,可以改用static valatile + enum变量保证线程安全:

    public class Test02b {
    
        enum ReadyToGo {
            T1, T2
        }
    
        static volatile ReadyToGo r = ReadyToGo.T1;
    
        public static void main(String[] args) {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            new Thread(() -> {
                for (char c : cA) {
                    while (!r.equals(ReadyToGo.T1)) {
                    }
                    System.out.print(c);
                    r = ReadyToGo.T2;
                }
            }).start();
    
            new Thread(() -> {
                for (char c : cB) {
                    while (!r.equals(ReadyToGo.T2)) {
                    }
                    System.out.print(c);
                    r = ReadyToGo.T1;
                }
            }).start();
        }
    }
    

      

    方法3:利用ReentrantLock可重入锁及Condition条件

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Test03 {
    
        public static void main(String[] args) {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            Lock lock = new ReentrantLock();
            Condition cond1 = lock.newCondition();
            Condition cond2 = lock.newCondition();
    
            CountDownLatch latch = new CountDownLatch(1);
    
            new Thread(() -> {
                //保证t1先执行
                latch.countDown();
    
                lock.lock();
                try {
                    for (char c : cA) {
                        System.out.print(c);
                        //"唤醒"满足条件2的线程t2
                        cond2.signal();
                        //卡住满足条件1的线程t1
                        cond1.await();
                    }
                    //输出最后1个字符后,把t2也唤醒(否则t2一直await永远退出不了)
                    cond2.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
    
            new Thread(() -> {
                try {
                    //先把t2卡住,保证t1先输出
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                lock.lock();
                try {
                    for (char c : cB) {
                        System.out.print(c);
                        //"唤醒"满足条件1的线程t1
                        cond1.signal();
                        //卡住满足条件2的线程t2
                        cond2.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "t2").start();
            
        }
    }
    

    方法4:利用阻塞队列BlockingQueue

    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    public class Test04 {
    
        public static void main(String[] args) {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            BlockingQueue<Boolean> q1 = new LinkedBlockingQueue<>(1);
            BlockingQueue<Boolean> q2 = new LinkedBlockingQueue<>(1);
    
            new Thread(() -> {
                for (char c : cA) {
                    System.out.print(c);
                    try {
                        //放行t2
                        q2.put(true);
                        //阻塞t1
                        q1.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t1").start();
    
            new Thread(() -> {
                for (char c : cB) {
                    try {
                        //先阻塞t2
                        q2.take();
                        System.out.print(c);
                        //再放行t1
                        q1.put(true);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "t2").start();
            
        }
    }
    

    点评:巧妙利用了阻塞队列的特性,思路新颖

    方法5:利用IO管道输入/输出流

    import java.io.IOException;
    import java.io.PipedInputStream;
    import java.io.PipedOutputStream;
    
    public class Test05 {
    
        public static void main(String[] args) throws IOException {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            PipedInputStream input1 = new PipedInputStream();
            PipedInputStream input2 = new PipedInputStream();
            PipedOutputStream output1 = new PipedOutputStream();
            PipedOutputStream output2 = new PipedOutputStream();
    
            input1.connect(output2);
            input2.connect(output1);
    
            //相当于令牌(在2个管道中流转)
            String flag = "1";
    
            new Thread(() -> {
    
                byte[] buffer = new byte[1];
                for (char c : cA) {
                    try {
                        System.out.print(c);
                        //将令牌通过output1->input2给到t2
                        output1.write(flag.getBytes());
                        //从output2->input1读取令牌(没有数据时,该方法会block,即:相当于卡住自己)
                        input1.read(buffer);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }, "t1").start();
    
            new Thread(() -> {
                byte[] buffer = new byte[1];
                for (char c : cB) {
                    try {
                        //读取t1通过output1->input2传过来的令牌(无数据时,会block住自己)
                        input2.read(buffer);
                        System.out.print(c);
                        //将令牌通过output2->input1给到t1
                        output2.write(flag.getBytes());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }, "t2").start();
        }
    }
    

    效率极低,纯属炫技。主要利用了管道流read操作,无数据时,会block的特性,类似阻塞队列。

     

    方法6:利用synchronized/notify/wait

    import java.util.concurrent.CountDownLatch;
    
    public class Test06 {
    
        public static void main(String[] args) {
            char[] cA = "ABCDEFG".toCharArray();
            char[] cB = "1234567".toCharArray();
    
            Object lockObj = new Object();
    
            CountDownLatch latch = new CountDownLatch(1);
    
            new Thread(() -> {
                //保证t1先输出
                latch.countDown();
    
                synchronized (lockObj) {
                    for (char c : cA) {
                        System.out.print(c);
                        //通知等待锁释放的其它线程,即:交出锁,然后通知t2去抢
                        lockObj.notify();
                        try {
                            //自己进入等待锁的队列(即:卡住自己)
                            lockObj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //输出完后,把自己唤醒,以便线程能结束
                    lockObj.notify();
                }
    
            }, "t1").start();
    
            new Thread(() -> {
                try {
                    //先卡住t2,让t1先输入
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                synchronized (lockObj) {
                    for (char c : cB) {
                        System.out.print(c);
                        //通知等待锁释放的其它线程,即:交出锁,然后通知t1去抢
                        lockObj.notify();
                        try {
                            //自己进入等待锁的队列(即:卡住自己)
                            lockObj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    //                lockObj.notify();
                }
            }, "t2").start();
        }
    }
    

    这是正统解法,原理是先让t1抢到锁(这时t2在等待锁),然后输出1个字符串后,通知t2抢锁,然后t1开始等锁,t2也是类似原理。

  • 相关阅读:
    你做的页面有哪些浏览器测试过?这些浏览器的内核分别是什么?
    响应式布局
    Promise(二)
    Promise(一)
    同步和异步的自我理解
    null 与 undefined 区别
    python pandas 数据处理
    【python】list、tuple、dict、set、dataframe、narray、series之间的区别
    时间序列常用模型
    Linux系统学习笔记
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/how-to-make-two-threads-outputing-char-alternartively.html
Copyright © 2011-2022 走看看