• 线程间的调度顺序


    目录

    ♫join和sleep

    ♫wait

    ♫notify和notifyAll


    我们知道线程是抢占式执行,随机调度的,而这也是诱发线程安全的根本原因。我们虽然无法指定线程之间的调度顺序,但是可以通过JVM提供的一些API让某个线程阻塞,主动放弃CPU,给其他线程让路,从而间接调整线程间的调度顺序。

    ♫join和sleep

    我们已经知道通过join可以让一个线程等待令另一个线程执行完毕/一定时间,sleep可以让线程阻塞指定时间:

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t1 = new Thread(()->{
    4. try {
    5. //让t1线程休眠100毫秒
    6. Thread.sleep(100);
    7. } catch (InterruptedException e) {
    8. throw new RuntimeException(e);
    9. }
    10. System.out.println("t1线程执行完毕");
    11. });
    12. t1.start();
    13. //主线程等待t1执行完毕(超过500毫秒不再等待)
    14. t1.join(500);
    15. System.out.println("主线程执行完毕");
    16. }
    17. }

    运行结果:

     但使用joinsleep无法精确控制线程执行到哪行代码再进入阻塞状态,使用waitnotify/notify就可以更精确地控制线程进入阻塞状态的时间。

    wait

    wait可以让一个线程暂停执行,等待另一个线程调用notify或notifyAll唤醒它。wait的执行顺序是先释放锁→阻塞等待→收到通知后重写尝试获取锁调,如果没有锁就使用wait运行时会报错:

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object o1 = new Object();
    4. o1.wait();
    5. }
    6. }

    运行结果:

    非法的监视器(synchronized)状态异常,故wait得先获取锁后使用:

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object o1 = new Object();
    4. synchronized (o1) {
    5. System.out.println("wait前");
    6. o1.wait();
    7. System.out.println("wait后");
    8. }
    9. }
    10. }

    运行结果:

    此时,wait就进入阻塞状态,等待notify或notifyAll唤醒它。

    注:wait释放锁,进入阻塞状态后,其它线程是可以获取到o1对象的锁的

    使用wait时还可以设定等待通知的时间,避免死等:

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object o1 = new Object();
    4. synchronized (o1) {
    5. System.out.println("wait前");
    6. //500毫秒后仍没有通知则重新尝试获取锁
    7. o1.wait(500);
    8. System.out.println("wait后");
    9. }
    10. }
    11. }

    运行结果:

    此时,主线程只会等待通知500毫秒,500毫秒后不需要notify或notifyAll就可以重新尝试获取锁。

    注:虽然wait(500)与sleep(500)很像,但wait(500)通过notify或notifyAll唤醒不会抛出异常,属于正常的代码,而sleep(500)虽然能通过interrupt唤醒,但是却是以异常的形式唤醒的,属于出问题的代码。

    ♫notify和notifyAll

    notify用来唤醒wait等待的线程,使用notify同样得先获取锁,且使用notify的对象需要和使用wait的对象一致:

    1. public class Test {
    2. public static void main(String[] args) {
    3. Object o1 = new Object();
    4. Thread t1 = new Thread(()->{
    5. synchronized (o1) {
    6. System.out.println("wait前");
    7. try {
    8. o1.wait();
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. System.out.println("wait后");
    13. }
    14. });
    15. Thread t2 = new Thread(()->{
    16. synchronized (o1) {
    17. System.out.println("notify前");
    18. o1.notify();
    19. System.out.println("notify后");
    20. }
    21. });
    22. t1.start();
    23. t2.start();
    24. }
    25. }

    运行结果:

    此处,先执行t1线程,t1掉用wait方法阻塞后执行t2,t2调用notify方法后唤醒t1线程。但由于线程的随机调度,并不能保证t1线程比t2线程先执行,如果是t2线程先执行的话,notify先执行,那么notify相当于是无效通知,等到t1线程调用wait方法后就没有对应的notify唤醒了。

    注:在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

    若有多个线程在等待o1对象,o1.notify则是随机唤醒一个等待的线程:

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object o1 = new Object();
    4. Thread t1 = new Thread(()->{
    5. synchronized (o1) {
    6. try {
    7. System.out.println("t1 wait前");
    8. o1.wait();
    9. System.out.println("t1 wait后");
    10. } catch (InterruptedException e) {
    11. throw new RuntimeException(e);
    12. }
    13. }
    14. });
    15. Thread t2 = new Thread(()->{
    16. synchronized (o1) {
    17. try {
    18. System.out.println("t2 wait前");
    19. o1.wait();
    20. System.out.println("t2 wait后");
    21. } catch (InterruptedException e) {
    22. throw new RuntimeException(e);
    23. }
    24. }
    25. });
    26. t1.start();
    27. t2.start();
    28. //由于线程进入wait的等待状态会释放锁,故t1和t2都会一起进入wait的等待状态
    29. //让t1和t2都进入wait的等待状态
    30. Thread.sleep(100);
    31. synchronized (o1) {
    32. o1.notify();
    33. }
    34. }
    35. }

    运行结果:

    使用o1.notifyAll则是唤醒所有等待的线程:

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object o1 = new Object();
    4. Thread t1 = new Thread(()->{
    5. synchronized (o1) {
    6. try {
    7. System.out.println("t1 wait前");
    8. o1.wait();
    9. System.out.println("t1 wait后");
    10. } catch (InterruptedException e) {
    11. throw new RuntimeException(e);
    12. }
    13. }
    14. });
    15. Thread t2 = new Thread(()->{
    16. synchronized (o1) {
    17. try {
    18. System.out.println("t2 wait前");
    19. o1.wait();
    20. System.out.println("t2 wait后");
    21. } catch (InterruptedException e) {
    22. throw new RuntimeException(e);
    23. }
    24. }
    25. });
    26. t1.start();
    27. t2.start();
    28. //由于线程进入wait的等待状态会释放锁,故t1和t2都会一起进入wait的等待状态
    29. //让t1和t2都进入wait的等待状态
    30. Thread.sleep(100);
    31. synchronized (o1) {
    32. o1.notifyAll();
    33. }
    34. }
    35. }

    运行结果:

    调用interrupt方法也可以导致 wait 抛出 InterruptedException 异常而终止等待:

    1. public class Test {
    2. public static void main(String[] args) {
    3. Object o1 = new Object();
    4. Thread t1 = new Thread(()->{
    5. synchronized (o1) {
    6. try {
    7. System.out.println("wait前");
    8. o1.wait();
    9. System.out.println("wait后");
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. });
    15. t1.start();
    16. t1.interrupt();
    17. }
    18. }

    运行结果:

    ♫例子 

     下面我们通过使用wait和notify实现三个线程顺序打印ABC(每个线程打印一个字母):

    1. public class Test {
    2. public static void main(String[] args) throws InterruptedException {
    3. Object o1 = new Object();
    4. Object o2 = new Object();
    5. Thread t1 = new Thread(()->{
    6. System.out.println("A");
    7. synchronized (o1) {
    8. o1.notify();
    9. }
    10. });
    11. Thread t2 = new Thread(()->{
    12. synchronized (o1) {
    13. try {
    14. o1.wait();
    15. } catch (InterruptedException e) {
    16. throw new RuntimeException(e);
    17. }
    18. }
    19. System.out.println("B");
    20. synchronized (o2) {
    21. o2.notify();
    22. }
    23. });
    24. Thread t3 = new Thread(()->{
    25. synchronized (o2) {
    26. try {
    27. o2.wait();
    28. } catch (InterruptedException e) {
    29. throw new RuntimeException(e);
    30. }
    31. }
    32. System.out.println("C");
    33. });
    34. t2.start();
    35. t3.start();
    36. Thread.sleep(100);
    37. t1.start();
    38. }
    39. }

    运行结果:

    o1对象确保t1线程打印A在T1线程打印B之前,o2对象确保t2线程打印B在t3线程打印C之前,sleep(100)是为了尽可能让线程先wait后notify,以避免无效通知最终导致程序僵住了。

  • 相关阅读:
    公链常用的共识算法
    WPF实现一个表格数据从cs获取动态渲染
    高可用k8s集群(k8s-1.29.2)
    常用压缩解压缩命令
    【驯服野生verilog-mode全记录】day2 —— 模块的例化
    如何在 uniapp 里面使用 pinia 数据持久化 (pinia-plugin-persistedstate)
    Eclipse常用设置
    记录一次Linux挂载NAS共享的SMB文件系统经历
    python+vue+elementui特色旅游管理系统django593
    河南灌溉排涝设计丙级升乙级:人员配备结构与升级难点
  • 原文地址:https://blog.csdn.net/qq_61872165/article/details/134035672