使用Lock

问题

三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC….”的字符串

思路

使用一个取模的判断逻辑 C%M ==N,题为 3 个线程,所以可以按取模结果编号:0、1、2,他们与 3 取模结果仍为本身,则执行打印逻辑。

代码

  1. public class PrintABCUsingLock {
  2. private int times; // 控制打印次数
  3. private int state; // 当前状态值:保证三个线程之间交替打印
  4. private Lock lock = new ReentrantLock();
  5. public PrintABCUsingLock(int times) {
  6. this.times = times;
  7. }
  8. private void printLetter(String name, int targetNum) {
  9. for (int i = 0; i < times; ) {
  10. lock.lock();
  11. if (state % 3 == targetNum) {
  12. state++;
  13. i++;
  14. System.out.print(name);
  15. }
  16. lock.unlock();
  17. }
  18. }
  19. public static void main(String[] args) {
  20. PrintABCUsingLock loopThread = new PrintABCUsingLock(1);
  21. new Thread(() -> {
  22. loopThread.printLetter("B", 1);
  23. }, "B").start();
  24. new Thread(() -> {
  25. loopThread.printLetter("A", 0);
  26. }, "A").start();
  27. new Thread(() -> {
  28. loopThread.printLetter("C", 2);
  29. }, "C").start();
  30. }
  31. }

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

问题

三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC….”的字符串

思路

我们用对象监视器来实现,通过 wait 和 notify() 方法来实现等待、通知的逻辑,A 执行后,唤醒 B,B 执行后唤醒 C,C 执行后再唤醒 A,
这样循环的等待、唤醒来达到目的

代码

  1. public class PrintABCUsingWaitNotify {
  2. private int state;
  3. private int times;
  4. private static final Object LOCK = new Object();
  5. public PrintABCUsingWaitNotify(int times) {
  6. this.times = times;
  7. }
  8. public static void main(String[] args) {
  9. PrintABCUsingWaitNotify printABC = new PrintABCUsingWaitNotify(10);
  10. new Thread(() -> {
  11. printABC.printLetter("A", 0);
  12. }, "A").start();
  13. new Thread(() -> {
  14. printABC.printLetter("B", 1);
  15. }, "B").start();
  16. new Thread(() -> {
  17. printABC.printLetter("C", 2);
  18. }, "C").start();
  19. }
  20. private void printLetter(String name, int targetState) {
  21. for (int i = 0; i < times; i++) {
  22. synchronized (LOCK) {
  23. while (state % 3 != targetState) {
  24. try {
  25. LOCK.wait();
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. state++;
  31. System.out.print(name);
  32. LOCK.notifyAll();
  33. }
  34. }
  35. }
  36. }

问题

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

使用对象监视器实现,两个线程 A、B 竞争同一把锁,只要其中一个线程获取锁成功,就打印 ++i,并通知另一线程从等待集合中释放,

然后自身线程加入等待集合并释放锁即可

  1. public class OddEvenPrinter {
  2. private Object monitor = new Object();
  3. private final int limit;
  4. private volatile int count;
  5. OddEvenPrinter(int initCount, int times) {
  6. this.count = initCount;
  7. this.limit = times;
  8. }
  9. public static void main(String[] args) {
  10. OddEvenPrinter printer = new OddEvenPrinter(0, 10);
  11. new Thread(printer::print, "odd").start();
  12. new Thread(printer::print, "even").start();
  13. }
  14. private void print() {
  15. synchronized (monitor) {
  16. while (count < limit) {
  17. try {
  18. System.out.println(String.format("线程[%s]打印数字:%d", Thread.currentThread().getName(), ++count));
  19. monitor.notifyAll();
  20. monitor.wait();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. //防止有子线程被阻塞未被唤醒,导致主线程不退出
  26. monitor.notifyAll();
  27. }
  28. }
  29. }

问题

用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D…26Z

  1. public class NumAndLetterPrinter {
  2. private static char c = 'A';
  3. private static int i = 0;
  4. static final Object lock = new Object();
  5. public static void main(String[] args) {
  6. new Thread(() -> printer(), "numThread").start();
  7. new Thread(() -> printer(), "letterThread").start();
  8. }
  9. private static void printer() {
  10. synchronized (lock) {
  11. for (int i = 0; i < 26; i++) {
  12. if (Thread.currentThread().getName() == "numThread") {
  13. //打印数字 1-26
  14. System.out.print((i + 1));
  15. // 唤醒其他在等待的线程
  16. lock.notifyAll();
  17. try {
  18. // 让当前线程释放锁资源,进入 wait 状态
  19. lock.wait();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. } else if (Thread.currentThread().getName() == "letterThread") {
  24. // 打印字母 A-Z
  25. System.out.print((char) ('A' + i));
  26. // 唤醒其他在等待的线程
  27. lock.notifyAll();
  28. try {
  29. // 让当前线程释放锁资源,进入 wait 状态
  30. lock.wait();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. lock.notifyAll();
  37. }
  38. }
  39. }

使用 Lock/Condition

  • Condition 中的 await() 方法相当于 Object 的 wait() 方法,Condition 中的 signal() 方法相当于 Object 的 notify() 方法,Condition 中的 signalAll() 相当于 Object 的 notifyAll() 方法。
  • 不同的是,Object 中的 wait(),notify(),notifyAll()方法是和”同步锁”(synchronized 关键字)捆绑使用的;而 Condition 是需要与”互斥锁”/“共享锁”捆绑使用的。

    问题

    三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC….”的字符串

    代码

    1. public class PrintABCUsingLockCondition {
    2. private int times;
    3. private int state;
    4. private static Lock lock = new ReentrantLock();
    5. private static Condition c1 = lock.newCondition();
    6. private static Condition c2 = lock.newCondition();
    7. private static Condition c3 = lock.newCondition();
    8. public PrintABCUsingLockCondition(int times) {
    9. this.times = times;
    10. }
    11. public static void main(String[] args) {
    12. PrintABCUsingLockCondition print = new PrintABCUsingLockCondition(10);
    13. new Thread(() -> {
    14. print.printLetter("A", 0, c1, c2);
    15. }, "A").start();
    16. new Thread(() -> {
    17. print.printLetter("B", 1, c2, c3);
    18. }, "B").start();
    19. new Thread(() -> {
    20. print.printLetter("C", 2, c3, c1);
    21. }, "C").start();
    22. }
    23. private void printLetter(String name, int targetState, Condition current, Condition next) {
    24. for (int i = 0; i < times; ) {
    25. lock.lock();
    26. try {
    27. while (state % 3 != targetState) {
    28. current.await();
    29. }
    30. state++;
    31. i++;
    32. System.out.print(name);
    33. next.signal();
    34. } catch (Exception e) {
    35. e.printStackTrace();
    36. } finally {
    37. lock.unlock();
    38. }
    39. }
    40. }
    41. }

使用 Lock 锁的多个 Condition 可以实现精准唤醒,所以碰到那种多个线程交替打印不同次数的题就比较容易想到

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

使用 Semaphore

  • 在信号量上我们定义两种操作:信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
  • acquire(获取) 当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减 1),要么一直等下去,直到有线程释放信号量,或超时。
  • release(释放)实际上会将信号量的值加 1,然后唤醒等待的线程

问题

三个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC….”的字符串

代码

  1. public class PrintABCUsingSemaphore {
  2. private int times;
  3. private static Semaphore semaphoreA = new Semaphore(1); // 只有 A 初始信号量为 1,第一次获取到的只能是 A
  4. private static Semaphore semaphoreB = new Semaphore(0);
  5. private static Semaphore semaphoreC = new Semaphore(0);
  6. public PrintABCUsingSemaphore(int times) {
  7. this.times = times;
  8. }
  9. public static void main(String[] args) {
  10. PrintABCUsingSemaphore printer = new PrintABCUsingSemaphore(1);
  11. new Thread(() -> {
  12. printer.print("A", semaphoreA, semaphoreB);
  13. }, "A").start();
  14. new Thread(() -> {
  15. printer.print("B", semaphoreB, semaphoreC);
  16. }, "B").start();
  17. new Thread(() -> {
  18. printer.print("C", semaphoreC, semaphoreA);
  19. }, "C").start();
  20. }
  21. private void print(String name, Semaphore current, Semaphore next) {
  22. for (int i = 0; i < times; i++) {
  23. try {
  24. System.out.println("111" + Thread.currentThread().getName());
  25. current.acquire(); // A 获取信号执行,A 信号量减 1,当 A 为 0 时将无法继续获得该信号量
  26. System.out.print(name);
  27. next.release(); // B 释放信号,B 信号量加 1(初始为 0),此时可以获取 B 信号量
  28. System.out.println("222" + Thread.currentThread().getName());
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }
  34. }

如果题目中是多个线程循环打印的话,一般使用信号量解决是效率较高的方案,上一个线程持有下一个线程的信号量,通过一个信号量数组将全部关联起来,这种方式不会存在浪费资源的情况

问题

通过 N 个线程顺序循环打印从 0 至 100

代码

  1. public class LoopPrinter {
  2. private final static int THREAD_COUNT = 3;
  3. static int result = 0;
  4. static int maxNum = 10;
  5. public static void main(String[] args) throws InterruptedException {
  6. final Semaphore[] semaphores = new Semaphore[THREAD_COUNT];
  7. for (int i = 0; i < THREAD_COUNT; i++) {
  8. //非公平信号量,每个信号量初始计数都为 1
  9. semaphores[i] = new Semaphore(1);
  10. if (i != THREAD_COUNT - 1) {
  11. System.out.println(i+"==="+semaphores[i].getQueueLength());
  12. //获取一个许可前线程将一直阻塞, for 循环之后只有 syncObjects[2] 没有被阻塞
  13. semaphores[i].acquire();
  14. }
  15. }
  16. for (int i = 0; i < THREAD_COUNT; i++) {
  17. // 初次执行,上一个信号量是 syncObjects[2]
  18. final Semaphore lastSemphore = i == 0 ? semaphores[THREAD_COUNT - 1] : semaphores[i - 1];
  19. final Semaphore currentSemphore = semaphores[i];
  20. final int index = i;
  21. new Thread(() -> {
  22. try {
  23. while (true) {
  24. // 初次执行,让第一个 for 循环没有阻塞的 syncObjects[2] 先获得令牌阻塞了
  25. lastSemphore.acquire();
  26. System.out.println("thread" + index + ": " + result++);
  27. if (result > maxNum) {
  28. System.exit(0);
  29. }
  30. // 释放当前的信号量,syncObjects[0] 信号量此时为 1,下次 for 循环中上一个信号量即为 syncObjects[0]
  31. currentSemphore.release();
  32. }
  33. } catch (Exception e) {
  34. e.printStackTrace();
  35. }
  36. }).start();
  37. }
  38. }
  39. }

使用 LockSupport

  • LockSupport 是 JDK 底层的基于 sun.misc.Unsafe 来实现的类,用来创建锁和其他同步工具类的基本线程阻塞原语。
  • 它的静态方法unpark()和park()可以分别实现阻塞当前线程和唤醒指定线程的效果,所以用它解决这样的问题会更容易一些。
  • 在 AQS 中,就是通过调用 LockSupport.park( )和 LockSupport.unpark() 来实现线程的阻塞和唤醒的。

    问题

    按线程顺序打印

    代码

    1. public class PrintABCUsingLockSupport {
    2. private static Thread threadA, threadB, threadC;
    3. public static void main(String[] args) {
    4. threadA = new Thread(() -> {
    5. for (int i = 0; i < 10; i++) {
    6. // 打印当前线程名称
    7. System.out.print(Thread.currentThread().getName());
    8. // 唤醒下一个线程
    9. LockSupport.unpark(threadB);
    10. // 当前线程阻塞
    11. LockSupport.park();
    12. }
    13. }, "A");
    14. threadB = new Thread(() -> {
    15. for (int i = 0; i < 10; i++) {
    16. // 先阻塞等待被唤醒
    17. LockSupport.park();
    18. System.out.print(Thread.currentThread().getName());
    19. // 唤醒下一个线程
    20. LockSupport.unpark(threadC);
    21. }
    22. }, "B");
    23. threadC = new Thread(() -> {
    24. for (int i = 0; i < 10; i++) {
    25. // 先阻塞等待被唤醒
    26. LockSupport.park();
    27. System.out.print(Thread.currentThread().getName());
    28. // 唤醒下一个线程
    29. LockSupport.unpark(threadA);
    30. }
    31. }, "C");
    32. threadA.start();
    33. threadB.start();
    34. threadC.start();
    35. }
    36. }

问题

用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D…26Z

代码
  1. public class NumAndLetterPrinter {
  2. private static Thread numThread, letterThread;
  3. public static void main(String[] args) {
  4. letterThread = new Thread(() -> {
  5. for (int i = 0; i < 26; i++) {
  6. System.out.print((char) ('A' + i));
  7. LockSupport.unpark(numThread);
  8. LockSupport.park();
  9. }
  10. }, "letterThread");
  11. numThread = new Thread(() -> {
  12. for (int i = 1; i <= 26; i++) {
  13. System.out.print(i);
  14. LockSupport.park();
  15. LockSupport.unpark(letterThread);
  16. }
  17. }, "numThread");
  18. numThread.start();
  19. letterThread.start();
  20. }
  21. }