面试官:请用五种方法实现多线程交替打印问题

三个线程T1、T2、T3,如何让他们按顺序执行?

这是一道面试中常考的并发编程的代码题,与它相似的问题有:

  • 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC…
  • 两个线程交替打印1-100的奇偶数
  • N个线程循环打印1-100

其实这类问题本质上都是线程通信问题,思路基本上都是一个线程执行完毕,阻塞该线程,唤醒其他线程,按顺序执行下一个线程。下面先来看最简单的,如何按顺序执行三个线程。

synchronized+wait/notify

基本思路就是线程A、线程B、线程C三个线程同时启动,因为变量num的初始值为0,所以线程B或线程C拿到锁后,进入while()循环,然后执行wait()方法,线程线程阻塞,释放锁。只有线程A拿到锁后,不进入while()循环,执行num++,打印字符A,最后唤醒线程B和线程C。此时num值为1,只有线程B拿到锁后,不被阻塞,执行num++,打印字符B,最后唤醒线程A和线程C,后面以此类推。

class Wait_Notify_ACB {

    private int num;
    private static final Object LOCK = new Object();

    private void printABC(int targetNum) {
            synchronized (LOCK) {
                while (num % 3 != targetNum) {    //想想这里为什么不能用if代替while,想不起来可以看公众号上一篇文章
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num++;
                System.out.print(Thread.currentThread().getName());
                LOCK.notifyAll();
            }
    }
    
    public static void main(String[] args) {
        Wait_Notify_ACB  wait_notify_acb = new Wait_Notify_ACB ();
        new Thread(() -> {
            wait_notify_acb.printABC(0);
        }, "A").start();
        new Thread(() -> {
            wait_notify_acb.printABC(1);
        }, "B").start();
        new Thread(() -> {
            wait_notify_acb.printABC(2);
        }, "C").start();
    }
}

输入结果:

ABC
Process finished with exit code 0

接下来看看第一个问题,三个线程T1、T2、T3轮流打印ABC,打印n次。其实只需要将上述代码加一个循环即可,这里假设n=10。

class Wait_Notify_ACB {

    private int num;
    private static final Object LOCK = new Object();

    private void printABC(int targetNum) {
        for (int i = 0; i < 10; i++) {
            synchronized (LOCK) {
                while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num++;
                System.out.print(Thread.currentThread().getName());
                LOCK.notifyAll();
            }
        }
    }
    public static void main(String[] args) {
        Wait_Notify_ACB  wait_notify_acb = new Wait_Notify_ACB ();
        new Thread(() -> {
            wait_notify_acb.printABC(0);
        }, "A").start();
        new Thread(() -> {
            wait_notify_acb.printABC(1);
        }, "B").start();
        new Thread(() -> {
            wait_notify_acb.printABC(2);
        }, "C").start();
    }    
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC
Process finished with exit code 0

下面看第二个问题,两个线程交替打印1-100的奇偶数,为了减少输入所占篇幅,这里将100 改成了10。基本思路上面类似,线程odd先拿到锁——打印数字——唤醒线程even——阻塞线程odd,以此循环。

class  Wait_Notify_Odd_Even{

    private Object monitor = new Object();
    private volatile int count;

    Wait_Notify_Odd_Even(int initCount) {
        this.count = initCount;
    }

    private void printOddEven() {
        synchronized (monitor) {
            while (count < 10) {
                try {
                    System.out.print( Thread.currentThread().getName() + ":");
                    System.out.println(++count);
                    monitor.notifyAll();
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //防止count=10后,while()循环不再执行,有子线程被阻塞未被唤醒,导致主线程不能退出
            monitor.notifyAll();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {

        Wait_Notify_Odd_Even waitNotifyOddEven = new Wait_Notify_Odd_Even(0);
        new Thread(waitNotifyOddEven::printOddEven, "odd").start();
        Thread.sleep(10); //为了保证线程odd先拿到锁
        new Thread(waitNotifyOddEven::printOddEven, "even").start();
    }
}

运行结果:

odd:1
even:2
odd:3
even:4
odd:5
even:6
odd:7
even:8
odd:9
even:10

再看第三个问题,N个线程循环打印1-100,其实仔细想想这个和三个线程循环打印ABC并没有什么本质区别,只需要加上判断是否到了打印数字的最大值的语句即可。假设N=3,为了能把输出结果完全显示,打印1-10,代码如下:

class Wait_Notify_100 {

    private int num;
    private static final Object LOCK = new Object();
    private int maxnum = 10;

    private void printABC(int targetNum) {
        while (true) {
            synchronized (LOCK) {
                while (num % 3 != targetNum) { //想想这里为什么不能用if代替,想不起来可以看公众号上一篇文章
                    if(num >= maxnum){
                        break;
                    }
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(num >= maxnum){
                    break;
                }
                num++;
                System.out.println(Thread.currentThread().getName() + ": " + num);
                LOCK.notifyAll();
            }
        }

    }
    
    public static void main(String[] args) {
        Wait_Notify_100  wait_notify_100 = new Wait_Notify_100 ();
        new Thread(() -> {
            wait_notify_100.printABC(0);
        }, "thread1").start();
        new Thread(() -> {
            wait_notify_100.printABC(1);
        }, "thread2").start();
        new Thread(() -> {
            wait_notify_100.printABC(2);
        }, "thread3").start();
    }    
}

输出结果:

thread1: 1
thread2: 2
thread3: 3
thread1: 4
thread2: 5
thread3: 6
thread1: 7
thread2: 8
thread3: 9
thread1: 10

面试官: 大家都是用的synchronized+wait/notify,你能不能换个方法解决该问题?

我: 好的,我还会用join()方法

下面介绍的方法只给出第一道题的代码了,否则太长了,相信大家可以举一反三

join()

join()方法:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。基于这个原理,我们使得三个线程按顺序执行,然后循环多次即可。无论线程1、线程2、线程3哪个先执行,最后执行的顺序都是线程1——>线程2——>线程3。代码如下:

class Join_ABC {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(new printABC(null),"A");
            Thread t2 = new Thread(new printABC(t1),"B");
            Thread t3 = new Thread(new printABC(t2),"C");
            t0.start();
            t1.start();
            t2.start();
            Thread.sleep(10); //这里是要保证只有t1、t2、t3为一组,进行执行才能保证t1->t2->t3的执行顺序。
        }

    }

    static class printABC implements Runnable{
        private Thread beforeThread;
        public printABC(Thread beforeThread) {
            this.beforeThread = beforeThread;
        }
        @Override
        public void run() {
            if(beforeThread!=null) {
                try {
                    beforeThread.join(); 
                    System.out.print(Thread.currentThread().getName());
                }catch(Exception e){
                    e.printStackTrace();
                }
            }else {
                System.out.print(Thread.currentThread().getName());
            }

        }
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官: 还会其他方法吗?

我: 还会使用Lock解决该问题。

Lock

该方法很容易理解,不管哪个线程拿到锁,只有符合条件的才能打印。代码如下:

 class Lock_ABC {

    private int num;   // 当前状态值:保证三个线程之间交替打印
    private Lock lock = new ReentrantLock();


    private void printABC(int targetNum) {
        for (int i = 0; i < 10; ) {
            lock.lock();
            if (num % 3 == targetNum) {
                num++;
                i++;
                System.out.print(Thread.currentThread().getName());
            }
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Lock_ABC lockABC = new Lock_ABC();

        new Thread(() -> {
            lockABC.printABC(0);
        }, "A").start();

        new Thread(() -> {
            lockABC.printABC(1);
        }, "B").start();

        new Thread(() -> {
            lockABC.printABC(2);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官: 该方法存在什么问题,可以进一步优化吗

我: 可以使用Lock+Condition实现对线程的精准唤醒,减少对同步锁的无意义竞争,浪费资源。

Lock+Condition

该思路和synchronized+wait/notify方法的很像,synchronized对应lock,await/signal方法对应wait/notify方法。下面的代码为了能精准地唤醒下一个线程,创建了多个Condition对象。

class LockConditionABC {

    private int num;
    private static Lock lock = new ReentrantLock();
    private static Condition c1 = lock.newCondition();
    private static Condition c2 = lock.newCondition();
    private static Condition c3 = lock.newCondition();

    private void printABC(int targetNum, Condition currentThread, Condition nextThread) {
        for (int i = 0; i < 10; ) {
            lock.lock();
            try {
                while (num % 3 != targetNum) {
                    currentThread.await();  //阻塞当前线程
                }
                num++;
                i++;
                System.out.print(Thread.currentThread().getName());
                nextThread.signal();    //唤醒下一个线程,而不是唤醒所有线程
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        LockConditionABC print = new LockConditionABC();
        new Thread(() -> {
            print.printABC(0, c1, c2);
        }, "A").start();
        new Thread(() -> {
            print.printABC(1, c2, c3);
        }, "B").start();
        new Thread(() -> {
            print.printABC(2, c3, c1);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官: 除了该方法,还有什么方法可以避免唤醒其他无意义的线程避免资源浪费?

我: 可以通过使用信号量来实现。

Semaphore

Semaphore:用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。

一个线程要访问共享资源,先使用acquire()方法获得信号量,如果信号量的计数器值大于等于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。

当某个线程使用完共享资源后,使用release()释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

代码如下:

class SemaphoreABC {

    private static Semaphore s1 = new Semaphore(1); //因为先执行线程A,所以这里设s1的计数器为1
    private static Semaphore s2 = new Semaphore(0);
    private static Semaphore s3 = new Semaphore(0);


    private void printABC(Semaphore currentThread, Semaphore nextThread) {
        for (int i = 0; i < 10; i++) {
            try {
                currentThread.acquire();       //阻塞当前线程,即信号量的计数器减1为0
                System.out.print(Thread.currentThread().getName());
                nextThread.release();          //唤醒下一个线程,即信号量的计数器加1

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SemaphoreABC printer = new SemaphoreABC();
        new Thread(() -> {
            printer.printABC(s1, s2);
        }, "A").start();
        Thread.sleep(10);
        new Thread(() -> {
            printer.printABC(s2, s3);
        }, "B").start();
        Thread.sleep(10);
        new Thread(() -> {
            printer.printABC(s3, s1);
        }, "C").start();
    }
}

输出结果:

ABCABCABCABCABCABCABCABCABCABC

面试官: 除了上述五种方法,还有其他方法吗

我: 还有LockSupport、CountDownLatch、AtomicInteger等等。

面试官: 那如何实现三个线程循环打印ACB,其中A打印两次,B打印三次,C打印四次呢?

我:

面试官: 如何用两个线程交叉打印数字和字符呢?例如A1B2C3…Z26

我:

大家可以思考下后面两个升级版的问题,原理都是相通的。

微信搜索公众号路人zhang,回复面试手册,领取更多高频面试题PDF版及更多面试资料。

推荐阅读:

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值