你会这道阿里多线程面试题吗?

背景

在前几天,群里有个群友问了我一道面试阿里的时候遇到的多线程题目,这个题目比较有意思,在这里和你们分享一下。java

废话很少说,直接上题目:git

经过N个线程顺序循环打印从0至100,如给定N=3则输出:
thread0: 0
thread1: 1
thread2: 2
thread0: 3
thread1: 4
.....
复制代码

一些常常刷面试题的朋友,以前确定遇到过下面这个题目:github

两个线程交替打印0~100的奇偶数:
偶线程:0
奇线程:1
偶线程:2
奇线程:3
复制代码

这两个题目看起来类似,第二个题目稍微来讲比较简单一点,你们能够先思考一下两个线程奇偶数如何打印。面试

两线程奇偶数打印

有一些人这里可能会用讨巧的,用一个线程进行循环,在每次循环里面都会作是奇数仍是偶数的判断,而后打印出这个咱们想要的结果。在这里咱们不过多讨论这种违背题目本意的作法。数组

其实要作这个题目咱们就须要控制两个线程的执行顺序,偶线程执行完以后奇数线程执行,这个有点像通知机制,偶线程通知奇线程,奇线程再通知偶线程。而一看到通知/等待,立马就有朋友想到了Object中的wait和notify。没错,这里咱们用wait和notify对其进行实现,代码以下:bash

public class 交替打印奇偶数 {
    static class SoulutionTask implements Runnable{
        static int value = 0;
        @Override
        public void run() {
            while (value <= 100){
                synchronized (SoulutionTask.class){
                    System.out.println(Thread.currentThread().getName() + ":" + value++);
                    SoulutionTask.class.notify();
                    try {
                        SoulutionTask.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new SoulutionTask(), "偶数").start();
        new Thread(new SoulutionTask(), "奇数").start();
    }
}
复制代码

这里咱们有两个线程,经过notify和wait用来控制咱们线程的执行,从而打印出咱们目标的结果多线程

N个线程循环打印

再回到咱们最初的问题来,N个线程进行循环打印,这个问题我再帮助群友解答了以后,又再次把这个问题在群里面抛了出来,很多老司机以前看过交替打印奇偶数这道题目,因而立刻作出了几个版本,让咱们看看老司机1的代码:ide

public class 老司机1 implements Runnable {

    private static final Object LOCK = new Object();
    /**
     * 当前即将打印的数字
     */
    private static int current = 0;
    /**
     * 当前线程编号,从0开始
     */
    private int threadNo;
    /**
     * 线程数量
     */
    private int threadCount;
    /**
     * 打印的最大数值
     */
    private int maxInt;

    public 老司机1(int threadNo, int threadCount, int maxInt) {
        this.threadNo = threadNo;
        this.threadCount = threadCount;
        this.maxInt = maxInt;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (LOCK) {
                // 判断是否轮到当前线程执行
                while (current % threadCount != threadNo) {
                    if (current > maxInt) {
                        break;
                    }
                    try {
                        // 若是不是,则当前线程进入wait
                        LOCK.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                // 最大值跳出循环
                if (current > maxInt) {
                    break;
                }
                System.out.println("thread" + threadNo + " : " + current);
                current++;
                // 唤醒其余wait线程
                LOCK.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        int threadCount = 3;
        int max = 100;
        for (int i = 0; i < threadCount; i++) {
            new Thread(new 老司机1(i, threadCount, max)).start();
        }
    }
}

复制代码

核心方法在run里面,能够看见和咱们交替打印奇偶数原理差很少,这里将咱们的notify改为了notifyAll,这里要注意一下不少人会将notifyAll理解成其余wait的线程所有都会执行,实际上是错误的。这里只会将wait的线程解除当前wait状态,也叫做唤醒,因为咱们这里用同步锁synchronized块包裹住,那么唤醒的线程会作会抢夺同步锁。学习

这个老司机的代码的确能跑通,可是有一个问题是什么呢?当咱们线程数很大的时候,因为咱们不肯定唤醒的线程究竟是否是下一个要执行的就有可能会出现抢到了锁但不应本身执行,而后又进入wait的状况,好比如今有100个线程,如今是第一个线程在执行,他执行完以后须要第二个线程执行,可是第100个线程抢到了,发现不是本身而后又进入wait,而后第99个线程抢到了,发现不是本身而后又进入wait,而后第98,97...直到第3个线程都抢到了,最后才到第二个线程抢到同步锁,这里就会白白的多执行不少过程,虽然最后能完成目标。ui

还有其余老司机用lock/condition也实现了这样的功能,还有老司机用比较新颖的方法好比队列去作,固然这里就很少提了,大体的原理都是基于上面的,这里我说一下个人作法,在Java的多线程中提供了一些经常使用的同步器,在这个场景下比较适合于使用Semaphore,也就是信号量,咱们上一个线程持有下一个线程的信号量,经过一个信号量数组将所有关联起来,代码以下:

static int result = 0;
    public static void main(String[] args) throws InterruptedException {
        int N = 3;
        Thread[] threads = new Thread[N];
        final Semaphore[] syncObjects = new Semaphore[N];
        for (int i = 0; i < N; i++) {
            syncObjects[i] = new Semaphore(1);
            if (i != N-1){
                syncObjects[i].acquire();
            }
        }
        for (int i = 0; i < N; i++) {
            final Semaphore lastSemphore = i == 0 ? syncObjects[N - 1] : syncObjects[i - 1];
            final Semaphore curSemphore = syncObjects[i];
            final int index = i;
            threads[i] = new Thread(new Runnable() {

                public void run() {
                    try {
                        while (true) {
                            lastSemphore.acquire();
                            System.out.println("thread" + index + ": " + result++);
                            if (result > 100){
                                System.exit(0);
                            }
                            curSemphore.release();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            });
            threads[i].start();
        }
    }
复制代码

经过这种方式,咱们就不会有白白唤醒的线程,每个线程都按照咱们所约定的顺序去执行,这其实也是面试官所须要考的地方,让每一个线程的执行都能再你手中获得控制,这也能够验证你多线程知识是否牢固。

最后这篇文章被我收录于JGrowing-Java面试篇,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:https://github.com/javagrowing/JGrowing 麻烦给个小星星哟。

若是你们以为这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O:

相关文章
相关标签/搜索