Java并发学习之线程状态及Thread常用方法详解

时间:2022-04-27
本文章向大家介绍Java并发学习之线程状态及Thread常用方法详解,主要内容包括线程状态及Thread常用方法详解、II. Thread解析、III. 小结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

线程状态及Thread常用方法详解

I. 线程状态

在前面线程创建的一篇博文中,明确说明只有在调用 Thread#start()方法之后,线程才会启动;那线程创建完和这个启动又是什么关系呢?启动是否又是运行呢?本节则主要集中在线程的各个状态的解释以及状态变迁的原因

先来一个图,说明下线程的五个状态

1. 创建

顾名思义,就是创建了一个线程,也就通过 new Thread() 触发

2. 就绪状态

就绪,表示线程已经准备好了,随时可以进入运行,

start() 调用之后,线程进入就绪状态,这个时候是准备运行,但是并没有执行

3. 运行状态

表示线程在执行了,真正工作跑任务

4. 阻塞状态

线程运行之后,发生了一些变故,需要挂起时,这时就进入阻塞,把cpu和资源让给其他的线程去执行;这个就是阻塞状态了

也就是说,必须是有运行状态进入阻塞状态

5. 结束

线程执行完了,也是时候收拾收拾,各回各家了,就表示这个线程该干的活干完了,到过河拆桥的时候了,赶紧把这个线程丢到垃圾堆吧(线程回收),这个状态就是线程结束(或者说线程死亡状态)


上面说了五个线程状态,各是什么意思,下面简单说下他们的关系

以一个线程的使用流程为例

LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 创建一个线程
Thread thread = new Thread(() -> {
    try {
        System.out.println(queue.take());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
// 启动线程
thread.start();
// 主线程挂起,保证thread线程逻辑进入并执行
Thread.sleep(2000);
// 主线程向队列中塞一个数据,唤醒thread线程
queue.put("hello world");
// 等待线程执行完毕
thread.join();
// 线程执行结束
System.out.println("---over---");

创建线程有四种方式,可以参考 《Java并发学习之四种线程创建方式的实现与对比》,

结合上面的case,分析下五种状态的转换过程:

  1. 首先是通过new来创建一个线程对象 thread, 这儿时候,线程就处于创建状态了
  2. 接着我需要线程工作了,然后调用thread.start()方法,来启动线程
  • 这个时候,线程并不会直接运行,此时会进入就绪状态(进入可运行线程池),也就表示我准备好了,随时可以工作
  • 那么什么时候工作呢?这个就不由我们来控制了,实际是由线程调度程序从可运行线程池中挑一个线程来工作
  1. 运气来了,thread线程执行了,假设其从一个阻塞队列queue中取数据
  • 然而此时queue为空,导致获取不到数据,线程被阻塞,等待队列非空,这个时候线程就由运行状态进入阻塞状态了
  • 主线程此时往队列中塞入一个数据,thread线程被唤醒,此时依然是进入就绪状态,等待线程调度程序来执行它
  1. 等线程执行完毕后,就进入了死亡状态,然后就开始gc回收资源了

II. Thread解析

在java这门编程语言中,要使用线程,多半是离不开接触Thread这个类,为什么会说是多半呢? 因为有些时候,我们借助线程池,fork/join等来实现并发时,可能并不需要显示的利用的Thread类,但底层其实是离不开的

这里也不讲Thread是怎么工作的,实现原理啥的,比较复杂,我也莫不准,就从使用角度出发,来看看里面常用的方法,都是干嘛用的,以及什么时候用

1. start 方法

第一个就是这个start()方法了,启动线程

执行该方法之后,线程进入就绪状态,对使用者而言,希望线程执行就是调用的这个方法(注意调用之后不会立即执行)

这个方法的主要目的就是告诉系统,我们的线程准备好了,cpu有空了赶紧来执行我们的线程


2. run 方法

这个就有意思了,我们采用继承Thread类来创建线程时,需要覆盖的就是这个方法,把线程执行的业务逻辑,放在这个方法里面,但是线程的执行,却是start()方法

run 方法中为具体的线程执行的代码逻辑,一般而言,都不应该被直接进行调用

那么问题来了,如果直接调用了会怎样?

直接调用Thread的run方法,并不会报错,且可以正常执行,但是执行是在调用这个方法的线程中执行的,不会让thread这个线程进入就绪状态,运行状态啥的,其实质就是一个普通对象的普通方法调用


3. sleep 方法

睡眠一段时间,这个过程中不会释放线程持有的锁, 传入int类型的参数,表示睡眠多少ms 让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会

我们最常见的一种使用方式是在主线程中直接调用 Thread.sleep(100) , 表示先等个100ms, 然后再继续执行


4. wait 方法

wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问

wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程

通常我们执行wait方法是因为当前线程的执行,可能依赖到其他线程,如登录线程中,若发现用户没有注册,则等待,等用户注册成功后继续走登录流程(我们不考虑这个逻辑是否符合实际),

这里就可以在登录线程中调用 wait方法, 在注册线程中,在执行完毕之后,调用notify方法通知登录线程,注册完毕,然后继续进行登录后续action


5. yield 方法

暂停当前正在执行的线程对象,并执行其他线程 yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

这个方法的执行,有点像一个拿到面包的人对另外几个人说,我把面包放在桌上,我们从新开始抢,那么下一个拿到面包的还是这些人中的某个(大家机会均等)

想象不出啥时候会这么干


6. join 方法

启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行

从上面的描述也可以很容易看出什么场景需要调用这个方法,主线程和子线程谁先结束不好说,如果主线程提前结束了,导致整个应用都关了,这个时候子线程没执行完,就呵呵了;

其次就是子线程执行一系列计算,主线程会用到计算结果,那么就可以执行这个方法,保证子线程执行完毕后再使用计算结果


7. setDaemon 方法

这个比较有意思,将线程定义为守护线程,那么什么是守护线程?

用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

这里有几点需要注意:

  1. thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程
  2. 在Daemon线程中产生的新线程也是Daemon的。
  3. 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑

因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务。一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样。这对程序是毁灭性的。造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了

III. 小结

1. 线程状态

线程有五个状态

  • new一个线程对象后,首先进入创建状态
  • 执行Thread#start方法之后,进入就绪状态
  • 线程调度程序将就绪状态的线程标记为运行状态并真正运行
  • 线程运行过程中,可以挂起,进入阻塞状态,阻塞状态恢复后,接着进入就绪而不是立马又恢复运行状态
  • 线程执行完了,就进入结束/死亡状态

2. Thread使用注意

  • 线程执行的业务逻辑,放在run()方法中
  • 使用 thread.start() 启动线程
  • wait方法需要和notify方法配套使用
  • 守护线程必须在线程启动之前设置
  • 如果需要等待线程执行完毕,可以调用 join()方法