• Day23_8 Java学习之多线程安全、死锁及状态


    目录

    一、多线程安全

    线程安全的概述

    二、死锁

    死锁的概述:

    三、线程状态

    线程状态概述

    四、多线程等待唤醒机制

    多线程等待唤醒机制的概述


    一、多线程安全

    • 线程安全的概述

    解析:

    1.多线程安全问题的出现:就是多个线程操作同一共享数据时才会出现的问题,

    举个例子:比如有一个景区系统卖100张票,一共有4个窗口同时卖票,若不时时共享数据就会出现卖重复票或者跳票的现象出现,而这些票又是真的,而卖出重复票与跳票又不合生活实际,因此我们必须解决这种问题出现。

    2.解决这种问题的方法比较合理的就是:将数据同步共享就能完美解决问题咯。

    3.在java中有3种同步数据方式分别如下:

    3.1同步代码块:

    加同步格式:

    synchronized( 需要一个任意的对象(锁))

        {

    代码块中放操作共享数据的代码。

    }

    注意:其中的锁对象可以为任意,且不唯一即可。

    3.2同步方法

    public synchronized void 方法名(参数列表){

            方法体中放操作共享数据的代码。

    }

    注意:1.如果一个方法内部,所有代码都需要被同步,那么就用同步方法; 2.同步方法的锁对象由默认给予,且为this对象。

    3.3静态同步方法

    public synchronized void 方法名(参数列表){

            方法体中放操作共享数据的代码。

    }

    注意:静态同步方法的锁对象为class对象。

    3.4锁升级:在JDK5以后java中提供了一个Lock接口,在这个接口中定义了锁的释放和获取的方法,后期程序中需要使用同步,这时可以使用Lock接口的实现类显示的完成锁的获取和释放的动作;这种方式供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有。

    使用步骤如下:

    Lock锁也称同步锁,加锁与释放锁方法化了,如下:

    public void lock():加同步锁。

    public void unlock():释放同步锁。

    使用注意:在使用Lock锁对象时,需要手动的书写代码用来:获取锁、释放锁,上述方式由于Lock是接口,不能创建对象,只能使用我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数。

    买票代码使用演示:

     任务类代码:其中四种使用方式都写入,自己切换演示即可,我们这里演示第一种使用方式

    1. package com.feisi.week7.day2;
    2. import java.util.concurrent.locks.Lock;
    3. import java.util.concurrent.locks.ReentrantLock;
    4. public class SellTicketTask implements Runnable {
    5. // 定义100张票
    6. int tickets = 100;
    7. Lock lock = new ReentrantLock();
    8. @Override
    9. public void run() {
    10. // 使用循环模拟一直买票
    11. while (true) {
    12. // 加锁,当一个线程进来之后就上锁,防止其他线程进来捣乱
    13. // 使用同步代码块
    14. synchronized ("") {
    15. // 判断是否还有余票
    16. if (tickets > 0) {
    17. try {
    18. // 为了解决线程拥挤,方便线程的调度
    19. Thread.sleep(10);
    20. } catch (InterruptedException e) {
    21. e.printStackTrace();
    22. }
    23. System.out.println(Thread.currentThread().getName() + " " + tickets);
    24. // 余票减一
    25. tickets--;
    26. }
    27. }
    28. }
    29. //sh();
    30. }
    31. // 非静态唯一对象为this 同步代码块为自己给定的对象且唯一
    32. public synchronized void sy() {
    33. // 使用循环模拟一直买票
    34. while (true) {
    35. // 加锁,当一个线程进来之后就上锁,防止其他线程进来捣乱
    36. // 使用同步代码块
    37. // 判断是否还有余票
    38. if (tickets > 0) {
    39. try {
    40. // 为了解决线程拥挤,方便线程的调度
    41. Thread.sleep(10);
    42. } catch (InterruptedException e) {
    43. e.printStackTrace();
    44. }
    45. System.out.println(Thread.currentThread().getName() + " " + tickets);
    46. // 余票减一
    47. tickets--;
    48. }
    49. }
    50. }
    51. /*// 静态为class对象也就是本身的字节码对象
    52. public static synchronized void syj() {
    53. // 使用循环模拟一直买票
    54. while (true) {
    55. // 判断是否还有余票
    56. if (tickets > 0) {
    57. try {
    58. // 为了解决线程拥挤,方便线程的调度
    59. Thread.sleep(10);
    60. } catch (InterruptedException e) {
    61. e.printStackTrace();
    62. }
    63. System.out.println(Thread.currentThread().getName() + " " + tickets);
    64. // 余票减一
    65. tickets--;
    66. }
    67. }
    68. }*/
    69. // 使用lock接口 上锁 直观一些
    70. public void sh() {
    71. // 使用循环模拟一直买票
    72. while (true) {
    73. // 加锁,当一个线程进来之后就上锁,防止其他线程进来捣乱
    74. // 使用同步代码锁 上锁
    75. lock.lock();
    76. // 判断是否还有余票
    77. if (tickets > 0) {
    78. try {
    79. // 为了解决线程拥挤,方便线程的调度
    80. Thread.sleep(10);
    81. } catch (InterruptedException e) {
    82. e.printStackTrace();
    83. }
    84. System.out.println(Thread.currentThread().getName() + " " + tickets);
    85. // 余票减一
    86. tickets--;
    87. }
    88. // 解锁
    89. lock.unlock();
    90. }
    91. }
    92. }

    测试类:

    1. package com.feisi.week7.day2;
    2. public class Test {
    3. public static void main(String[] args) {
    4. // 创建线程任务类对象
    5. SellTicketTask sellTicketTask = new SellTicketTask();
    6. // 创建线程类对象,开启线程
    7. Thread thread = new Thread(sellTicketTask, "线程1");
    8. Thread thread1 = new Thread(sellTicketTask, "线程2");
    9. Thread thread2 = new Thread(sellTicketTask, "线程3");
    10. Thread thread3 = new Thread(sellTicketTask, "线程4");
    11. // 开始线程
    12. thread.start();
    13. thread1.start();
    14. thread2.start();
    15. thread3.start();
    16. }
    17. }

     运行结果:这里展示一部分结果,可见上述多线程安全现象依然解决。

     

    二、死锁

    • 死锁的概述:

    解析:

    1.死锁是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

    2.死锁的现象的出现必须集齐以下几点:

    2.1有多把锁

    2.2有多个线程

    2.3有同步代码块嵌套

    3.简单的来说就是,举个例子:两个人为了打开一个有两把锁的箱子拿到里面的东西,东西只有一份,缺一把都打不开拿,然而A拿到了第一把钥匙,B拿到了第二把钥匙,两个人都想拿到那一份东西,所以你的钥匙不给我,我的也不给你,就会造成所谓的死锁互斥现象谁也拿不到拿一份东西,在程序中则体现为导致程序卡主,无法继续运行。

    特别注意:1.解决死锁现象只能修改源代码才能解决,此外别无它法。2. 可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)

    现象出现演示:

    1. package com.feisi.week7.day2;
    2. public class DeadLock implements Runnable{
    3. // 定义锁
    4. Object lock_a = new Object();
    5. Object lock_b = new Object();
    6. boolean flag = true;
    7. @Override
    8. public void run() {
    9. // 循环
    10. while (true) {
    11. if (flag){
    12. // 先做a 再做b
    13. synchronized (lock_a){
    14. System.out.println(Thread.currentThread().getName()+"在做A事情");
    15. synchronized (lock_b){
    16. System.out.println(Thread.currentThread().getName()+"在做B事情");
    17. }
    18. }
    19. }else {
    20. // 先做b 再做a
    21. synchronized (lock_b){
    22. System.out.println(Thread.currentThread().getName()+"在做B事情");
    23. synchronized (lock_a){
    24. System.out.println(Thread.currentThread().getName()+"在做A事情");
    25. }
    26. }
    27. }
    28. }
    29. }
    30. }

    测试类:

    1. package com.feisi.week7.day2;
    2. public class Test {
    3. public static void main(String[] args) {
    4. // 创建任务类对象 死锁现象
    5. DeadLock deadLock = new DeadLock();
    6. Thread thread = new Thread(deadLock,"线程1");
    7. Thread thread1 = new Thread(deadLock,"线程2");
    8. // 启动线程
    9. thread.start();
    10. // 要让主线程休眠一会
    11. try {
    12. Thread.sleep(1);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. deadLock.flag = false;
    17. thread1.start();
    18. }
    19. }

    运行结果:由下图可见就造成了死锁现象。 

    三、线程状态

    • 线程状态概述

    解析:

    1.线程生命周期:就是线程从生到死。

    2.当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中java.lang.Thread.State这个枚举中给出了六种线程状态:如下表所示:

    注意:关于CPU执行线程有两个概念需要了解下:

    CPU的执行资格:在CPU执行的队列中等待。还没有被CPU执行。

    CPU的执行权:当前持有CPU资源,正在被执行。

    线程状态导致状态发生条件
    NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。
    Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典叫法)
    Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
    Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
    Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
    Terminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

    结合下图理解:

    特别注意:其中我们需要重点理解从可运行run到非运行状态之间切换即可。 

    四、多线程等待唤醒机制

    • 多线程等待唤醒机制的概述

    解析:

    1.在使用多线程的时候可规划为两种情况:一是所有的线程做同样的任务,二是多线程中做着不同的任务。

    2.在做不同任务时,在程序中,最常见的一种模型,就是生产者和消费者模型。生产者线程和消费者线程之间需要进行通信,我们可以使用等待唤醒机制来实现生产者线程和消费者线程之间的通信。

    1.唤醒机制所涉及的方法

    Object类的方法 :

    wait()        :让当前线程进入等待状态
    notify()    :唤醒一个正在等待的线程,唤醒是随机的
    void notifyAll() 唤醒在此对象监视器上等待的所有线程。 

    注意事项: 1.必须要使用锁对象来调用的。2.由于锁对象可以是任意类型的对象,这些方法放在Object类中最好,因为Object类是所有类超类。3.两个方法必须要在同步里面调用,因为在同步里面才有锁对象。

    特别注意:1.如果一个线程执行了wait()方法,那么当前线程进入等待状态,并且会释放锁对象,下次即使被唤醒必须获取到锁对象才可以执行。 2.且两个线程必须使用同锁对象。3.且在调用wait(),notify()和notifyAll()的线程在调用这些方法前必须"拥有"对象的锁,否则将会报错如下异常,因此为了解决如下异常将调用上述3种方法时,应使用同步代码块让其线程获取当前锁再进行操作。

     

    案例演示:包子铺卖包子案例

    包子类:

    1. package com.feisi.week7.day2;
    2. public class Baozi {
    3. String pi;
    4. String xing;
    5. boolean flag = true;
    6. public Baozi(String pi, String xing, boolean flag) {
    7. this.pi = pi;
    8. this.xing = xing;
    9. this.flag = flag;
    10. }
    11. }

    生产者类:

    1. package com.feisi.week7.day2;
    2. public class Zuo implements Runnable{
    3. Baozi baozi;
    4. public Zuo(Baozi baozi) {
    5. this.baozi = baozi;
    6. }
    7. @Override
    8. public void run() {
    9. if (baozi.flag){
    10. // 给予当前线程锁对象
    11. synchronized ("锁"){
    12. try {
    13. "锁".wait();
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. }
    18. }
    19. // 给予当前线程锁对象
    20. synchronized ("锁"){
    21. baozi.flag = true;
    22. System.out.println("我生产了"+baozi.pi+"皮"+baozi.xing+"馅的包子");
    23. // 唤醒消费者 吃包子
    24. "锁".notify();
    25. }
    26. }
    27. }

    消费者类:

    1. package com.feisi.week7.day2;
    2. public class Chi implements Runnable{
    3. Baozi baozi;
    4. public Chi(Baozi baozi) {
    5. this.baozi = baozi;
    6. }
    7. @Override
    8. public void run() {
    9. // 若没有包子则消费沉睡
    10. if (baozi.flag==false){
    11. // 给予当前线程锁对象
    12. synchronized ("锁"){
    13. try {
    14. "锁".wait();
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. }
    20. // 给予当前线程锁对象
    21. synchronized ("锁"){
    22. // 吃完包子把生产者叫醒
    23. baozi.flag = false;
    24. System.out.println("我吃了了"+baozi.pi+"皮"+baozi.xing+"馅的包子");
    25. "锁".notify();
    26. }
    27. }
    28. }

    测试类:

    1. package com.feisi.week7.day2;
    2. public class Test2 {
    3. public static void main(String[] args) {
    4. // 创建任务线程对象
    5. Baozi baozi = new Baozi("白面","牛肉",true);
    6. Chi chi = new Chi(baozi);
    7. Zuo zuo = new Zuo(baozi);
    8. // 创建线程
    9. Thread thread = new Thread(chi);
    10. Thread thread1 = new Thread(zuo);
    11. // 开启线程
    12. thread.start();
    13. thread1.start();
    14. }
    15. }

    运行结果:由于我没有加循环就只有两个输出结果,想多输出的小伙伴再代码中加上循环即可,且解决了上述异常问题。

     

  • 相关阅读:
    【数据结构】关于字典之类的东西
    LeetCode热题(12.买卖股票的最佳时机)
    MySQL
    Helm实战案例二:在Kubernetes(k8s)上使用helm安装部署日志管理系统EFK
    ExtJS 组件查询(Component Query)
    阿里云物联网应用层开发:第二部分,云产品流转
    Java线程任务 创建一个单独的任务线程并提交到线程池中执行 案例
    vue3中a-table使用sortablejs设置了列拖动时,固定列处理
    07-类文件结构
    正则表达式相关概念及不可见高度页面的获取
  • 原文地址:https://blog.csdn.net/weixin_43717536/article/details/126486099