关于java:手撕面试题多个线程顺序执行问题

23次阅读

共计 8967 个字符,预计需要花费 23 分钟才能阅读完成。

大家在换工作面试中,除了一些惯例算法题,还会遇到各种须要手写的题目,所以打算总结进去,给大家个参考。

第一篇打算总结下阿里最喜爱问的多个线程程序打印问题,我遇到的是机试,间接写出运行。同类型的题目有很多,比方

  1. 三个线程别离打印 A,B,C,要求这三个线程一起运行,打印 n 次,输入形如“ABCABCABC….”的字符串
  2. 两个线程交替打印 0~100 的奇偶数
  3. 通过 N 个线程程序循环打印从 0 至 100
  4. 多线程按顺序调用,A->B->C,AA 打印 5 次,BB 打印 10 次,CC 打印 15 次,反复 10 次
  5. 用两个线程,一个输入字母,一个输入数字,交替输入 1A2B3C4D…26Z

其实这类题目考查的都是 线程间的通信问题,基于这类题目,做一个整顿,不便日后手撕面试官,文化的打工人,手撕面试题。

应用 Lock

咱们以第一题为例:三个线程别离打印 A,B,C,要求这三个线程一起运行,打印 n 次,输入形如“ABCABCABC….”的字符串。

思路:应用一个取模的判断逻辑 C%M ==N,题为 3 个线程,所以能够按取模后果编号:0、1、2,他们与 3 取模后果仍为自身,则执行打印逻辑。

public class PrintABCUsingLock {

    private int times; // 管制打印次数
    private int state;   // 以后状态值:保障三个线程之间交替打印
    private Lock lock = new ReentrantLock();

    public PrintABCUsingLock(int times) {this.times = times;}

    private void printLetter(String name, int targetNum) {for (int i = 0; i < times;) {lock.lock();
            if (state % 3 == targetNum) {
                state++;
                i++;
                System.out.print(name);
            }
            lock.unlock();}
    }

    public static void main(String[] args) {PrintABCUsingLock loopThread = new PrintABCUsingLock(1);

        new Thread(() -> {loopThread.printLetter("B", 1);
        }, "B").start();
        
        new Thread(() -> {loopThread.printLetter("A", 0);
        }, "A").start();
        
        new Thread(() -> {loopThread.printLetter("C", 2);
        }, "C").start();}
}

main 办法启动后,3 个线程会抢锁,然而 state 的初始值为 0,所以第一次执行 if 语句的内容只能是 线程 A,而后还在 for 循环之内,此时 state = 1,只有 线程 B 才满足 1% 3 == 1,所以第二个执行的是 B,同理只有 线程 C 才满足 2% 3 == 2,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 for (int i = 0; i < times;i++) 这样。

应用 wait/notify

其实遇到这类型题目,好多同学可能会先想到的就是 join(),或者 wati/notify 这样的思路。算是比拟传统且万能的解决方案。也有些面试官会要求不能应用这种形式。

思路:还是以第一题为例,咱们用对象监视器来实现,通过 waitnotify() 办法来实现期待、告诉的逻辑,A 执行后,唤醒 B,B 执行后唤醒 C,C 执行后再唤醒 A,这样循环的期待、唤醒来达到目标。

public class PrintABCUsingWaitNotify {

    private int state;
    private int times;
    private static final Object LOCK = new Object();

    public PrintABCUsingWaitNotify(int times) {this.times = times;}

    public static void main(String[] args) {PrintABCUsingWaitNotify printABC = new PrintABCUsingWaitNotify(10);
        new Thread(() -> {printABC.printLetter("A", 0);
        }, "A").start();
        new Thread(() -> {printABC.printLetter("B", 1);
        }, "B").start();
        new Thread(() -> {printABC.printLetter("C", 2);
        }, "C").start();}

    private void printLetter(String name, int targetState) {for (int i = 0; i < times; i++) {synchronized (LOCK) {while (state % 3 != targetState) {
                    try {LOCK.wait();
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }
                state++;
                System.out.print(name);
                LOCK.notifyAll();}
        }
    }
}

同样的思路,来解决下第 2 题:两个线程交替打印奇数和偶数

应用对象监视器实现,两个线程 A、B 竞争同一把锁,只有其中一个线程获取锁胜利,就打印 ++i,并告诉另一线程从期待汇合中开释,而后本身线程退出期待汇合并开释锁即可。

public class OddEvenPrinter {private Object monitor = new Object();
    private final int limit;
    private volatile int count;

    OddEvenPrinter(int initCount, int times) {
        this.count = initCount;
        this.limit = times;
    }

    public static void main(String[] args) {OddEvenPrinter printer = new OddEvenPrinter(0, 10);
        new Thread(printer::print, "odd").start();
        new Thread(printer::print, "even").start();}

    private void print() {synchronized (monitor) {while (count < limit) {
                try {System.out.println(String.format("线程 [%s] 打印数字:%d", Thread.currentThread().getName(), ++count));
                    monitor.notifyAll();
                    monitor.wait();} catch (InterruptedException e) {e.printStackTrace();
                }
            }
            // 避免有子线程被阻塞未被唤醒,导致主线程不退出
            monitor.notifyAll();}
    }
}

同样的思路,来解决下第 5 题:用两个线程,一个输入字母,一个输入数字,交替输入 1A2B3C4D…26Z

public class NumAndLetterPrinter {
    private static char c = 'A';
    private static int i = 0;
    static final Object lock = new Object();

    public static void main(String[] args) {new Thread(() -> printer(), "numThread").start();
        new Thread(() -> printer(), "letterThread").start();}

    private static void printer() {synchronized (lock) {for (int i = 0; i < 26; i++) {if (Thread.currentThread().getName() == "numThread") {
                    // 打印数字 1 -26
                    System.out.print((i + 1));
                    // 唤醒其余在期待的线程
                    lock.notifyAll();
                    try {
                        // 让以后线程开释锁资源,进入 wait 状态
                        lock.wait();} catch (InterruptedException e) {e.printStackTrace();
                    }
                } else if (Thread.currentThread().getName() == "letterThread") {
                    // 打印字母 A -Z
                    System.out.print((char) ('A' + i));
                    // 唤醒其余在期待的线程
                    lock.notifyAll();
                    try {
                        // 让以后线程开释锁资源,进入 wait 状态
                        lock.wait();} catch (InterruptedException e) {e.printStackTrace();
                    }
                }
            }
            lock.notifyAll();}
    }
}

应用 Lock/Condition

还是以第一题为例,应用 Condition 来实现,其实和 wait/notify 的思路一样。

Condition 中的 await() 办法相当于 Object 的 wait() 办法,Condition 中的 signal() 办法相当于 Object 的 notify() 办法,Condition 中的 signalAll() 相当于 Object 的 notifyAll() 办法。

不同的是,Object 中的 wait(),notify(),notifyAll()办法是和 "同步锁"(synchronized 关键字) 捆绑应用的;而 Condition 是须要与 "互斥锁"/"共享锁" 捆绑应用的。

public class PrintABCUsingLockCondition {

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

    public PrintABCUsingLockCondition(int times) {this.times = times;}

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

    private void printLetter(String name, int targetState, Condition current, Condition next) {for (int i = 0; i < times;) {lock.lock();
            try {while (state % 3 != targetState) {current.await();
                }
                state++;
                i++;
                System.out.print(name);
                next.signal();} catch (Exception e) {e.printStackTrace();
            } finally {lock.unlock();
            }
        }
    }
}

应用 Lock 锁的多个 Condition 能够实现精准唤醒,所以碰到那种多个线程交替打印不同次数的题就比拟容易想到,比方解决第四题:多线程按顺序调用,A->B->C,AA 打印 5 次,BB 打印 10 次,CC 打印 15 次,反复 10 次

代码就不贴了,思路雷同。

以上几种形式,其实都会存在一个锁的争夺过程,如果抢锁的的线程数量足够大,就会呈现很多线程抢到了锁但不该本人执行,而后就又解锁或 wait() 这种操作,这样其实是有些浪费资源的。

应用 Semaphore

在信号量上咱们定义两种操作:信号量次要用于两个目标,一个是用于多个共享资源的互斥应用,另一个用于并发线程数的管制。

  1. acquire(获取)当一个线程调用 acquire 操作时,它要么通过胜利获取信号量(信号量减 1),要么始终等上来,直到有线程开释信号量,或超时。
  2. release(开释)实际上会将信号量的值加 1,而后唤醒期待的线程。

先看下如何解决第一题:三个线程循环打印 A,B,C

public class PrintABCUsingSemaphore {
    private int times;
    private static Semaphore semaphoreA = new Semaphore(1); // 只有 A 初始信号量为 1, 第一次获取到的只能是 A
    private static Semaphore semaphoreB = new Semaphore(0);
    private static Semaphore semaphoreC = new Semaphore(0);

    public PrintABCUsingSemaphore(int times) {this.times = times;}

    public static void main(String[] args) {PrintABCUsingSemaphore printer = new PrintABCUsingSemaphore(1);
        new Thread(() -> {printer.print("A", semaphoreA, semaphoreB);
        }, "A").start();

        new Thread(() -> {printer.print("B", semaphoreB, semaphoreC);
        }, "B").start();

        new Thread(() -> {printer.print("C", semaphoreC, semaphoreA);
        }, "C").start();}

    private void print(String name, Semaphore current, Semaphore next) {for (int i = 0; i < times; i++) {
            try {System.out.println("111" + Thread.currentThread().getName());
                current.acquire();  // A 获取信号执行,A 信号量减 1, 当 A 为 0 时将无奈持续取得该信号量
                System.out.print(name);
                next.release();    // B 开释信号,B 信号量加 1(初始为 0),此时能够获取 B 信号量
                System.out.println("222" + Thread.currentThread().getName());
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
}

如果题目中是多个线程循环打印的话,个别应用信号量解决是效率较高的计划,上一个线程持有下一个线程的信号量,通过一个信号量数组将全副关联起来,这种形式不会存在浪费资源的状况。

接着用信号量的形式解决下第三题:通过 N 个线程程序循环打印从 0 至 100

public class LoopPrinter {

    private final static int THREAD_COUNT = 3;
    static int result = 0;
    static int maxNum = 10;

    public static void main(String[] args) throws InterruptedException {final Semaphore[] semaphores = new Semaphore[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            // 非偏心信号量,每个信号量初始计数都为 1
            semaphores[i] = new Semaphore(1);
            if (i != THREAD_COUNT - 1) {System.out.println(i+"==="+semaphores[i].getQueueLength());
                // 获取一个许可火线程将始终阻塞, for 循环之后只有 syncObjects[2] 没有被阻塞
                semaphores[i].acquire();}
        }
        for (int i = 0; i < THREAD_COUNT; i++) {// 首次执行,上一个信号量是 syncObjects[2]
            final Semaphore lastSemphore = i == 0 ? semaphores[THREAD_COUNT - 1] : semaphores[i - 1];
            final Semaphore currentSemphore = semaphores[i];
            final int index = i;
             new Thread(() -> {
                try {while (true) {// 首次执行,让第一个 for 循环没有阻塞的 syncObjects[2] 先取得令牌阻塞了
                        lastSemphore.acquire();
                        System.out.println("thread" + index + ":" + result++);
                        if (result > maxNum) {System.exit(0);
                        }
                        // 开释以后的信号量,syncObjects[0] 信号量此时为 1,下次 for 循环中上一个信号量即为 syncObjects[0]
                        currentSemphore.release();}
                } catch (Exception e) {e.printStackTrace();
                }
            }).start();}
    }
}

应用 LockSupport

LockSupport 是 JDK 底层的基于 sun.misc.Unsafe 来实现的类,用来创立锁和其余同步工具类的根本线程阻塞原语。它的静态方法 unpark()park()能够别离实现阻塞以后线程和唤醒指定线程的成果,所以用它解决这样的问题会更容易一些。

(在 AQS 中,就是通过调用 LockSupport.park()LockSupport.unpark() 来实现线程的阻塞和唤醒的。)

public class PrintABCUsingLockSupport {

    private static Thread threadA, threadB, threadC;

    public static void main(String[] args) {threadA = new Thread(() -> {for (int i = 0; i < 10; i++) {
                // 打印以后线程名称
                System.out.print(Thread.currentThread().getName());
                // 唤醒下一个线程
                LockSupport.unpark(threadB);
                // 以后线程阻塞
                LockSupport.park();}
        }, "A");
        threadB = new Thread(() -> {for (int i = 0; i < 10; i++) {
                // 先阻塞期待被唤醒
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                // 唤醒下一个线程
                LockSupport.unpark(threadC);
            }
        }, "B");
        threadC = new Thread(() -> {for (int i = 0; i < 10; i++) {
                // 先阻塞期待被唤醒
                LockSupport.park();
                System.out.print(Thread.currentThread().getName());
                // 唤醒下一个线程
                LockSupport.unpark(threadA);
            }
        }, "C");
        threadA.start();
        threadB.start();
        threadC.start();}
}

了解了思路,解决其余问题就容易太多了。

比方,咱们再解决下第五题:用两个线程,一个输入字母,一个输入数字,交替输入 1A2B3C4D…26Z

public class NumAndLetterPrinter {

    private static Thread numThread, letterThread;

    public static void main(String[] args) {letterThread = new Thread(() -> {for (int i = 0; i < 26; i++) {System.out.print((char) ('A' + i));
                LockSupport.unpark(numThread);
                LockSupport.park();}
        }, "letterThread");

        numThread = new Thread(() -> {for (int i = 1; i <= 26; i++) {System.out.print(i);
                LockSupport.park();
                LockSupport.unpark(letterThread);
            }
        }, "numThread");
        numThread.start();
        letterThread.start();}
}

写在最初

好了,以上就是罕用的五种实现计划,多练习几次,手撕没问题。

当然,这类问题,解决形式不止是我列出的这些,还会有 join、CountDownLatch、也有放在队列里解决的,思路有很多,面试官想考查的其实只是对多线程的编程功底,其实本人练习的时候,是个很好的坚固了解 JUC 的过程。

以梦为马,越骑越傻。诗和远方,越走越慌。不忘初心是对的,但切记要登程,加油吧,程序员。

在路上的你,能够微信搜「JavaKeeper」一起前行,无套路支付 500+ 本电子书和 30+ 视频教学和源码,本文 GitHub github.com/JavaKeeper 曾经收录,服务端开发、面试必备技能兵器谱,有你想要的。

正文完
 0