当前位置: 首页 > article >正文

4. 多线程(2)---线程的状态和多线程带来的风险

文章目录

  • 前言
  • 1. 线程的状态
    • 1.1. 观察线程的所有状态
    • 1.2. 通过不同线程的状态,来调试代码,观察现象
  • 2. 多线程的带来的风险---线程不安全
    • 2.1.观察线程不安全的现象
    • 2.2 线程不安全的原因
    • 2.3.线程不安全的原因


前言

上一篇博客我们学习了,线程的创建,这次我们讲解 线程的状态 和 线程不安全问题。


1. 线程的状态

1.1. 观察线程的所有状态

从操作系统的视角来看,线程的状态分为:就绪和阻塞。
Java线程也是对操作系统线程的封装,
针对状态这里,Java也进行了重新封装,一共分为下面几种状态

  • NEW:安排了工作,还未开始行动
  • RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作
  • BLOCKED:由于而造成的阻塞
  • WAITING:死等,没有超时时间的阻塞等待
  • TIMED_WAITING:有超时时间的阻塞等待
  • TERMINATED:工作完成了

我们有两种方式观察线程的状态

  1. 使用 getState() 方法
  2. 使用 jconsole 程序观察

下面我们写代码示例,来分别观察线程的状态

  • NEW:安排了工作,还未开始行动
  • TERMINATED:工作完成了
public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{

        });
        System.out.println(t.getState());
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }

在这里插入图片描述

  • TIMED_WAITING:有超时时间或者是指定时间的阻塞等待
public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState());
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

在这里插入图片描述
也可以使用 join(时间) 也会进入到 TIMED_WAITING

public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState());
        t.start();
        t.join(600000*1000);
    }
}

使用 jconsole 工具
在这里插入图片描述

  • RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作
public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
          while (true){
                /*try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }*/
            }
        });
        System.out.println(t.getState());
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

while 循环中这段指令虽然什么都没有做,但是一直进行个循环操作,在CPU上执行。

  • WAITING:死等,没有超时时间的阻塞等待
public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState());
        t.start();
        t.join();
    }
}

下图就是一个简易的不同状态下的转换。
在这里插入图片描述

1.2. 通过不同线程的状态,来调试代码,观察现象

一个程序员工作的大部分时间不是在写代码,而是在调试代码 (找 bug)。
在多次的程序中,理解线程状态,是帮助我们调试程序的关键。
比如,发现某个代码逻辑,好像卡死了 (明明调用了,没有执行/没有执行完)
检查流程:

  1. 使用jconsole 或者其他工具,查看当前的进程中所有的线程,找到对应的逻辑线程是什么
  2. 看线程的状态是什么
    看到 Timed_waiting / waiting,换衣是不是代码在某个方法上产生了阻塞,没有被即使唤醒
    看到 blocked,怀疑是不是代码中出现了死锁
    看到 Runnable,线程本身没有问题,考虑逻辑上某些条件是不是没有触发
  3. 再看看线程具体的调用栈,尤其是在阻塞的状态,现成代码阻塞在哪一行

2. 多线程的带来的风险—线程不安全

2.1.观察线程不安全的现象

下面我们来讨论一下线程不安全的情况。
举例一个例子:
使用两个线程,分别对 count进行加一操作,分别循环5000次,观察结果,预期是 10000次

public class Demo15 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0 ;i<50000;i++){
                count++;
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+count);
    }
}

这样写,会不会得到我们想要的结果呢?
在这里插入图片描述
每次结果都不一样,很难出现预期结果,每次发现都不是我们想要的结果。
为什么呢?
这个问题等会再说,
但是我们可以根据上面的代码进行修改,然后得到100000,那怎么进行修改呢?
只需要把start和join进行交换

public class Demo15 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0 ;i<50000;i++){
                count++;
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        System.out.println("count:"+count);
    }
}

结果如下:
在这里插入图片描述
这样改就可以了竟然,下面我们来讲解一下线程不安全出现的原因。

2.2 线程不安全的原因

我们先解释一下上面出现的原因。
现在就需要我们之前的知识了,站在CPU的角度来看执行指令
在这里插入图片描述
这个操作看起来是一行代码,实际上对应到 3 个CPU 指令

  1. load: 把内存中的值 (count变量) 读取到 CPU 寄存器
  2. add: 把指定寄存器中的值,进行+1操作,结果还是在这个寄存器中
  3. save:把寄存器中的值,写回到内存中

上述这三个指令执行的过程中,CPU随时可能会触发线程的调取切换(因为操作系统的调度是随机的)。
可能会发生下面的情况:
123线程切走
12切走…线程切回来,然后是3
1 线程切走… 线程切回来,然后23一起执行
1线程切走…线程切回来 2线程切走…线程切回来,然后是3线程
下面是图解:
在这里插入图片描述
我们拿出两个来看一下,count的变化
在这里插入图片描述
在这里插入图片描述
上面便是上述代码出现的问题的解释。

2.3.线程不安全的原因

  1. [根本] 操作系统对于线程的调度 是随机的。抢占式的

  2. 多个线程同时修改一个同一个变量
    上述代码t1线程和t2线程修改同一个内存空间
    如果是一个线程修改一个变量 - - - 没有问题
    如果是多个线程,不是同时修改同一个变量 - - - 没有问题
    如果是多个线程修改不同变量 - - - 没问题
    如果多个线程读取同一个变量 - - - 没问题
    取值操作 是 读操作,只有一条执行命令

  3. 修改的操作,不是原子的
    在数据库中我们在讲事务的时候,事务有四大特性:
    原子性 - - - 不可再分,一致性,持久性,隔离性。
    如果修改操作,只是对应一个CPU指令,就可以认为是原子的,CPU不会出现“一条指令执行一半”的情况。
    如果对应到多个CPU指令,那就不是原子的了。

  4. 内存可见性问题,引发的线程不安全

  5. 指令重排序,引发的线程不安全

第4条和第5条等到以后再讲。

关于为什么上面第二个代码可以成功,还有没有别的方法来实现线程安全,请听下回分析!



http://www.kler.cn/a/471615.html

相关文章:

  • STM32和国民技术(N32)单片机串口中断接收数据及数据解析
  • 【机器学习:四、多输入变量的回归问题】
  • 设计模式(1)——面向对象和面向过程,封装、继承和多态
  • Java 日期时间格式化标准
  • java 转义 反斜杠 Unexpected internal error near index 1
  • 【连续学习之LwM算法】2019年CVPR顶会论文:Learning without memorizing
  • 如何用代码提交spark任务并且获取任务权柄
  • 大数据技术(八)—— HBase数据读写流程和Api的使用
  • uniapp打包到宝塔并发布
  • 使用python将自己的程序封装成API
  • 使用Python实现医疗物联网设备:构建高效医疗监测系统
  • 快速排序进阶版(加入插入排序提高其性能)
  • 【代码随想录】刷题记录(93)-无重叠区间
  • Requests-数据解析bs4+xpath
  • UWB实操:用信号分析仪(频谱分析仪)抓取UWB频域的图像
  • 【JMeter】多接口关联
  • es 3期 第22节-Bucket特殊分桶聚合实战
  • 【往届已EI检索】第五届智慧城市工程与公共交通国际学术会议(SCEPT 2025)
  • 在 PhpStorm 中配置命令行直接运行 PHP 的步骤
  • 后端开发入门超完整速成路线(算法篇)
  • 计算机网络:无线网络
  • 矩阵和向量点乘叉乘元素乘
  • ue5 替换角色的骨骼网格体和动画蓝图
  • 计算机网络之---计算机网络的性能评估
  • Redis中的主从/Redis八股
  • 信息安全:Java自定义Jackson序列化器进行数据脱敏