• JavaEE 线程状态及线程安全


    线程状态

            线程的状态包括:new、runnable、blocked、waiting、timed_waiting、terminated这6种状态。

    (1)new状态表示安排了工作,但是还没有行动。创建了Thread对象,系统内核里面还没有状态,还没有调用start方法。

    (2)terminated状态表示系统里面的线程已经执行完毕,销毁了(相当于线程的run方法执行结束),但是Thread对象还在。

    (3)runnable状态表示就绪状态。就绪状态有两种含义,正在CPU上运行;还没有在CPU上运行,但是准备好了。

    除了上述的三种状态之外,剩下的都是阻塞状态了。

    (4)blocked状态表示阻塞状态,阻塞的原因是等待锁。后序说明。

    (5)waiting状态表示阻塞状态,阻塞的原因是调用了wait方法。后序说明。

    (6)timed_waiting状态表示阻塞状态,阻塞的原因是调用了sleep方法。

    1. public static void main(String[] args) {
    2. Thread thread = new Thread(() -> {
    3. System.out.println("thread");
    4. });
    5. //new状态,在start之前获取,线程还没有创建
    6. System.out.println(thread.getState());
    7. thread.start();
    8. //runnable状态,线程运行
    9. System.out.println(thread.getState());
    10. try {
    11. thread.join();
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. //terminated状态,在join之后获取,线程已经结束
    16. System.out.println(thread.getState());
    17. }

    想要看到阻塞状态中的timed_waiting状态,就需要让线程处于休眠(sleep状态)。

    整个过程画个图来理解。

            整个线程的执行,理想的状态时直接new => runnable => terminated 状态,但是免不了操作系统的随机调度或者调用了不同的方法,让线程进入阻塞状态。

    线程安全

    我们来看这样的一段代码:使用两个线程,对同一个变量进行累加。

    1. public class demo3 {
    2. int sum = 0;
    3. public static void main(String[] args) throws InterruptedException {
    4. demo3 demo = new demo3();
    5. Thread thread1 = new Thread(() -> {
    6. for (int i = 0; i < 5000; i++) {
    7. demo.sum++;
    8. }
    9. });
    10. Thread thread2 = new Thread(() -> {
    11. for (int i = 0; i < 5000; i++) {
    12. demo.sum++;
    13. }
    14. });
    15. thread1.start();
    16. thread2.start();
    17. thread1.join();
    18. thread2.join();
    19. System.out.println(demo.sum);
    20. }
    21. }

     对sum变量累加1000次,得到的结果应该时1000。但是结果可能是:

            发现比sum的值比1000小。这就涉及到一个风险,多线程带来的安全问题。 线程安全问题,是多线程编程最重要,最困难的问题。线程安全问题的源头,是调度器的随机调度(抢占式执行)。在随机调度下,程序的执行出现了多种可能。其中某些可能导致代码出了BUG。随机调度的顺序不一样,导致的结果不一样。

    对于sum++这样的语句,一行代码对应了三个机械指令:

    (1)从内存读取数据到CPU(load);

    (2)在CPU寄存器中,完成加法运算(add);

    (3)把寄存器的数据写回到内存中(save);

            这些步骤在单线程下执行是没有问题的,但是在多线程下,经过操作系统的随机调度在加上编译器的优化,造成的结果也就不确定了。 

    造成线程不安全的原因

    (1)操作系统的随机调度/抢占式执行,是造成线程不安全的根本原因,这个咱们是改变不了的;

    (2)多个线程修改同一个变量。如果只是一个线程修改变量,是没什么问题的。如果多个线程读取同一个变量,也是没什么问题的。但是如果是多个线程修改同一个变量,就会出问题。比如上述代码的sum。所以在写代码的时候,可以通过调整程序的设计,让多个线程不要修改同一个变量;

    (3)有些修改操作,不是原子的。“原子”意味着不可拆分的最小单位。在代码中,只对应一条机器指令,比如赋值操作,加减操作等都是对应一条机器指令,而++,--等操作对应了多条指令。这个解决需要加锁操作;

    (4)内存可见性引起的线程安全问题。一个场景,比如一个线程频繁读、一个线程修改,特别容易因为内存可见性,引发问题。当一个线程频繁读,判断的时候,编译器发现这个过程没有进行任何修改,于是对于读,判断进行了优化,只变成了判断。而另一个线程突然修改了值,这个时候,这个线程没有做出反应,于是造成了bug。整个过程,内存改了,但是在优化的背景下,没有读到修改情况;

    1. static int flg = 0;
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread1 = new Thread(() -> {
    4. while (flg == 0) {
    5. //什么也不做
    6. }
    7. System.out.println("结束");
    8. });
    9. Thread thread2 = new Thread(() -> {
    10. System.out.println("改变值:");
    11. Scanner scanner = new Scanner(System.in);
    12. flg = scanner.nextInt();
    13. });
    14. thread1.start();
    15. thread2.start();
    16. thread1.join();
    17. thread2.join();
    18. }

            你会发现,输入1后,程序还是在运行,没有结束。使用vloatile关键字手动禁止编译器优化。在flg变量之前加上vloatile关键字。

     (5)指令重排序。也是操作系统进行了优化操作。就是对指令进行重新排序优化。这样的操作在单线程的时候能保证逻辑的正确性,但是在多线程下就不确定了。

  • 相关阅读:
    y45.第三章 Kubernetes从入门到精通 -- k8s中运行web服务(十八)
    计算机组成原理习题课第四章-2(唐朔飞)
    大话云原生数据库中的存算分离
    前端css样式小知识点(大杂烩)
    [附源码]计算机毕业设计ssm新能源电动汽车充电桩服务APPSpringboot程序
    SpringCloud
    vue/自定义指令
    【洛谷 B2003】输出第二个整数 题解(顺序结构+输入输出)
    pc端vue2项目使用uniapp组件
    Webmin--System and Server Status模块
  • 原文地址:https://blog.csdn.net/Naion/article/details/125995964