每一个事物都有其生命周期,也就是事物从出生开始到最终消亡这中间的整个过程;在其整个生命周期的历程中,会有不一样阶段,每一个阶段对应着一种状态,好比:人的一辈子会经历从婴幼儿、青少年、青壮年、中老年到最终死亡,离开这人世间,这是人一辈子的状态;一样的,线程做为一种事物,也有生命周期,在其生命周期中也存在着不一样的状态,不一样的状态之间还会有互相转换。java
在上文中,咱们提到了线程通讯,在多线程系统中,不一样的线程执行不一样的任务;若是这些任务之间存在联系,那么执行这些任务的线程之间就必须可以通讯,共同协调完成系统任务。linux
在本文中,咱们接着来讲说线程通讯中的线程的生命周期。程序员
咱们先来查看jdk
文档,在Java
中,线程有如下几个状态:编程
在Java
中,给定的时间点上,一个线程只能处于一种状态,上述图片中的这些状态都是虚拟机状态,并非操做系统的线程状态。线程对象的状态存放在Thread
类的内部类(State
)中,是一个枚举,存在着6种固定的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
。windows
状态之间的转换以下图所示:服务器
下面就来对这些状态一一解释:网络
1.新建状态(new
): 使用new
建立一个线程对象,仅仅在堆中分配内存空间,在调用start方法
以前的线程所处的状态;在此状态下,线程还没启动,只是建立了一个线程对象存储在堆中;好比:多线程
Thread t = new Thread(); // 此时t就属于新建状态
当新建状态下的线程对象调用了start方法
,该线程对象就重新建状态进入可运行状态(runnable
);线程对象的start方法只能调用一次,屡次调用会发生IllegalThreadStateException
;并发
2.可运行状态(runnable
):又能够细分红两种状态,ready
和running
,分别表示就绪状态和运行状态。ide
start方法
以后,等待JVM
的调度(此时该线程并无运行),还未开始运行;JVM
调度,处在运行中;若是存在多个CPU
,那么容许多个线程并行运行;3.阻塞状态(blocked
):处于运行中的线程由于某些缘由放弃CPU时间片
,暂时中止运行,就会进入阻塞状态;此时JVM
不会给线程分配CPU时间片
,直到线程从新进入就绪状态(ready
),才有可能转到运行状态;
阻塞状态只能先进入就绪状态,进而由操做系统转到运行状态,不能直接进入运行状态;阻塞状态发生的两种状况:
A线程
处于运行中,试图获取同步锁时,但同步锁却被B线程
获取,此时JVM
会把A线程
存到共享资源对象的锁池中,A线程
进入阻塞状态;IO
请求时,该线程会进入阻塞状态;4.等待状态(waiting
):运行中的线程调用了wait方法
(无参数的wait方法
),而后JVM
会把该线程储存到共享资源的对象等待池中,该线程进入等待状态;处于该状态中的线程只能被其余线程唤醒;
5.计时等待状态(timed waiting
):运行中的线程调用了带参数的wait方法
或者sleep方法
,此状态下的线程不会释放同步锁/同步监听器,如下几种状况都会进入计时等待状态:
wait(long time)
方法,JVM
会把当前线程存在共享资源对象等待池中,线程进入计时等待状态;sleep(long time)
方法,该线程进入计时等待状态;6.终止状态(terminated
):也能够称为死亡状态,表示线程终止,它的生命走到了尽头;线程一旦终止,就不能再重启启动,不然会发生IllegalThreadStateException
;有如下几种状况线程会进入终止状态:
run方法
而退出,寿终正寝,属于正常死亡;线程休眠:让运行中的的线程暂停一段时间,进入计时等待状态。
方法:static void sleep(long millis)
调用sleep
后,当前线程放弃CPU时间片
,进入计时等待状态,在指定时间段以内,调用sleep
的线程不会得到执行的机会,此状态下的线程不会释放同步锁/同步监听器;
该方法更多的用于模拟网络延迟,让多线程并发访问同一个资源的错误效果更明显;也有让程序的执行便于观察的调用:
public static void main(String[] args) { for (int i = 5; i > 0; i-- ) { System.out.println("还剩 " + i + " 秒"); Thread.sleep(1000); } System.out.println("时间到"); }
联合线程
线程的 join方法
表示一个线程等待另外一个线程完成后才执行;join方法
被调用以后,调用join方法
的线程对象所在的线程处于阻塞状态,调用join方法
的线程对象进入运行状态。之因此把这种方式称为联合线程,是由于经过join方法
把当前线程和当前线程所在的线程联合成一个线程。
public class JoinThreadDemo { public static void main(String []args) throws Exception { System.out.println("开始"); JoinThread join = new JoinThread(); for (int i = 0; i < 50; i++) { System.out.println("i : " + i); if ( i == 10) { join.start(); } if (i == 20) { join.join(); } } System.out.println("结束"); } } class JoinThread extends Thread { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println("join : " + i); } } }
运行结果打印以下:
能够看到,当i = 20
时,join线程
对象开始执行,主线程(主函数)进入阻塞状态,暂停执行;join线程
对象运行完成后,主线程(主函数)才从新开始执行。那为啥i=10
时,join线程
对象没有执行呢?缘由是虽然join线程
对象调用了start方法
,但还未得到JVM
调度,因此没有执行。
后台线程
后台线程,在后台运行的线程,其目的是为其余线程提供服务,也称为“守护线程"。JVM
的垃圾回收线程就是典型的后台线程。
在Java
中,开发者们经过代码建立的线程默认都是前台线程,若是想要转为后台线程能够经过调用 setDaemon(true)
来实现,该方法必须在start方法
以前调用,不然会触发 IllegalThreadStateException
异常,由于线程一旦启动,就没法对其作修改了。
由前台线程建立的新线程除非特别设置,不然都是前台线程,同理,后台线程建立的新线程也是后台线程。如果不知道某个线程是前台线程仍是后台线程,可经过线程对象调用 isDaemon()
方法来判断。
若全部的前台线程都死亡,后台线程自动死亡,如果前台线程没有结束,后台线程是不会结束的。
线程优先级
每一个线程都有优先级,优先级的高低只与线程得到执行机会的次数多少有关,并不是是线程优先级越高的就必定先执行,由于哪一个线程的先运行取决于CPU
的调度,没法经过代码控制。
在Java
中,支持了从1 - 10
的10
个优先级,1
是最低优先级,10
是最高优先级,默认优先级是5
;jdk
文档中的线程优先级以下图所示:
MAX_PRIORITY=10
,最高优先级MIN_PRIORITY=1
,最低优先级NORM_PRIORITY=5
,默认优先级Java
在Thread
类中提供了获取、设置线程优先级的方法:
int getPriority()
:返回线程的优先级。
void setPriority(int newPriority)
: 设置线程的优先级。
每一个线程在建立时都有默认优先级,主线程默认优先级为5
,若是A线程建立了B线程
,那么B线程
和A线程
具备相同优先级;虽然Java
中可设置的优先级有10
个,但不一样的操做系统支持的线程优先级不一样的,windows
支持的,linux
不见得支持;因此,通常状况下,不建议自定义,建议使用上述Thread
类中提供的三个优先级,由于这三个优先级各个操做系统均支持。
线程礼让
yield
方法:表示当前线程对象提示调度器本身愿意让出CPU时间片
,可是调度器却不必定会采纳,由于调度器一样也有选择是否采纳的自由,他能够选择忽略该提示。
调用该方法以后,线程对象进入就绪状态,因此彻底有可能出现某个线程调用了yield()
以后,线程调度器又把它调度出来从新执行。
在开发中不多会使用到该方法,该方法主要用于调试或者测试,好比在多线程竞争条件下,让错误重现现象或者更加明显。
sleep方法
和yield方法
的区别:
CPU时间片
,把运行的机会给其余线程;sleep方法
会给其余线程运行机会,可是并不会在乎其余线程的优先级;而yield方法
只会给相同优先级或者更高优先级的线程运行的机会;sleep方法
后,线程进入计时等待状态,而调用yield方法
后,线程进入就绪状态;
线程组
在Java
中,ThreadGroup
类表示线程组,能够对属于同组的线程进行集中管理,在建立线程对象时,能够经过构造器指定其所属的线程组。
Thread(ThreadGroup group,String name);
若是A线程
建立了B线程
,若是没有设置B线程
的分组,那么B线程
会默认加入到A线程
的线程组;一旦线程加入某个线程组,该线程就会一直存在于该线程组中直至该线程死亡,也就是说一个线程只能有存在于一个线程组中,在一个线程的整个生命周期中,线程组一经设定,便不能中途修改。当Java
程序运行时,JVM
会建立名为main
的线程组,在默认状况下,全部的线程都归属于该改线程组下。
在JDK的java.util包中提供了Timer类,使用此类能够定时执行特定的任务;
其中有几个经常使用的方法:
// 将指定的任务(task)安排在指定的时间(time)执行 void schedule(TimerTask task, Date time) // 从指定的时间(firstTime)开始,按照某一周期(period),重复执行定时任务(task) void schedule(TimerTask task, Date firstTime, long period) // 指定的任务在(task)必定的延时(delay)后执行 void schedule(TimerTask task, long delay) // 指定的任务(task),在必定的延时(delay)后,按必定周期(period)重复执行 void schedule(TimerTask task, long delay, long period)
TimerTask类表示定时器执行的某一项任务;
经过jdk
文档中的描述,不难发现,TimerTask
类其实也是一个线程,具备线程的属性和操做,因此经过前几篇文章的介绍,这个类的使用已经很熟悉了。就再也不这里赘述了。
线程死锁, 当A线程
在等待由B线程
持有的锁时,而B线程
也在等待A线程
持有的锁,此时,这种线程现象称为线程死锁;因为JVM
不检测也不试图避免这种状况的发生,因此程序员必需要避免死锁的发生。
多线程通讯的时候很容易形成死锁,线程死锁没法解决,只能避免; 当多个线程都要访问共享的资源A、B、C
时,要保证每个线程都按照相同的顺序去访问他们,好比都先访问A
,而后是B
,最后才是C
。
Java 的Thread类存在一些因死锁被废弃过期的方法:
CPU时间片
,暂停运行;由上述两个方法可能致使的的死锁状况:
假设有A、B
两个线程,首先A线程
得到对象锁,正在执行一个同步方法,若是B线程
调用A线程
的suspend方法
,此时A线程
会暂停运行,并放弃CPU时间片
,可是并不会释放拥有的锁,从而致使A、B
两个线程都处于等待中;B
在等待A
释放锁,而A
已暂停,没办法释放锁;这样就会出现不管A、B
哪一个线程都不能得到锁。
哲学家就餐问题
哲学家就餐的问题也是一个描述死锁很好的例子,如下是问题描述(内容来源于百度百科):
假设有五位哲学家围坐在一张圆形餐桌旁,作如下两件事情之一:吃饭,或者思考。吃东西的时候,他们就中止思考,思考的时候也中止吃东西。餐桌中间有一大碗意大利面(也能够是其余的食物,好比:米饭,由于吃米饭必须用两根筷子),每两个哲学家之间有一只餐叉。由于用一只餐叉很难吃到意大利面,因此假设哲学家必须用两只餐叉吃东西,且他们只能使用本身左右手两边的那两只餐叉。
哲学家历来不交谈,这就颇有可能产生死锁,出现:每一个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反),永远都吃不到东西,最后饿死。即便没有死锁,也颇有可能耗尽服务器资源。
假设规定当哲学家等待另外一只餐叉超过五分钟后就放下本身手里的那一只餐叉,而且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但又会产生新的问题:若是五位哲学家在彻底相同的时刻进入餐厅,并同时拿起左边或者右边的餐叉,那么这些哲学家就会同时等待五分钟,同时放下手中的餐叉,又再等五分钟,哲学家任然会饿死。
在实际的计算机问题中,缺少餐叉能够类比为缺少共享资源。一种经常使用的计算机技术是资源加锁,保证在某个时刻,资源只能被一个程序或一段代码访问;当一个程序想要使用的资源已经被另外一个程序锁定,它就等待资源解锁。可是当多个程序涉及到加锁的资源时,在某些状况下仍然可能发生死锁。例如,某个程序须要访问两个文件,访问了其中一个文件,另一个文件被其余的线程锁定,这两个程序都在等待对方解锁另外一个文件,但这永远不会发生。
因此在Java 多线程开发中,尽可能避免死锁问题,由于发生这样的问题真的很头疼。尽可能多熟悉,多实践多线程中的理论和操做,从一次次的成功案例中体会Java 多线程设计的魅力。
完结。老夫虽不正经,但老夫一身的才华!关注我,获取更多编程科技知识。