V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mightofcode
V2EX  ›  Java

三线程打印 ABC,循环十次的 N 种实现方式

  •  
  •   mightofcode · 2020-01-01 15:49:06 +08:00 · 3876 次点击
    这是一个创建于 1823 天前的主题,其中的信息可能已经有所发展或是发生改变。

    编写一个程序,开启 3 个线程 A,B,C,这三个线程的输出分别为 A、B、C,每个线程将自己的 输出在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC....

    核心在于多线程同步

    完整代码: https://github.com/mightofcode/javacc

    方法 1,轮询 AtomicInteger

    缺点是轮询白耗 CPU,性能很差

    package com.mocyx.javacc.printer;
    
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * 轮询 AtomicInteger 实现交替输出 ABC
     * @author Administrator
     */
    @Component
    public class PollingPrinter implements Printer {
    
        private final AtomicInteger atomicInteger = new AtomicInteger(0);
    
        class Worker implements Runnable {
    
            private String pstr;
            private int index;
            private int gap;
            private int max;
    
            public Worker(String pstr, int index, int gap, int max) {
                this.pstr = pstr;
                this.index = index;
                this.gap = gap;
                this.max = max;
            }
    
            @Override
            public void run() {
                while (true) {
                    int v = atomicInteger.get();
                    if (v == max) {
                        return;
                    } else {
                        if (v % gap == index) {
                            System.out.print(pstr);
                            atomicInteger.set(v + 1);
                        }
                    }
                }
            }
        }
    
    
        @Override
        public void print() {
            List<Thread> threads = new ArrayList<>();
            threads.add(new Thread(new Worker("A", 0, 3, 30)));
            threads.add(new Thread(new Worker("B", 1, 3, 30)));
            threads.add(new Thread(new Worker("C", 2, 3, 30)));
            for (Thread t : threads) {
                t.start();
            }
            try {
                for (Thread t : threads) {
                    t.join();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    

    方法 2,使用 Lock & Condition 同步

    使用 Lock & Condition 要注意: 1 检查条件谓词,避免信号丢失和过早唤醒 2 注意在 finally 中进行 unlock,否则出现异常会 hang 住

    package com.mocyx.javacc.printer;
    
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 使用 Lock and Condition 进行同步
     *
     * @author Administrator
     */
    @Component
    public class SignalPrinter implements Printer {
        private final Lock lock = new ReentrantLock();
        private volatile int counter = 0;
    
        class Worker implements Runnable {
    
            Condition curCondition;
            Condition nextCondition;
            String pstr;
            int max;
            int index;
            int gap;
    
            public Worker(String pstr, int index, int gap, int max, Condition curCondition, Condition nextCondition) {
                this.pstr = pstr;
                this.max = max;
                this.curCondition = curCondition;
                this.nextCondition = nextCondition;
                this.index = index;
                this.gap = gap;
            }
    
            private boolean isMyTurn() {
                return counter % gap == index;
            }
    
            @Override
            public void run() {
                while (true) {
                    lock.lock();
                    try {
                        while (!isMyTurn()) {
                            try {
                                curCondition.await();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        if (counter < max) {
                            System.out.print(pstr);
                        }
                        counter += 1;
                        nextCondition.signalAll();
                    } finally {
                        lock.unlock();
                    }
                    if (counter >= max) {
                        return;
                    }
                }
            }
        }
    
        @Override
        public void print() {
            List<Thread> threads = new ArrayList<>();
            List<Condition> conditions = new ArrayList<>();
    
            conditions.add(lock.newCondition());
            conditions.add(lock.newCondition());
            conditions.add(lock.newCondition());
    
            threads.add(new Thread(new Worker("A", 0, 3, 30, conditions.get(0), conditions.get(1))));
            threads.add(new Thread(new Worker("B", 1, 3, 30, conditions.get(1), conditions.get(2))));
            threads.add(new Thread(new Worker("C", 2, 3, 30, conditions.get(2), conditions.get(0))));
    
            for (Thread t : threads) {
                t.start();
            }
    
            try {
                for (Thread t : threads) {
                    t.join();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    }
    
    

    方法 3,使用 Semphore 进行同步

    相比 Lock & Condition,使用 Semphore 代码比较简洁,不容易出错

    package com.mocyx.javacc.printer;
    
    import org.springframework.stereotype.Component;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Semaphore;
    
    /**
     * @author Administrator
     */
    @Component
    public class SemaphorePrinter implements Printer {
    
    
    
        class Worker implements Runnable {
            private String pstr;
    
            private Semaphore curSemphore;
            private Semaphore nextSemphore;
            private int count = 0;
    
            Worker(String pstr, int count, Semaphore curSemphore, Semaphore nextSemphore) {
                this.pstr = pstr;
                this.count = count;
                this.curSemphore = curSemphore;
                this.nextSemphore = nextSemphore;
            }
    
            @Override
            public void run() {
                for (int i = 0; i < count; i++) {
                    try {
                        curSemphore.acquire(1);
                        System.out.print(pstr);
                        nextSemphore.release(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        @Override
        public void print() {
            List<Thread> threads = new ArrayList<>();
            List<Semaphore> semaphores = new ArrayList<>();
    
            semaphores.add(new Semaphore(0));
            semaphores.add(new Semaphore(0));
            semaphores.add(new Semaphore(0));
    
            threads.add(new Thread(new Worker("A", 10, semaphores.get(0), semaphores.get(1))));
            threads.add(new Thread(new Worker("B", 10, semaphores.get(1), semaphores.get(2))));
            threads.add(new Thread(new Worker("C", 10, semaphores.get(2), semaphores.get(0))));
    
            for (Thread t : threads) {
                t.start();
            }
    
            semaphores.get(0).release(1);
    
            try {
                for (Thread t : threads) {
                    t.join();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    

    方法 4,使用 BlockingQueue 进行同步

    思路跟 go channel 类似,通过 BlockingQueue 传递信息

    package com.mocyx.javacc.printer;
    
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 使用阻塞队列进行同步
     *
     * @author Administrator
     */
    public class QueuePrinter implements Printer {
    
    
        static class Msg {
            public static final Msg PRINT_SUCCESS = new Msg();
            public static final Msg PRINT = new Msg();
            public static final Msg QUIT = new Msg();
        }
    
        class Channel {
            BlockingQueue<Msg> inQueue = new ArrayBlockingQueue<Msg>(100);
            BlockingQueue<Msg> outQueue = new ArrayBlockingQueue<Msg>(100);
        }
    
        class Worker implements Runnable {
    
            Channel inChannel;
            String pstr;
    
            Worker(String pstr, Channel inChannel) {
                this.inChannel = inChannel;
                this.pstr = pstr;
            }
    
            @Override
            public void run() {
                while (true) {
                    try {
                        Msg msg = inChannel.inQueue.take();
                        if (msg == Msg.PRINT) {
                            System.out.print(pstr);
                            inChannel.outQueue.put(Msg.PRINT_SUCCESS);
                        } else if (msg == Msg.QUIT) {
                            return;
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        @Override
        public void print() {
            List<Thread> threads = new ArrayList<>();
    
            List<Channel> channels = new ArrayList<>();
            channels.add(new Channel());
            channels.add(new Channel());
            channels.add(new Channel());
    
    
            threads.add(new Thread(new Worker("A", channels.get(0))));
            threads.add(new Thread(new Worker("B", channels.get(1))));
            threads.add(new Thread(new Worker("C", channels.get(2))));
    
            for (Thread t : threads) {
                t.start();
            }
    
            for (int i = 0; i < 30; i++) {
                try {
                    channels.get(i % channels.size()).inQueue.put(Msg.PRINT);
                    channels.get(i % channels.size()).outQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (Channel c : channels) {
                c.inQueue.add(Msg.QUIT);
            }
    
            try {
                for (Thread t : threads) {
                    t.join();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    
    }
    
    

    方法 0,Sleep 同步法

    这个方法确实能工作,但是可能会被面试官打

    package com.mocyx.javacc.printer;
    
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * sleep and sleep
     *
     * @author Administrator
     */
    public class SleepPrinter implements Printer {
    
        static class Worker implements Runnable {
            private String printStr;
            private int sleepGap;
            private int delay;
            private int count;
    
            public Worker(String printStr,  int delay, int sleepGap, int count) {
                this.printStr = printStr;
                this.sleepGap = sleepGap;
                this.delay = delay;
                this.count = count;
            }
    
            @Override
            public void run() {
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < count; i++) {
                    try {
                        Thread.sleep(sleepGap);
                        System.out.print(printStr);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    
    
        @Override
        public void print() {
            List<Thread> threads = new ArrayList<>();
            threads.add(new Thread(new Worker("A", 00, 30, 10)));
            threads.add(new Thread(new Worker("B", 10, 30, 10)));
            threads.add(new Thread(new Worker("C", 20, 30, 10)));
            for (Thread t : threads) {
                t.start();
            }
            try {
                for (Thread t : threads) {
                    t.join();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    }
    
    
    10 条回复    2020-01-09 17:07:02 +08:00
    lihongjie0209
        1
    lihongjie0209  
       2020-01-01 16:24:33 +08:00   ❤️ 1
    道理我都懂, 但是你在一个要求严格执行顺序的地方使用多线程的原因是什么? 有什么实际应用场景吗?
    HuHui
        2
    HuHui  
       2020-01-01 17:06:14 +08:00 via Android
    @lihongjie0209 面试就挺实际
    mightofcode
        3
    mightofcode  
    OP
       2020-01-01 18:24:14 +08:00
    @lihongjie0209 工作中没机会用,拿面试题练练手
    inwar
        4
    inwar  
       2020-01-01 18:58:51 +08:00 via Android
    应该把打印改成一个耗时任务,结果顺序输出,这样更实际一点
    1194129822
        5
    1194129822  
       2020-01-02 11:49:12 +08:00
    现在所有 Java 类型的赋值操作是原子性的,所以 volatile 加 yield 不香吗?这样即使是轮询也查不到相当于阻塞了,CPU 消耗很少。
    mightofcode
        6
    mightofcode  
    OP
       2020-01-02 16:37:32 +08:00
    @1194129822 为啥轮询查不到相当于阻塞了? yield 并不一定导致线程切换吧
    luxinfl
        7
    luxinfl  
       2020-01-03 14:52:53 +08:00
    认真的说,我一个都不会
    luxinfl
        8
    luxinfl  
       2020-01-03 16:19:28 +08:00
    while (true){
    lock.lock();
    if(in % 3 != index){
    lock.unlock();
    continue;
    }
    System.out.print(Thread.currentThread().getName());
    in++;
    if(in >31){
    lock.unlock();
    return;
    }
    lock.unlock();
    }

    这样写会不会很 ugly
    mightofcode
        9
    mightofcode  
    OP
       2020-01-03 21:09:09 +08:00
    @luxinfl 还好,本质上跟方案 1 用 AtomicInteger 是一个策略:轮询
    kkkkkrua
        10
    kkkkkrua  
       2020-01-09 17:07:02 +08:00
    用 condition,优雅点
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2366 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 02:04 · PVG 10:04 · LAX 18:04 · JFK 21:04
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.