大家好,欢迎来到IT知识分享网。
文章目录
一. 线程的状态
| 状态 | 说明 |
|---|---|
| NEW | 给线程安排了工作,但还未开始行动 |
| RUNNABLE | 可工作的,分为正在工作的和即将开始工作的 |
| BLOCKED | 线程等待获取锁的 |
| WAITING | 线程在无限等待唤醒 |
| TIMED_WAITING | 线程在等待唤醒,但是设置了时限 |
| TERMINATED | 进程结束 |
一、线程的主要状态
- 新建状态(New)
- 线程被创建但尚未启动。此时,线程对象已经分配了内存空间,但还没有执行。这是线程生命周期的起始状态。
- 就绪状态(Runnable)
- 线程已具备运行条件,等待CPU分配时间片。当调用线程的start()方法后,线程进入就绪状态,等待CPU调度。此时,线程已经获取到了CPU的执行权限,但还未真正开始执行。
- 运行状态(Running)
- 线程获得CPU资源并开始执行run()方法中的代码。这是线程实际执行操作的状态。
- 阻塞状态(Blocked)
- 线程因为某些原因(如等待I/O操作完成、获取锁失败等)暂时停止执行。在阻塞状态下,线程不会占用CPU资源,直到阻塞原因被消除后,线程重新进入就绪状态。
- 等待状态(Waiting)
- 线程无限期地等待某个事件的发生(如其他线程的特定操作)。在等待状态下,线程不会占用CPU资源,也不会自动唤醒,直到其他线程执行了特定的操作(如调用notify()或notifyAll()方法)。
- 超时等待状态(Timed Waiting)
- 线程等待某个事件的时间达到预设的超时时间。与等待状态类似,但超时等待状态会在超时后自动唤醒,无需其他线程的操作。例如,线程调用Thread.sleep(long millis)方法后进入超时等待状态。
- 终止状态(Terminated)
- 线程执行完毕或因异常而结束。这是线程生命周期的结束状态。一旦线程进入终止状态,就不能再重新启动。
二、线程状态转换
线程的状态转换是其生命周期中的重要概念。以下是一些常见的状态转换:
- 新建状态→就绪状态:调用线程的start()方法。
- 就绪状态→运行状态:线程获得CPU资源并开始执行。
- 运行状态→阻塞状态:线程等待某个事件(如I/O操作、锁等)而暂停执行。
- 阻塞状态→就绪状态:阻塞原因被消除,线程重新进入就绪状态。
- 运行状态→等待/超时等待状态:调用sleep方法,wait方法或者join方法
- 等待/超时等待状态→就绪状态:等待的事件发生或超时时间到达,线程被唤醒并重新进入就绪状态。
- 就绪状态/运行状态→终止状态:线程执行完毕或因异常而结束。
三、线程常用方法
与线程状态相关的常用方法包括:
- start():启动线程,使线程进入就绪状态。
- sleep(long millis):使当前线程暂停执行指定时间,进入超时等待状态。
- wait():使当前线程等待,直到其他线程调用notify()或notifyAll()方法,进入等待状态。
- notify():唤醒在此对象监视器上等待的单个线程。
- notifyAll():唤醒在此对象监视器上等待的所有线程。
- yield():暂停当前正在执行的线程对象,并执行其他线程。
- interrupt():中断线程,使其抛出InterruptedException异常或提前结束阻塞状态。
综上所述,线程状态是线程生命周期中的重要概念,了解线程状态及其转换对于多线程编程至关重要。
二. 线程安全
-定义
线程安全(Thread safety)是多线程编程中的一个重要概念,它指的是在多线程环境下,一个程序或者代码段在并发访问时,能够正确地保持其预期的行为和状态,而不会出现意外的错误或者不一致的结果。具体来说,当多个线程同时访问共享资源(如共享变量、共享数据结构、共享文件等)时,线程安全的编程技术和方法旨在解决并发访问问题,确保程序的正确性和稳定性。
-案例
以下是一个线程不安全的经典案例
// 此处定义⼀个 int 类型的变量 private static int count = 0; public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次 for (int i = 0; i < 50000; i++) {
count++; } }); Thread t2 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次 for (int i = 0; i < 50000; i++) {
count++; } }); t1.start(); t2.start(); // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来的 count 很能可能还没自增完 t1.join(); t2.join(); // 预期结果应该是 10w System.out.println("count: " + count); }
这是5次运行结果
输出结果: 66208 67240 58744 59128 61059
由此可见,该代码出现了严重的问题,线程1 和 线程2 在运行时出现了互相干扰,以至于达不到我们期望出现的结果。
-原因
- 系统线程调度的随机性
- 抢占式执行:线程在系统中的调度是随机的,即谁先抢到CPU资源谁先执行。这种抢占式执行方式导致线程的执行顺序存在很大的不确定性。
- 无序性:多个线程可能同时访问并修改同一个共享资源(如变量、数据结构等),由于执行顺序的无序性,可能引发数据竞争和状态不一致的问题。
- 原子性问题:
- 操作不可分割性:在多线程环境下,某些看似简单的操作(如自增操作count++)实际上可能包含多个步骤,分多条指令进行(如加载、增加、保存),这些步骤在多线程中可能被打断,导致操作不是原子的。
- 结果不可预测:如果一个线程正在对某个变量进行操作,中途被其他线程打断,那么该操作的结果可能是错误的,因为其他线程可能已经修改了该变量的值。
- 内存可见性
- 缓存不一致:在多线程环境中,编译器和处理器为了提高性能,可能会对代码进行优化,导致某个线程对共享变量的修改对其他线程不可见。
- 编译器优化:编译器可能会优化对某个变量的重复加载,使得变量只被加载一次,而后续对该变量的修改在其他线程中可能无法感知,这就是“可见性问题”。
案例:
public class Demo11 { private static int count = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { // while 方法体为空 while(count == 0){ ; } }); Thread t2 = new Thread(() -> { Scanner scan = new Scanner(System.in); count = scan.nextInt(); }); t1.start(); t2.start(); } }count == 0语句包含了两个指令:load指令 和cmp指令,load 指令是从内存中加载数据到寄存器中,cmp 指令是在寄存器中对 count 的值进行比较并执行跳转操作。因为load指令是从内存中访问数据,而 cmp 指令是在寄存器上操作的,所以 load 操作的速度远远慢于 cmp 操作的速度。
如果 while方法体 中没有写任何语句,在线程1中编译器会认为变量count没有发生改变而对count == 0这个语句进行优化,只进行一次 load 操作进而提高代码执行效率,但却会使程序出现bug,因为只进行一次 load 操作会使 count 的值不变,线程2的修改操作不能被线程1所感知,因此程序不会停止。这就是经典的“内存可见性问题”。而当我们在while中加入如IO等操作时,便不会出现以上优化情况,因为相对于IO操作,load操作的时间就可以忽略不记了。
- 指令重排序:
- 优化技术:编译器和处理器在执行程序时,为了提高性能和并行度,可能会对指令的执行顺序进行重新排序。
- 结果不可预知:在多线程环境中,指令重排序可能会导致线程之间的交互结果不可预知,从而引发线程安全问题。
指令重排序问题涉及CPU和编译器底层工作原理,过于复杂,这里不过多讨论。
三. 线程安全的解决方式
1. synchronized 关键字 – 监视器锁 (monitor lock)
synchronized是Java语言中的一个关键字,它提供了一种机制来控制多个线程对共享资源的访问,确保在同一时刻只有一个线程可以执行特定段的代码。synchronized关键字的主要用途和实现方式如下:
一、主要用途
- 互斥访问:当多个线程尝试同时访问共享资源时,synchronized关键字可以确保同一时刻只有一个线程可以访问该资源,从而避免数据不一致和其他并发问题。
- 方法同步:通过在方法声明中添加synchronized关键字,可以确保该方法在任何时刻只能被一个线程访问。
- 代码块同步:使用synchronized关键字修饰一个代码块,可以确保同一时刻只有一个线程可以执行该代码块。这种方式比方法同步更灵活,因为它允许更细粒度的同步控制。
- 静态方法或静态代码块同步:静态方法或静态代码块只能由类来调用,因此可以使用类名作为锁对象,以确保同一时刻只有一个线程可以访问该静态方法或静态代码块。
二、实现方式
- 修饰实例方法:当前锁定的是调用该方法的对象实例(this对象),针对同一个对象,多个对象之间不存在互斥情况。
class myRunnable implements Runnable{
int count = 0; public synchronized void increase(){
this.count++; } @Override public void run() {
for (int i = 0; i < 50000; i++) {
increase(); } } }
- 修饰静态方法:当前锁定的是这个类的所有实例共有的Class对象,多个对象之间存在互斥的情况。
class myRunnable implements Runnable{
static int count = 0; public synchronized static void increase(){
count++; } @Override public void run() {
for (int i = 0; i < 50000; i++) {
increase(); } } }
- 修饰代码块:当前锁定的是括号内指定的对象。你可以使用任何对象作为锁,但通常建议使用私有对象作为锁,以避免外部干扰。
public class Counter {
private int count = 0; private final Object lock = new Object(); public void increment() {
synchronized (lock) {
count++; } } public int getCount() {
// 这里不需要同步,因为只是读取操作 return count; } }
三、底层实现
synchronized关键字在JVM层面是通过对象监视器(Monitor)来实现的。每个Java对象都有一个关联的对象监视器,当线程进入同步代码块或同步方法时,会尝试获取该对象的监视器锁;如果获取成功,则继续执行同步代码;如果获取失败,则线程会被阻塞,直到监视器锁被释放。
四、使用注意事项
- 避免过度同步:过度的同步会导致程序性能下降,因为多个线程在访问共享资源时需要频繁地获取和释放锁。
- 锁的粒度:通常建议将锁的粒度尽量细化,即每次只锁定需要访问的共享资源的最小部分。这样可以减少锁竞争的可能性,提高并发性能。
- 避免死锁:在使用synchronized时,需要避免嵌套锁和循环等待等可能导致死锁的情况。
五、示例
以下是一个使用synchronized关键字实现线程安全计数器的简单示例:
public class Demo10 {
private static int count; public static void main(String[] args) throws InterruptedException {
Object lock = new Object(); Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
count++; } } }); Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
在这个示例中,我们使用一个私有的Object对象作为锁,以确保在任何时刻只有一个线程可以访问计数器的值。当需要增加计数器的值时,我们使用synchronized代码块来确保操作的原子性。
注意:以上代码中线程1和线程2的count == 0语句都要加锁,只加上其中一个或者都不加均会导致线程安全问题,这在上文已有提到,不再赘述。
2. volatile 关键字
volatile关键字是编程语言中的一个重要概念,特别是在处理并发编程和硬件交互时。它主要用在C/C++、Java等语言中,用于指示一个变量的值可能会意外地改变,因此编译器在访问该变量时不能进行优化,以确保变量的可见性和有序性。以下是关于volatile关键字的详细解释:
一、volatile关键字的作用
- 防止编译器优化:
- 编译器在编译代码时,为了提高执行效率,会对代码进行优化,包括对变量的读取、写入和访问进行优化。然而,在某些情况下,这种优化可能会导致意外的结果,特别是当变量的值可能在程序的其他部分发生变化时。使用volatile关键字可以告诉编译器不要对这些变量的访问进行优化。
- 保证变量可见性:
- 在多线程环境下,多个线程可能同时访问和修改共享变量。由于编译器的优化或缓存机制,一个线程对变量的修改可能不会立即对其他线程可见。使用volatile关键字可以确保当一个线程修改变量后,其他线程能够立即看到变量的最新值。
具体机制如下:
代码在写⼊ volatile 修饰的变量的时候,改变线程⼯作内存中volatile变量副本的值,将改变后的副本的值从⼯作内存刷新到主内存;
代码在读取 volatile 修饰的变量的时候,从主内存中读取volatile变量的最新值到线程的⼯作内存中,从⼯作内存中读取volatile变量的副本。
*⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存)
- 在多线程环境下,多个线程可能同时访问和修改共享变量。由于编译器的优化或缓存机制,一个线程对变量的修改可能不会立即对其他线程可见。使用volatile关键字可以确保当一个线程修改变量后,其他线程能够立即看到变量的最新值。
- 禁止指令重排序:
- 在Java等语言中,为了提高执行效率,编译器和处理器可能会对指令进行优化重新排序。然而,这种重排序可能会破坏程序的预期行为。使用volatile关键字可以禁止对包含volatile变量的代码块进行指令重排序优化。
二、volatile关键字的使用场景
- 硬件寄存器访问:
- 在嵌入式编程中,变量可能代表硬件寄存器的映射,这些寄存器的值可能会在硬件层面发生变化,不受程序控制。使用volatile关键字可以确保编译器在访问这些寄存器时不会进行优化。
- 多线程共享变量:
- 在多线程环境下,多个线程可能同时访问和修改共享变量。使用volatile关键字可以确保变量修改的可见性,但需要注意的是,volatile并不能保证原子性操作,对于复合操作(如i++)仍需要使用其他同步机制(如synchronized、AtomicInteger等)。
- 信号处理器中的变量:
- 在信号处理函数中,变量可能会因为信号的中断而发生变化。使用volatile关键字可以确保编译器不会对这些变量的访问进行优化,保证信号处理函数能够正确地读取和修改这些变量。
三、volatile关键字的注意事项
- 不保证原子性:
- volatile关键字只保证变量的可见性和有序性,但不保证原子性。对于需要原子性操作的场景,应使用synchronized、Atomic类等同步机制。
- 谨慎使用:
- volatile关键字的使用需要谨慎,因为它会阻止编译器优化,可能会降低程序的执行效率。因此,在不需要volatile特性的情况下,应避免使用它。
- 理解内存模型:
- 要深入理解volatile关键字的工作原理,需要了解所在编程语言的内存模型。例如,在Java中,需要了解Java内存模型(JMM)以及volatile变量在其中的特殊规则。
综上所述,volatile关键字是并发编程和硬件交互中的一个重要工具,但使用时需要了解其特性并谨慎选择使用场景。
注意:有读者反映死锁章节写得不够详细,现在将死锁章节独立出来,有兴趣的小伙伴可以参考死锁的产生与避免。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/115231.html