假期充电: 一道并发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也是类似原理。

posted @ 2020-02-18 16:59  菩提树下的杨过  阅读(373)  评论(0编辑  收藏