✅三个线程分别顺序打印0-100

Thread-0: 0
Thread-1: 1
Thread-2: 2
Thread-0: 3
Thread-1: 4
Thread-2: 5
....
Thread-1: 100

典型回答

这个问题主要考察多线程的线程安全和通信机制,常见的处理方式有notify/synchorized和condition/ reentrantlock。但是往往有同学只注意线程安全,而忽略了通信机制,常见的错误写法如下:

public class Test {

    private static int i = 1;

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new Print(i)).start();
        }
    }

    private static class Print implements Runnable {

        private final int index;

        public Print(int index) {
            this.index = index;
        }

        @Override
        public void run() {
            while(true) {
                synchronized (Print.class) {
                    if (i >= 101) {
                        return;
                    }
                    System.out.println("Thread-" + index + " " + i++);
                }
            }
        }
    }
}

这样写固然能通过锁来保证循环打印了1-100,但是却不能保证线程是按照顺序打印的,这个时候就需要用到线程的通信机制。

Synchronized

我们可以结合Sync和Object#notifyAll来完成,如下所示

public class SortTest {

    private static final Object LOCK = new Object();
    private static volatile int count = 0;
    private static final int MAX = 100;

    public static void main(String[] args) {
        Thread thread = new Thread(new Seq(0));
        Thread thread1 = new Thread(new Seq(1));
        Thread thread2 = new Thread(new Seq(2));
        thread.start();
        thread1.start();
        thread2.start();
    }

    static class Seq implements Runnable {

        private final int index;

        public Seq(int index) {
            this.index = index;
        }

        @Override
        public void run() {
            while (count < MAX) {
                synchronized (LOCK) {
                    try {
                        while (count % 3 != index) {
                            LOCK.wait();
                        }
                        if(count <=MAX){
                            System.out.println("Thread-" + index + ": " + count);
                        }
                        count++;
                        LOCK.notifyAll();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

ReentrantLock

我们可以使用ReentrantLock和Condition尝试解决这种问题。大概的解决思路就是先通过lock对资源加锁,然后通过condition指定的唤醒下一个线程。相信大家都已经发现,这种方式比Synchronized的优点就是sync只能唤醒一个线程或者全部唤醒来让大家竞争,但是通过condition我们可以唤醒指定线程,避免资源浪费

public class Test {
    private static final int WORKER_COUNT = 3;
    private static int countIndex = 0;
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static void main(String[] args){
        final List<Condition> conditions = new ArrayList<>();
        for(int i=0; i< WORKER_COUNT; i++){
            // 为每一个线程分配一个condition
            Condition condition = LOCK.newCondition();
            conditions.add(condition);
            Worker worker = new Worker(i, conditions);
            worker.start();
        }

    }

    static class Worker extends Thread{

        int index;
        List<Condition> conditions;

        public Worker(int index, List<Condition> conditions){
            super("Thread-"+index);
            this.index = index;
            this.conditions = conditions;
        }

        private void signalNext(){
            int nextIndex = (index + 1) % conditions.size();
            conditions.get(nextIndex).signal();
        }

        @Override
        public void run(){
            while(true) {
                //锁住 保证操作间同时只有一个线程
                LOCK.lock();
                try {
                    // 如果当前线程不满足打印条件,则等待
                    if (countIndex % 3 != index) {
                        conditions.get(index).await();
                    }
                    if (countIndex > 100) {
                        // 唤醒下一个线程,保证程序正常退出
                        signalNext();
                        // 退出循环 线程运行结束
                        return;
                    }
                    System.out.println((this.getName() + " " + countIndex));
                    // 计数器+1
                    countIndex ++;
                    // 通知下一个干活
                    signalNext();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    LOCK.unlock();
                }
            }
        }
    }
}

注意,对于Worker里面的逻辑,不能为了图省事,用下面的写法:

public void run(){
    while(true) {
        //锁住 保证操作间同时只有一个线程
        LOCK.lock();
        try {
           if (countIndex > 100) {
                // 唤醒下一个线程,保证程序正常退出
                signalNext();
                // 退出循环 线程运行结束
                return;
            }

            System.out.println((this.getName() + " " + countIndex));
            // 计数器+1
            countIndex ++;
            // 通知下一个干活
            signalNext();
            conditions.get(index).await();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            LOCK.unlock();
        }
    }
}

这种写法是错误的,原因是如果刚开始不让所有线程都等待,就有可能会导致线程竞争,举个例子:

step1:thread0执行逻辑

step2:thread2被锁阻塞

step3:thread0执行完成,唤醒thread1

step4:thread2和thread1竞争锁,thread2竞争成功,就会导致thread0执行完成后,直接由thread2执行

Thread#yield

除了线程之间的通信之外,我们还可以使用一种取巧的方式,就是通过指定线程打印某些值,如Thread0打印0,3,9等值。

核心思想是通过yield自旋的方式,如果当前的值不需要被当前线程打印,那么就让出该线程。如下所示:

private static volatile int count = 0;
private static final int MAX = 100;
static class OtherWorker implements Runnable {

        private final int index;

        public OtherWorker(int index) {
            this.index = index;
        }

        @Override
        public void run() {
            while (count < MAX) {
                while (count % 3 != index) {
                    Thread.yield();
                }
                if (count > MAX) {
                    return;
                }
                System.out.println("Thread-" + index + " " + count);
                count++;
            }
        }
    }

这种方式需要线程不停的竞争和自旋,性能显然比不过前两种方法。同时,因为yield并不能保证立刻让出CPU,所以这种方法是有风险的

知识扩展

这种问题还有很多变种,如三个线程顺序打印ABC,三个线程按照要求顺序打印ABC等。

这些问题都是上线问题的变体,譬如把1-100换成ABC,常见的1-100转为ABC的代码转换如下所示:

for (int i = 0; i < 100; i ++) {
    char ascii = (char)(65 + i%3);
}

原文: https://www.yuque.com/hollis666/xkm7k3/wcqebt