线程状态转换和线程安全

1. 线程状态

1. 什么是状态?为什么需要状态?

状态:表示线程所处的一个情况
为什么需要状态:管理线程,就需要先了解线程所处的状态

2. 线程所有状态以及线程状态转换

  1. 线程的所有状态:
    在JDK中, 线程的状态通常使用枚举的方式Thread.State 描述
    线程状态共分为:NEW,RUNNABLE,BLOCKED,TIMED_WAITING,WEITING和TERMANITED
  2. 线程状态转换(重要)
    线程状态转换图:
    在这里插入图片描述

注意事项:
1. new一个新线程,它的状态应该为 NEW 状态
2. RUNNABLE 状态中,其实又分为一个 READY 状态和 RUNNING 状态
3. 新线程只有调用了 start 方法 ,才可以进入到 READY 状态
4. 线程必须占有 CPU 时才可以运行代码,无论什么原因,一个线程现在没有CPU,都需要先进入到 READY 状态,排队等待分配 CPU,才能进入 RUNNING 状态
5. 什么时候线程会从 RUNNING 状态转换到 READY 状态?
(1). 该线程被高优先级线程抢占
(2). 该线程主动放弃 CPU(Thread.yield(), 放弃后仍保留抢 CPU 的资格)
(3).该线程的时间片耗尽
6. 若线程进入到 BOLCKED,WEITING,TIMED_WAITING 状态时,则该线程在放弃 CPU 的同时也放弃了抢 CPU 的资格,直到某个条件满足为止

由线程状态转换图可知,线程中的 isAlive()方法,可以认为是处于不是 NEW 和 TERMINITED 的状态都是活着的。

2. 线程安全(重要)

1. 什么是线程不安全?什么是线程安全?

1. 线程不安全:

public class ThreadUnsafeDemo {
    private static int v = 0;
    private static final int N = 10_000_000;

    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < N; i++) {
                v++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < N; i++) {
                v--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add a = new Add();
        Sub b = new Sub();
        a.start();
        b.start();

        a.join();
        b.join();
        // 到这时, a 和 b 都结束了
        System.out.println(v);
    }
}

按照预期结果来说,此时 v 的值应该是 0
但实际上,v 的值是一个随机的数,并非我们所预期的结果

2. 线程安全:
程序运行的结果 100% 符合预期,不会出现有时正确,有时错误的情况

2. 为什么会发生线程不安全?

  1. Java 中的一条语句,对应的不一定是一条字节码,更不一定是一条 CPU 指令
  2. 线程调度是具有随机性的。什么时候从 CPU 上被调度下来,什么时候被调度回 CPU 都是不确定的

3. 出现线程不安全的情况

  1. 线程之间有共享的数据时,可能会发生线程不安全
  2. 在线程之间有共享数据时,有线程修改了共享数据,会发生线程不安全

4. JVM 的运行时内存区域中,哪些是线程共享的,哪些是线程私有的?变量是如何进行存储的?

  1. 线程共享的:
    PC 区域
    栈区(Java 栈 / native 栈)
  2. 线程私有的:
    堆区
    方法区
    运行时常量池

在 Java 中,变量共分为基本数据类型和引用数据类型。
但是,变量的类型特征不能决定变量保存在哪个内存区域中。
局部变量 ----------> 栈帧(栈)
对象的属性 -----------> 对象(堆)
类的静态属性 ----------> 类(方法区)

对于变量而言
局部变量是线程私有的数据,不会发生共享,所以不需要特别考虑线程安全问题
属性/对象,静态属性/类,是线程共享的,所以需要考虑线程安全问题

5. 线程不安全的原因

1. 原子性

原子操作是不可再分的,在执行完毕后不会被任务或事件中断

变量赋值的原子性:
JVM 在设计时,是按照 32 bit 为最小操作单位设计的。也就是说,凡是类型 <= 32 bit 的变量都是具有原子性的
所以在 Java 的基本数据类型中,只有 Long 和 double 类型是不具备原子性的.

关于变量赋值的原子性还有以下两种情况:

int a = 18;
int b;
b = a;

此时 b = a 不是原子的,因为程序在运行时,共分为两步:先从内存中读取 a 的值到寄存器中,然后把寄存器中的值保存到 b 中。

final int a = 18;
int b;
b = a;

此时,b = a 是原子的,因为:a 是常量,实际上就没有读 a 的这一步了,直接给 b 赋值

2. 内存可见性

  1. 计算机中存储的读写速度和容量三角
    在这里插入图片描述
  2. 内存可见性问题
    如果 CPU 每次都从内存中读取数据,就可以保证每次拿到的值都是最新的数据
    但是由于 CPU 中高速缓存的存在,导致不一定拿到最新的数据,进而导致线程不安全问题

Java 内存模型(JMM)规定:所有线程都有自己的工作内存,线程要操作任何数据,首先需要把数据从内存中加载到工作内存中,这样线程就在工作内存中进行操作,在合适的机会下,把计算结果同步回工作内存中
在这里插入图片描述

3. 代码重排序

  1. 什么是代码重排序?
    在程序运行的时候, 为了提高执行效率, 编译器和处理器会对代码进行重排序.

    在单线程的情况下, 代码重排序后的结果和重排序之前得到的结果一致, 换句话说, 代码重排序不会对结果产生什么影响.

  2. 代码重排序产生的问题:
    (1). 代码重排序可能会导致多线程程序出现内存可见性问题
    (2). 代码重排序会导致有序性问题, 程序的读写顺序和内存的读写顺序不一样

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值