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

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

I. 线程状态

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

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

线程状态变迁图

1. 建立

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

2. 就绪状态

就绪,表示线程已经准备好了,随时能够进入运行,编程语言

start() 调用以后,线程进入就绪状态,这个时候是准备运行,可是并无执行学习

3. 运行状态

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

4. 阻塞状态

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

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

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()方法

扫描关注,java分享

QrCode

相关文章
相关标签/搜索