当前位置: 首页 > article >正文

深入学习Java的线程的生命周期

线程的状态/生命周期

五种状态
这是从 操作系统 层面来描述的

在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态
    当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】
    如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
    等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态
这是从 Java API 层面来描述的。

根据 Thread.State 枚举,Java中线程的状态分为6种:

1、初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2、运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3、阻塞(BLOCKED):表示线程阻塞于锁。

4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

6、终止(TERMINATED):表示该线程已经执行完毕。

状态之间的变迁如下图所示:

在这里插入图片描述

掌握这些状态可以让我们在进行Java程序调优时可以提供很大的帮助。

线程常见方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。

在这里插入图片描述

sleep与 yield
sleep方法

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
  • sleep当传入参数为0时,和yield相同
Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("执行完成");
    }
},"t1");
t1.start();
log.debug("线程t1的状态:"+t1.getState());
try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
log.debug("线程t1的状态:"+t1.getState());
//t1.interrupt();

在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序。

while(true) {
     try {
         Thread.sleep(50);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
}
  • 可以用 wait 或 条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep 适用于无需锁同步的场景

yield方法

  • yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
  • 具体的实现依赖于操作系统的任务调度器
  • 比如ConcurrentHashMap#initTable 方法中就使用了yield方法,

这是因为ConcurrentHashMap中可能被多个线程同时初始化table,但是其实这个时候只允许一个线程进行初始化操作,其他的线程就需要被阻塞或等待,但是初始化操作其实很快,这里Doug Lea大师为了避免阻塞或者等待这些操作引发的上下文切换等等开销,就让其他不执行初始化操作的线程干脆执行yield()方法,以让出CPU执行权,让执行初始化操作的线程可以更快的执行完成。

线程的优先级
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。

设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

Runnable task1 = () -> {
    int count = 0;
    for (;;) {
        System.out.println("t1---->" + count++);
    }
};
Runnable task2 = () -> {
    int count = 0;
    for (;;) {
        // Thread.yield();
        System.out.println("t2---->" + count++);
    }
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

join方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景。

为什么需要 join?

下面的代码执行,打印 count 是什么?

private static int count = 0;
public static void main(String[] args) throws InterruptedException {
    log.debug("开始执行");
    Thread t1 = new Thread(() -> {
        log.debug("开始执行");
        SleepTools.second(1);
        count = 5;
        log.debug("执行完成");
    },"t1");
    t1.start();
    log.debug("结果为:{}", count);
    log.debug("执行完成");
}

输出

19:30:09.614 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:30:09.660 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:30:09.660 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 结果为:0
19:30:09.662 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成
19:30:10.673 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成

分析

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 count=5
  • 而主线程一开始就要打印 count的结果,所以只能打印出 count=0

解决方法

  • 用 sleep 行不行?为什么?
  • 用 join,加在 t1.start() 之后即可

实现同步
以调用方角度来讲,如果:

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步
    在这里插入图片描述
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
    log.debug("开始执行");
    Thread t1 = new Thread(() -> {
        log.debug("开始执行");
        SleepTools.second(1);
        count = 5;
        log.debug("执行完成");
    },"t1");
    t1.start();
    //SleepTools.second(1);
    t1.join();
    log.debug("结果为:{}", count);
    log.debug("执行完成");
 }   

输出

19:36:05.192 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:36:05.235 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 开始执行
19:36:06.239 [t1] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成
19:36:06.239 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 结果为:5
19:36:06.240 [main] DEBUG com.tuling.learnjuc.base.ThreadJoinDemo - 执行完成

面试题
现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

可以利用join()方法实现,把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程T2中调用了线程T1的join()方法,直到线程T1执行完毕后,才会继续执行线程T2剩下的代码。

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        log.debug("线程t1执行完成");
    }
},"t1");
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("线程t2执行完成");
    }
},"t2");
Thread t3 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("线程t3执行完成");
    }
},"t3");
t1.start();
t2.start();
t3.start();

守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
    log.debug("开始运行...");
    SleepTools.second(3);
    log.debug("运行结束...");
}, "t1");
// 设置t1线程为守护线程
t1.setDaemon(true);
t1.start();
SleepTools.second(1);
log.debug("运行结束...");

输出

21:21:58.104 [main] DEBUG com.tuling.learnjuc.base.DaemonThreadDemo - 开始运行...
21:21:58.143 [t1] DEBUG com.tuling.learnjuc.base.DaemonThreadDemo - 开始运行...
21:21:59.158 [main] DEBUG com.tuling.learnjuc.base.DaemonThreadDemo - 运行结束...

守护线程的应用场景

守护线程看起来好像没什么用?但是实际上它的作用很大。

  • 比如在JVM中垃圾回收器就采用了守护线程,如果一个程序中没有任何用户线程,那么就不会产生垃圾,垃圾回收器也就不需要工作了。
  • 在一些中间件的心跳检测、事件监听等涉及定时异步执行的场景中也可以使用守护线程,因为这些都是在后台不断执行的任务,当进程退出时,这些任务也不需要存在,而守护线程可以自动结束自己的生命周期。

从这些实际场景中可以看出,对于一些后台任务,当不希望阻止JVM进程结束时,可以采用守护线程。

线程的终止
线程自然终止
要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。

面试题: 如何正确终止正在运行的线程?

stop(不要使用)
stop()方法已经被jdk废弃,调用stop方法无论run()中的逻辑是否执行完,都会释放CPU资源,释放锁资源。这会导致线程不安全,因为该方法会导致两个问题:

  • 立即抛出ThreadDeath异常,在run()方法中任何一个执行指令都可能抛出ThreadDeath异常。
  • 会释放当前线程所持有的所有的锁,这种锁的释放是不可控的。

比如:线程A的逻辑是转账(获得锁,1号账户减少100元,2号账户增加100元,释放锁),那线程A刚执行到1号账户减少100元就被调用了stop方法,释放了锁资源,释放了CPU资源。1号账户平白无故少了100元。

public class ThreadStopDemo {
    private static final Object lock = new Object();
    private static int account1 = 1000;
    private static int account2 = 0;
    public static void main(String[] args)  {
        Thread threadA = new Thread(new TransferTask(),"threadA");
        threadA.start();
        // 等待线程A开始执行
        SleepTools.ms(50);
        // 假设在转账过程中,我们强制停止了线程A
        threadA.stop();
        //验证锁是否释放
//        synchronized (lock){
//            System.out.println("主线程加锁成功");
//        }
    }
    static class TransferTask implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                try{
                    System.out.println("开始转账...");
                    // 1号账户减少100元
                    account1 -= 100;
                    // 休眠100ms
                    SleepTools.ms(50);
                    // 假设在这里线程被stop了,那么2号账户将不会增加,且锁会被异常释放
                    System.out.println("1号账户余额: " + account1);
                    account2 += 100; // 2号账户增加100元
                    System.out.println("2号账户余额: " + account2);
                    System.out.println("转账结束...");
                }catch (Throwable t){
                    System.out.println("线程A结束执行");
                    t.printStackTrace();
                }
            }
        }
    }
}

因此,在实际应用中,一定不能使用stop()方法来终止线程,那么如何安全地实现线程的终止呢?

中断机制
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。线程通过检查自身的中断标志位是否被置为true来进行响应,

线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、obj.wait等),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

不建议自定义一个取消标志位来中止线程的运行。因为run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,

1、一般的阻塞方法,如sleep等本身就支持中断的检查,

2、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

中断正常运行的线程

中断正常运行的线程, 不会清空中断状态

Thread t1 = new Thread(()->{
    while(true) {
        Thread current = Thread.currentThread();
        boolean interrupted = current.isInterrupted();
        if(interrupted) {
            log.debug(" 中断状态: {}", interrupted);
            break;
        }
    }
}, "t1");
t1.start();
//中断线程t1
t1.interrupt();
log.debug("中断状态:{}",t1.isInterrupted());

输出

20:45:17.596 [t1] DEBUG com.tuling.learnjuc.base.ThreadInterruptDemo -  中断状态: true
20:45:17.596 [main] DEBUG com.tuling.learnjuc.base.ThreadInterruptDemo - 中断状态:true
中断 sleep,wait,join 的线程

这几个方法都会让线w程进入阻塞状态,中断线程会清空中断状态。以 sleep 为例:

Thread t1 = new Thread(()->{
    while(true) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}, "t1");
t1.start();
Thread.sleep(100);
//中断线程t1
t1.interrupt();
log.debug("中断状态:{}",t1.isInterrupted());

http://www.kler.cn/a/522570.html

相关文章:

  • ASP.NET代码审计 SQL注入篇(简单记录)
  • 16、智能驾驶域控的材料回收
  • 计算机网络之计算机网络主要性能
  • AI大模型开发原理篇-2:语言模型雏形之词袋模型
  • Nginx前端后端共用一个域名如何配置
  • SpringBoot 中的测试jar包knife4j(实现效果非常简单)
  • 【快速上手】阿里云百炼大模型
  • 领域知识图谱的应用案例---下
  • vxe-table和element表尾合计行
  • “com.docker.vmnetd”将对你的电脑造成伤害。 如何解决 |Mac
  • 基于Flask的豆瓣电影可视化系统的设计与实现
  • IDEA 中 Maven 依赖变灰并带斜线的解决方法及原理分析
  • 数据结构——实验七·排序
  • 【LeetCode: 704. 二分查找 + 二分】
  • 海外问卷调查渠道查如何设置:最佳实践+示例
  • 在生产环境中部署和管理 Apache:运维从入门到精通
  • 关于数字地DGND和模拟地AGND隔离
  • Layui 列表根据不同数据展示不同内容,并展示对应颜色
  • The Simulation技术浅析(一)
  • 【PySide6快速入门】QInputDialog输入对话框
  • java语法学习
  • TypeScript进阶(三):断言
  • 【PySide6拓展】QCalendarWidget 日历控件
  • 一分钟搭建promehteus+grafana+alertmanager监控平台
  • 前端力扣刷题 | 4:hot100之 子串
  • Mybatis-plus缓存