首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【JavaSE】线程的状态与生命周期 && 线程安全 && synchronized && 编译器优化 && JMM && 编译器优化问题

【JavaSE】线程的状态与生命周期 && 线程安全 && synchronized && 编译器优化 && JMM && 编译器优化问题

原创
作者头像
lirendada
发布2026-04-04 09:39:57
发布2026-04-04 09:39:57
1170
举报
文章被收录于专栏:JavaJava

线程的状态与生命周期

线程的状态是枚举类型 Thread.State,如下所示:

状态名

英文名

描述

NEW

新建

线程对象已创建,但还没调用 start()

RUNNABLE

可运行

线程已启动,等待 CPU 调度执行 run() 方法

BLOCKED

阻塞

线程因等待锁资源,而暂停执行

WAITING

等待

线程无限期地等待另一个线程的唤醒(例如调用了 wait()、join())

TIMED_WAITING

计时等待

线程在等待一个特定的时间后被唤醒(如 sleep(long)、join(long)、wait(long))

TERMINATED

终止

线程已完成执行或异常退出

我们可以通过 Thread.getState() 来获取当前进程在某个时刻的状态,如下所示:

代码语言:javascript
复制
public class demo {
    public static void main(String[] args) throws InterruptedException {
        // 打印所有状态
        for(Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
        System.out.println("-----------");
        
        // 打印 t 线程某个时刻的状态
        Thread t = new Thread(() -> {
            for(int i = 0;i < 5;i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState() + " " + t.isAlive());
        t.start();
        System.out.println(t.getState() + " " + t.isAlive());

        t.join();
        System.out.println(t.getState() + " " + t.isAlive());
    }
}

// 运行结果:
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
-----------
NEW false
RUNNABLE true
TERMINATED false

线程安全

一、线程不安全的原因

  1. 线程的调度执行是随机的(抢占式调度),这是线程不安全的根本原因
  2. 多个线程同时修改同一个变量
  3. 修改变量的操作,不是原子性的,比如 count++ 并不是原子操作。
    1. 注意:java 中对于 内置类型读取赋值,是原子性的
  4. 内存可见性、指令重排序问题
    1. 这两个问题通常用 volatile 来解决,但是 volatile 并不保证原子性

二、synchronized 关键字

进入 synchronized 修饰的代码块,相当于加锁

退出 synchronized 修饰的代码块,相当于解锁

实际上 synchronized 底层不只是加锁,还有锁升级等机制,具体可以看多线程进阶的笔记!

① 修饰实例方法

代码语言:javascript
复制
public synchronized void doSomething() {
    // 临界区代码
}
  • 锁对象是 this(当前实例)。
  • 多个线程调用同一个对象的该方法时,会串行执行。

修饰静态方法

代码语言:javascript
复制
public static synchronized void doSomethingStatic() {
    // 临界区代码
}
  • 锁对象是 类名.class
  • 多个线程访问同一个类的静态同步方法,也会被串行化。

修饰代码块(推荐⭐⭐⭐)

代码语言:javascript
复制
private final Object lock = new Object(); // 自定义锁对象

public void doSomething() {
    synchronized (lockObject) {
        // 临界区代码
    }
}
  • 锁对象除了内置类型以外,可以是任意类型的对象,但通常不能是 StringIntegerBoolean 等常量对象,因为多个类、多个线程可能锁的是同一个对象!
  • 锁对象最好声明为 private。因为如果锁对象是 public 的,别的类也能访问和同步这个对象,可能破坏原有的同步逻辑。
  • 此外,为了保证锁对象在多线程可见性和稳定性上的安全性,通过要给锁对象加上 final 关键字修饰
    • 举个例子,给你一把锁,如果别人偷偷换了一把新的,你以为门锁住了,其实别人可以从另一把锁进来,这就是 "非 final locker" 导致的线程安全崩溃。

💥synchronized 是可重入锁

java 中,synchronized 同步块对同一条线程来说是可重入的,即不会出现自己把自己锁死的问题

可重入锁指的是 同一个线程 可以多次获得同一个锁,不会发生死锁。每进入一层 synchronized 或调用一层锁方法,锁的持有计数 lock count + 1,退出对应的一层,lock count - 1。只有当计数变为 0,锁才真正释放。

如下面代码所示:

代码语言:javascript
复制
for (int i = 0; i < 50000; i++) { 
    // 连续两次锁同一个对象,在java中并不会出现死锁,只会被看做是一层加锁
    synchronized (locker) {  // lock count+1
        synchronized (locker) {  // lock count+1
            count++; 
        } // lock count-1
    } // lock count-1
}

注意如果是一个线程进行多层获取同一个锁,只有锁计数 count0,才会释放该锁对象,如下所示:

代码语言:javascript
复制
Object locker = new Object();
synchronized (locker) {     // +1
    synchronized (locker) { // +1
        synchronized (locker) { // +1
            synchronized (locker) { // +1
                // 此时锁计数 = 4,线程持有 locker 的锁 4 次
            } // -1(计数 3)
        } // -1(计数 2)
    } // -1(计数 1)
} // -1(计数 0,真正释放锁)

三、编译器优化问题

java 程序编译期间,用 javac.java 文件编译成 .class 文件,这并不涉及到优化问题,但后续 JIT 编译器(Just-In-Time Compiler)会在 JVM 运行期间将 热点代码(即被 JVM 判定为值得优化的代码) 编译为本地机器码,提升性能,这就形成了一些优化比如内联函数、指令重排序、内存不可见、锁优化

这些优化实际上是很有必要的,因为很多编程新手对于程序的性能把握并不好,所以编译器这一层就多干了很多活来提高程序的效率,相当于提高了程序的效率下限。

但这些优化又无形带来了一些编程时候的 bug,为了防止这些 bug,我们可以使用内存屏障语义的关键字来避免,如下所示:

  • volatile:阻止指令重排序 + 保证变量对其他线程的可见性
  • synchronized:包含获取/释放锁的内存屏障
  • final:构造后不可变,构造安全(不能被重排)

① 内存可见性问题 -- volatile

以下面代码为例,在 t2 输入非零之后,正常情况应该是 t1 就结束循环了,但程序却停不下来,t1 一直在循环之中,这是为什么❓❓❓

代码语言:javascript
复制
public class demo1 {
    private static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
           while(count == 0) {
               // do something
           }
           System.out.println("t1循环结束!!!");
        });

        Thread t2 = new Thread(() -> {
           Scanner sc = new Scanner(System.in);
           count = sc.nextInt();
           System.out.println("t2结束!!!");
        });

        t1.start();
        t2.start();
    }
}

这就是之前提到的编译器优化带来的问题,编译器会把 while(count == 0) 认为是热点代码,因为这是一个循环,会大量重复地访问 count,所以编译器会将原本存放在内存中的 count,复制到 CPU 的寄存器或者缓存中,然后在 CPU 中访问,这可以大大提高访问速度,因为 CPU 速度可是要比内存访问速度快上几千倍不止的!

但正是因为这个问题,t2 修改的 count 还是那个存放在内存中的 count,而此时 t1 那边跑的是 CPU 中的 count,看不到内存那边已经修改了,所以就会一直判断 while(count == 0) 是成功的,导致了程序错误!

此时只需要在 count 前面加上 volatile 进行修饰,强制让编译器访问内存的那份 count,即可解决这个优化带来的问题,但同时速度就会降低!

② Java内存模型 -- JMM

JVM 定义了一种 Java 内存模型(Java Memory ModelJMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此会由于不同平台下的内存模型的差异,导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。

在多线程中,CPU 和编译器会出于性能考虑进行:

  • 缓存优化(每个线程有自己的工作内存)
  • 指令重排序(编译器或 CPU 会乱序执行某些语句)

这些优化可能导致一个线程修改的变量,另一个线程看不到,从而引发 "明明已经赋值却读取到旧值" 等并发 bug,而 JMM 就是为了解决这些问题而生。

JMM 的主要目标是定义程序中各个变量的访问规则,即在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括 实例字段静态字段构成数组对象的元素,但不包括 局部变量方法参数,因为后两者是线程私有的,不会被线程共享。

JMM 规定了所有的变量都存储在 "主内存" 中。每个线程还有自己的 "工作内存",线程的 "工作内存" 中保存了被该线程使用到的变量的 "主内存" 副本拷贝,线程对变量的所有操作(如读取、赋值等)都必须在 "工作内存" 进行,而不能直接读写 "主内存" 中的变量。所以不同的线程之间也无法直接访问对方 "工作内存" 中的变量,线程间变量值的传递均需要通过 "主内存" 来完成。

"主内存" 和 "工作内存" 的描述如下表所示:

位置

描述

主内存(Main Memory)

所有共享变量都在这里

工作内存(Working Memory)

每个线程自己的变量副本(缓存)

看起来很复杂,其实很简单!"主内存" 就是我们平常所说的主存,而 "工作内存" 实际上是 CPU 中的缓存和寄存器,而 JMM 这一套规则,实际上和我们上面讲解 volatile 时候解释的内存可见性是一个道理的,只不过用 "工作内存" 这个术语来涵盖了我们提到的 CPU 中的缓存或者寄存器!

即原始数据都是存放在 "主内存",然后根据 JMM 优化会放到 "工作内存" 中执行,速度会大大提高,但就会存在 "主内存" 和 "工作内存" 数据不一致的情况,此时就要通过 volatile 来解决!

如果只关注操作系统或者硬件来说,根本就没有 "主内存"、"工作内存" 的说法!

虽然 java 官方给的 "工作内存" 这个概念让人很晕,实际上就是指 CPU 中的缓存和寄存器,甚至指以后可能更新的缓存技术,但它的目的实际上是要让 java 程序员不用去关心底层是什么结构,让 "工作内存" 来直接代指这些缓存或者寄存器,甚至以后可能更新出来的技术,只需要让程序员知道这是 "工作内存" 的概念即可!

③ 指令重排序

简单的说,指令重排序就是编译器或 CPU 为了优化性能,改变了语句执行顺序,导致和原先的程序逻辑不一致的情况!

为了避免这种情况,同样是要使用 volatilesynchronizedfinal 等手段建立有序性屏障!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程的状态与生命周期
  • 线程安全
    • 一、线程不安全的原因
    • 二、synchronized 关键字
      • ① 修饰实例方法
      • ② 修饰静态方法
      • ③ 修饰代码块(推荐⭐⭐⭐)
      • 💥synchronized 是可重入锁
    • 三、编译器优化问题
      • ① 内存可见性问题 -- volatile
      • ② Java内存模型 -- JMM
      • ③ 指令重排序
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档