Java线程通信及线程虚假唤醒知识总结

时间:2022-08-26 19:27:30

线程通信

线程在内部运行时,线程调度具有一定的透明性,程序通常无法控制线程的轮换执行。但Java本身提供了一些机制来保证线程协调运行。

假设目前系统中有两个线程,分别代表存款和取钱。当钱存进去,立马就取出来挪入指定账户。这涉及到线程间的协作,使用到Object类提供的wait()、notify()、notifyAll()三个方法,其不属于Thread类,而属于Object,而这三个方法必须由监视器对象来调用:

  • synchronized修饰的方法,因为该类的默认实例(this)就是同步监视器,因此可以在同步方法中直接调用
  • synchronized修饰的同步代码块,同步监视器是synchronized括号里的对象,因此必须使用该对象来调用

三个方法解释如下:

  • wait():当前线程等待,释放当前对象锁,让出CPU,直到其他线程使用notify或者notifyAll唤醒该线程
  • notify():唤醒在此同步监视器上等待的单个线程,若存在多个线程,则随机唤醒一个。执行了notify不会马上释放锁,只有完全退出synchronized代码块或者中途遇到wait,呈wait状态的线程才可以去争取该对象锁
  • notifyAll():唤醒在此同步监视器上的所有线程,同上。

现在用两个同步方法分别代表存钱取钱

  • 当余额为0时,进入存钱流程,执行存钱操作后,唤醒取钱线程
  • 当余额为0时,进入取钱流程,发现num==0,进入阻塞状态,等待被唤醒
  1. /**
  2. * 存一块钱
  3. *
  4. * @throws InterruptedException
  5. */
  6. public synchronized void increase() throws InterruptedException {
  7. // 当余额为1,说明已经存过钱,等待取钱。存钱方法阻塞
  8. if (num == 1) {
  9. this.wait();
  10. }
  11. // 执行存钱操作
  12. num++;
  13. System.out.println(Thread.currentThread().getName() + ":num=" + num);
  14. // 唤醒其他线程
  15. this.notifyAll();
  16. }
  17.  
  18. /**
  19. * 取一块钱
  20. *
  21. * @throws InterruptedException
  22. */
  23. public synchronized void decrease() throws InterruptedException {
  24. // 当余额为0,说明已经取过钱,等待存钱。取钱方法阻塞
  25. if (num == 0) {
  26. this.wait();
  27. }
  28. num--;
  29. System.out.println(Thread.currentThread().getName() + ":num=" + num);
  30. this.notifyAll();
  31. }

调用方法:

  1. private int num = 0;
  2.  
  3. public static void main(String[] args) {
  4. Test test = new Test();
  5.  
  6. new Thread(() -> {
  7. for (int i = 0; i < 10; i++) {
  8. try {
  9. test.increase();
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }, "存钱").start();
  15.  
  16. new Thread(() -> {
  17. for (int i = 0; i < 10; i++) {
  18. try {
  19. test.decrease();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }, "取钱").start();
  25. }

结果没有什么问题

Java线程通信及线程虚假唤醒知识总结

线程虚假唤醒

上述线程通信看起来似乎没有什么问题,但若此时将存钱和取钱的人数各增加1,再看运行结果

  1. private int num = 0;
  2.  
  3. public static void main(String[] args) {
  4. Test test = new Test();
  5.  
  6. new Thread(() -> {
  7. for (int i = 0; i < 10; i++) {
  8. try {
  9. test.increase();
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }, "存钱1").start();
  15.  
  16. new Thread(() -> {
  17. for (int i = 0; i < 10; i++) {
  18. try {
  19. test.decrease();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. }, "取钱1").start();
  25.  
  26. new Thread(() -> {
  27. for (int i = 0; i < 10; i++) {
  28. try {
  29. test.increase();
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }, "存钱2").start();
  35.  
  36. new Thread(() -> {
  37. for (int i = 0; i < 10; i++) {
  38. try {
  39. test.decrease();
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }, "取钱2").start();
  45. }

产生的结果已经不是最初的只有0和1

Java线程通信及线程虚假唤醒知识总结

造成这个结果的原因就是线程间的虚假唤醒

由于目前分别有多个取款和存款线程。假设其中一个存款线程执行完毕,并使用wait释放同步监视器锁定,那其余多个取款线程将同时被唤醒,此时余额为1,如果有10个同时取钱,那余额会变为-9,造成结果错误。

因此,每次线程从wait中被唤醒,都必须再次测试是否符合唤醒条件,如果不符合那就继续等待。

由于多个线程被同时唤醒,在if(xxxx){wait();}处 if判断只会执行一次,当下一个被唤醒的线程过来时,由于if已经判断过,则直接从wait后面的语句继续执行,因此将if换成while可解决该问题,下次被唤醒的线程过来,while重新判断一下,发现上一个被唤醒的线程已经拿到锁,因此这个被虚假唤醒的线程将继续等待锁。

  1. /**
  2. * 存一块钱
  3. *
  4. * @throws InterruptedException
  5. */
  6. public synchronized void increase() throws InterruptedException {
  7. while (num == 1) {// 防止每次进来的唤醒线程只判断一次造成虚假唤醒,替换成while
  8. this.wait();
  9. }
  10. num++;
  11. System.out.println(Thread.currentThread().getName() + ":num=" + num);
  12. this.notifyAll();
  13. }
  14.  
  15. /**
  16. * 取一块钱
  17. *
  18. * @throws InterruptedException
  19. */
  20. public synchronized void decrease() throws InterruptedException {
  21. while (num == 0) {
  22. this.wait();
  23. }
  24. num--;
  25. System.out.println(Thread.currentThread().getName() + ":num=" + num);
  26. this.notifyAll();
  27. }

再次运行,结果正常:

Java线程通信及线程虚假唤醒知识总结

到此这篇关于Java线程通信及线程虚假唤醒知识总结的文章就介绍到这了,更多相关Java线程通信及线程虚假唤醒内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/qq_26012495/article/details/117908249