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也是类似原理。

  • 相关阅读:
    OK335xS-Android mkmmc-android-ubifs.sh hacking
    OK335xS-Android pack-ubi-256M.sh hacking
    OK335xS Ubuntu 12.04.1 版本 Android 开发环境搭建
    Qt Quick Hello World hacking
    Qt QML referenceexamples attached Demo hacking
    QT 5.4.1 for Android Ubuntu QtWebView Demo
    I.MX6 working note for high efficiency
    QT 5.4.1 for Android Windows环境搭建
    mkbootimg hacking
    Generate And Play A Tone In Android hacking
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/how-to-make-two-threads-outputing-char-alternartively.html
Copyright © 2011-2022 走看看