
线程的状态是枚举类型 Thread.State,如下所示:
状态名 | 英文名 | 描述 |
|---|---|---|
NEW | 新建 | 线程对象已创建,但还没调用 start() |
RUNNABLE | 可运行 | 线程已启动,等待 CPU 调度执行 run() 方法 |
BLOCKED | 阻塞 | 线程因等待锁资源,而暂停执行 |
WAITING | 等待 | 线程无限期地等待另一个线程的唤醒(例如调用了 wait()、join()) |
TIMED_WAITING | 计时等待 | 线程在等待一个特定的时间后被唤醒(如 sleep(long)、join(long)、wait(long)) |
TERMINATED | 终止 | 线程已完成执行或异常退出 |
我们可以通过 Thread.getState() 来获取当前进程在某个时刻的状态,如下所示:
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
count++ 并不是原子操作。java 中对于 内置类型 的 读取 和 赋值,是原子性的!volatile 来解决,但是 volatile 并不保证原子性!synchronized 关键字进入 synchronized 修饰的代码块,相当于加锁
退出 synchronized 修饰的代码块,相当于解锁
实际上
synchronized底层不只是加锁,还有锁升级等机制,具体可以看多线程进阶的笔记!
public synchronized void doSomething() {
// 临界区代码
}this(当前实例)。public static synchronized void doSomethingStatic() {
// 临界区代码
}类名.class。private final Object lock = new Object(); // 自定义锁对象
public void doSomething() {
synchronized (lockObject) {
// 临界区代码
}
}String、Integer、Boolean 等常量对象,因为多个类、多个线程可能锁的是同一个对象!private。因为如果锁对象是 public 的,别的类也能访问和同步这个对象,可能破坏原有的同步逻辑。final 关键字修饰。synchronized 是可重入锁在 java 中,synchronized 同步块对同一条线程来说是可重入的,即不会出现自己把自己锁死的问题。
可重入锁指的是 同一个线程 可以多次获得同一个锁,不会发生死锁。每进入一层 synchronized 或调用一层锁方法,锁的持有计数 lock count + 1,退出对应的一层,lock count - 1。只有当计数变为 0,锁才真正释放。
如下面代码所示:
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
}注意如果是一个线程进行多层获取同一个锁,只有锁计数 count 为 0,才会释放该锁对象,如下所示:
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 一直在循环之中,这是为什么❓❓❓
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,即可解决这个优化带来的问题,但同时速度就会降低!
JMM
JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此会由于不同平台下的内存模型的差异,导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。
在多线程中,CPU 和编译器会出于性能考虑进行:
CPU 会乱序执行某些语句)这些优化可能导致一个线程修改的变量,另一个线程看不到,从而引发 "明明已经赋值却读取到旧值" 等并发 bug,而 JMM 就是为了解决这些问题而生。
JMM 的主要目标是定义程序中各个变量的访问规则,即在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括 实例字段、静态字段 和 构成数组对象的元素,但不包括 局部变量 和 方法参数,因为后两者是线程私有的,不会被线程共享。
JMM 规定了所有的变量都存储在 "主内存" 中。每个线程还有自己的 "工作内存",线程的 "工作内存" 中保存了被该线程使用到的变量的 "主内存" 副本拷贝,线程对变量的所有操作(如读取、赋值等)都必须在 "工作内存" 进行,而不能直接读写 "主内存" 中的变量。所以不同的线程之间也无法直接访问对方 "工作内存" 中的变量,线程间变量值的传递均需要通过 "主内存" 来完成。
"主内存" 和 "工作内存" 的描述如下表所示:
位置 | 描述 |
|---|---|
主内存(Main Memory) | 所有共享变量都在这里 |
工作内存(Working Memory) | 每个线程自己的变量副本(缓存) |
看起来很复杂,其实很简单!"主内存" 就是我们平常所说的主存,而 "工作内存" 实际上是 CPU 中的缓存和寄存器,而 JMM 这一套规则,实际上和我们上面讲解 volatile 时候解释的内存可见性是一个道理的,只不过用 "工作内存" 这个术语来涵盖了我们提到的 CPU 中的缓存或者寄存器!
即原始数据都是存放在 "主内存",然后根据 JMM 优化会放到 "工作内存" 中执行,速度会大大提高,但就会存在 "主内存" 和 "工作内存" 数据不一致的情况,此时就要通过 volatile 来解决!
如果只关注操作系统或者硬件来说,根本就没有 "主内存"、"工作内存" 的说法!
虽然
java官方给的 "工作内存" 这个概念让人很晕,实际上就是指CPU中的缓存和寄存器,甚至指以后可能更新的缓存技术,但它的目的实际上是要让java程序员不用去关心底层是什么结构,让 "工作内存" 来直接代指这些缓存或者寄存器,甚至以后可能更新出来的技术,只需要让程序员知道这是 "工作内存" 的概念即可!
简单的说,指令重排序就是编译器或 CPU 为了优化性能,改变了语句执行顺序,导致和原先的程序逻辑不一致的情况!
为了避免这种情况,同样是要使用 volatile、synchronized、final 等手段建立有序性屏障!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。