Java多线程(四)- 线程有哪些状态?分别表示什么意思?

版权声明:写文章辛苦,请不要复制粘贴,如果要,请注明来处 https://blog.csdn.net/u012627861/article/details/82904603

线程的状态在面试中经常被问起,以至于八九成的程序员都知道线程的几种状态,但每一种状态是什么意思,对于什么情况会进入什么状态,不一定都能拿捏的准(这么说并不表示我一定拿捏的准)。所以在这里我准备多花点时间去探讨一下。
线程的状态有以下几种:

  • NEW:新建、初始化
  • RUNNABLE:可运行状态
  • BLOCKED:阻塞中
  • WAITING:无限等待中
  • TIMED_WAITING:有时间限制的等待中
  • TERMINATED:结束、死亡状态
    就我这种不是很聪明的人而言,无论你用中文还是英文来说线程的状态,我始终都不会理解得很透彻,云里雾里。
    于是产生下列问题:
    a. 不看书和博客,从哪里可以知道线程的这几种状态?
    b. 书上和博客关于状态的描述,我们怎么知道他们说的就是对的呢?
    c. 怎么确认某个线程进入了某个状态呢?
    对于第一个问题,你可以看看Java API,看到线程相关的类中有一个Thread.State类,上面有注释。也可以去翻Thread的源码注释。
    对于第二个问题,你看完以后你就知道是对的还是错的了。事实证明,我们需要多阅读API和源码,才能走在前线。
    对于第三个问题,请看下面的代码
public static void main(String[] args){
	Thread t = new Thread();
	System.out.println(t.getState());
}

上面的代码运行结果为

NEW

可以知道,线程对象提供State对象用于反应线程的状态,我们可以获取状态名称来知道线程当前处于什么状态。
那么什么情况进入什么状态?这些状态代表着什么意思?看下面对各种状态的代码实现:

第一种状态:NEW,我们可以理解为线程处于新建、等待启动的状态,这种状态下,线程就仅仅是一个对象,就像一个杯具摆在那里,你不去用它,它什么也不是。当我们用new关键字构建了一个线程对象,就意味着线程处于NEW状态
public static void main(String[] args){
	Thread t = new Thread();
	System.out.println(t.getState().name());
}

构建一个对象,线程处于NEW状态。不信你可以试试。

第二种状态:RUNNABLE,中文意思是可运行的,意思就是说我们这个线程可以被CPU执行了,但CPU未必就正在执行。当线程调用start方法,就意味着线程进入了RUNNABLE状态(但不仅限于调用start方法,线程在BLOCKED、WAIT,TIMED_WAITING状态下都可以变为RUNNABLE状态)
public static void main(String[] args){
	Thread t = new Thread();
	t.start();
	System.out.println(t.getState().name());
}

上面的代码运行结果为

RUNNABLE

需要注意的是,这里输出的结果未必就是RUNNABLE,也有可能是TERMINATED,为什么?理论上可以分析一下,首先需要提醒的是,main方法也是要被CPU执行的,这也是一个线程——main线程。当我们执行t.start()后,t 线程进入RUNNABLE状态,如果在调用start()后CPU立马切换到 t 线程直到把 t 线程处理完毕后再切换到main线程,那么输出的结果就是TERMINATED。否则输出的就是RUNNABLE了。但是你可以尝试一下,你运行很多次,估计都是输出RUNNABLE,这又是为什么,后面再讲。

还有一个问题,我能不能重复调用start方法呢?答案是能,但是,会报错!!!,为什么?

  • 从代码层面上来说,如果你连续调用两次start,会抛出java.lang.IllegalThreadStateException。在Thread的源代码中,有一个volatile的修饰的变量threadStatus,默认为0,当调用start方法时会判断该值是否为0,如果不是,那么就会抛出java.lang.IllegalThreadStateException,如果是,那么会调用start0这个native方法。而start0这个native方法,我猜测会修改threadStatus的值,使得线程调用start方法后threadStatus发生改变,从而使得再次调用start时抛出异常。
  • 从设计层面上来说,首先我们了解一下Illegal是什么意思,意思为非法的,违反规则的。也就是说多线程的设计里头有一套规则,这套规则里头肯定大致说了一下状态的变更问题,比如TERMINATED能否变成NEW,没同步的对象能否进入WAIT状态等。一些不合理和没必要的切换,都视为IllegalThreadState,从而抛出java.lang.IllegalThreadStateException。我们第一次调用start方法,线程状态从NEW变更为RUNNABLE,第二次调用是不是应该从RUNNABLE变更为RUNNABLE,这就属于没必要(也可以视为不合理)的变更。底层实现完全可以避免这种情况,可以在start方法里头判断线程状态,如果线程状态不是NEW,就忽略掉来处理这个问题,但Thread源码中他们为什么没有这么去做,自己慢慢体会。
第三种状态:BLOCKED,中文翻译过来为阻塞的意思,只要线程去获取别的线程正在使用的资源,就会进入BLOCKED状态

举几个生活中的小例子来阐述一下阻塞是什么意思。比如说我们去打水,饮水机被别人占用了,我们就只能等;比如说我们去蹲坑,坑被别人占用了,我们就得等,当然这里说的是只有一个坑的情况下;再比如说我们去买票,窗口被别人占用了,我们就得等,当然还有军人优先窗口,这种情况后面讲。

技术源于生活而高于生活,当我们不断加强技术的学习,会发现在这些技术的实现过程和原理在生活中都可以找到影子,自己慢慢体会。

下面的代码将进入BLOCKED状态

// 这是一个同步方法,理想状态下执行完这个同步方法需要占用CPU 10毫秒的时间,但实际上大于10毫秒,不信你可以稍微去分析一下这几行代码。
public synchronized static void syncMethod(String threadName){
	System.out.println(threadName + "访问同步方法");
	long startTime = System.currentTimeMillis();
	while(System.currentTimeMillis() - startTime < 10){}
}

// main方法
public static void main(String[] args){
	// 这是即将被阻塞的线程,我称其为"blockThread",在这里调用了syncMethod同步方法.
	// 如果syncMethod方法被别的线程使用,就会进入阻塞状态
	final Thread blockThread = new Thread(new Runnable() {
		@Override
		public void run() {
			syncMethod("blockThread");
		}
	});
	
	// 这是一个不断的输出blockThread状态的线程,我称其为"listener"
	// 目的是让我们知道blockThread状态的变更情况,在没有这个线程的情况下,我们不能实时获取blockThread线程的状态,不信你可以去分析实践一下
	// 当blockThread被CPU处理完以后,listen线程也将结束。
	new Thread(new Runnable() {
		
		public void run() {
			while(true){
				String stateName = blockThread.getState().name();
				System.out.println(stateName);
				// 如果blockThread线程进入TERMINATED状态,则结束循环
				if(stateName.equals("TERMINATED")){
					break;
				}
			}
		}
	}).start();
	
	// 这是一个用于阻塞blockThread线程执行的线程,我称之为门槛线程doorsill。
	// 这里将占用CPU多少时间有兴趣的同学可以去分析一下,我反正有点晕。
	new Thread(new Runnable(){
		@Override
		public void run() {
			long startTime = System.currentTimeMillis();
			while(System.currentTimeMillis() - startTime < 20){
				syncMethod("doorsill"); // 占用10毫秒
				t2 = System.currentTimeMillis();
			}
		}
	}).start();
	
	// 启动 blockThread 线程
	blockThread.start();
}

以上代码输出什么?如果你给出的是一个答案,那么肯定是错了或者对了一部分。以上代码输出理论上来说不只一种,我们来分析一下。
首先我们构建了blockThread线程,然后启动了listener线程。下面就要开始分情况了。

  • 情况一:listener启动后CPU继续执行,此时CPU继续处理main线程,执行的是doorsill.start()启动了门槛线程。启动完成后假设切换到listener线程,那么此时得到blockThread状态为NEW。
  • 情况二:listener启动后CPU继续执行,此时CPU切换到了listener线程,那么此时得到blockThread状态也为NEW。
  • 情况三:listener启动后CPU继续执行,此时CPU继续处理main线程,执行了doorsill.start(),继续执行blockThread.start(),然后切换到listener线程,那么此时得到的blockThread便是RUNNABLE。
  • 情况四:listener启动后CPU继续执行,此时CPU继续处理main线程,执行了doorsill.start(),继续执行blockThread.start(),然后切换到doorsill线程调用了同步方法syncMethod,syncMethod没有被执行完毕,CPU就切换到了blockThread线程,blockThread也调用了syncMethod方法,于是blockThread就进入了BLCOKED状态,此时CPU切换到listener线程,那么得到blockThread状态就为BLCOKED。

我这里贴出我运行的结果,因为输出的行数过多,重复的部分我用“…“来代替。

NEW

doorsill访问同步方法
NEW

RUNNABLE

BLOCKED

doorsill访问同步方法
BLOCKED

blockThread访问同步方法
RUNNABLE

TERMINATED

分析一下这种情况

  • 首先输出了blockThread状态为NEW,我相信你知道为什么了,如果不知道,看看上面的四种情况。
  • 然后输出了“doorsill访问同步方法“,说明门槛doorsill线程被CPU处理了一下,但不一定处理完了。可以肯定的是,CPU处理的时候一定执行到了调用syncMethod步骤,并且执行了syncMethod同步方法,输出了“doorsill访问同步方法“,但不意味着syncMethod方法被执行完毕。
  • 然后又输出了blockThread状态为NEW,这说明在listener线程执行输出的这个动作的时候,blockThread还是没有被启动。
  • 紧接着输出了大量的RUNNABLE,这说明CPU切换到了main线程,将blockThread.start()执行完毕了,让blockThread进入到了RUNNABLE状态,此后切换到listener线程,执行了输出操作。这途中并不是一直执行listener线程,在整个期间线程不断的被切换,但在这个阶段,只要blockThread还没有被doorsill线程阻塞,它就处于RUNNABLE状态。
  • 然后输出了blockThread状态为BLOCKED,从第二个步骤我们知道CPU已经执行过doorsill并且调用了syncMethod,很明显,syncMethod正在被doorsill使用。当CPU执行blockThread线程时也去调用了syncMethod,那么此时blockThread就进入了BLOCKED。listener也就输出了BLOCKED。
  • 然后又输出了“doorsill访问同步方法“,说明syncMethod在释放锁以后doorsill再次获取到了执行权。
  • 紧接着又输出了大量的BLOCKED,这个我相信不用我解释了。
  • 最后输出了“blockThread访问同步方法“,这说明doorsill用完syncMethod后,blockThread获取到了执行权,此时状态从BLOCKED变更为RUNNABLE,在执行syncMethod的10毫秒里面,listener也不断被CPU间断的执行,也就输出了后面大量的RUNNABLE。blockThread如愿以偿执行完了syncMethod后,进入了TERMINATED状态,宣布线程结束。

到这里,BLOCKED状态就算写完了。

第四种状态:WAITING,线程处于无限等待中,等待什么?等待共享的资源允许被使用或等待其它线程执行完毕。当我们调用wait方法,线程则进入了WAITING状态,请注意,wait和notify都是Object下的方法

理解WAITING状态,首先要搞彻底搞清楚阻塞,阻塞我们前面举了个买票的例子,我们排着队去买票,前面的人买了,我们就可以买了。但是等待的情况就是,你后面有个人拿着枪指着你不准你买票,比如说我。我要你让开,只要我的枪不从你身上拿开,你就不敢动,此时你处于等待状态,等待我允许你用窗口。当我把枪从你身上拿开,我站在你前面,我占用窗口买票,你又进入了阻塞状态,等我买完了,你就可以买了,此时你进入了RUNNABLE状态。等你买完票,整个买票过程结束了,你便进入了TERMINATED状态。
在这个例子中,窗口就是共享的资源,为什么说线程WAITING状态表示的是等待共享资源允许被使用,而不是说等待共享资源被释放。是因为等待共享资源被释放是一种阻塞,窗口你可以用,但是需要等别人用完。而等待共享资源允许被使用是窗口不允许你用,你得等我允许你用你才能用。我可能一分钟后允许你用,也可能一辈子都不允许你用。所以WAITING是一个无限等待的状态。下面列出“等待共享的资源允许被使用”和"等待其它线程执行完毕 "两种情况的代码实现。

  • 通过调用wait,让线程进入WAITING状态。通过调用notifynotifyAll来允许其它线程使用共享资源,但请务必记住,waitnotify都是Object的方法,任何对象都有这两个方法。并且这两个方法只有在同步块中才能使用。为什么只能在同步块中使用,代码后面有说明。
public static void main(String[] args) throws Exception{
	final Object window = new Object();
			
	// 这是即将处于无限等待状态的线程,称之为waitThread
	final Thread waitThread = new Thread(new Runnable() {
		public void run() {
			synchronized (window) {
				System.out.println("waitThread获得了window对象同步锁,可以使用window,但此后调用window.wait()让出了window对象同步锁和CPU");
				try {
					// 释放window锁,并且让出CPU,让别的线程可以使用
					window.wait();
					System.out.println("waitThread第二次获得了window对象同步锁");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	});
	waitThread.start();
	
	// 这是一个用于释放window对象使用权的线程,称之为manThread
	new Thread(new Runnable() {
		
		public void run() {
			synchronized (window) {
				System.out.println("manThread获得了window对象同步锁");
				System.out.println("waitThread状态:" + waitThread.getState().name());
				// 释放window锁,但依然占用CPU
				window.notify();
				System.out.println("manThread调用了window.notify, 释放了window使用权");
				System.out.println("waitThread状态:" + waitThread.getState().name());
				
				// 再占用window一秒钟
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				
				System.out.println("manThread释放了window对象同步锁");
			}
		}
	}).start();
}

上面的代码可以看到使用了同步块,用同步块去锁住window对象。在同步块中调用wait和notify方法。不用同步块行不行?我又是怎么知道要加上同步块的?
对于第一个问题,首先我们要明白,如果是非同步块,window对象就不存在锁,那么任何线程都可以直接使用,也就不需要wait操作了。那么非同步块为什么不能直接调用notify呢?这个问题,我有点晕,不想再往里头想了,有兴趣的同学可以思考一下。简而言之,他们的出现就是为了解决在同步块的情况下,如何切换同步块执行,从而满足我们的业务需求,保证数据的准确性。试想一下如果没有wait和notify,我们如何切换同步块执行
对于第二个问题,你可以去看看Object的wait和notify方法,上面有注释。JavaAPI中也有写到。

分析以上代码,看看wait和notify是怎样配合使用达到同步块的切换的。没有看明白的话,可以再举一个稍微好点的例子,我们开启了十个线程去处理数据(称为P线程),开启第十一个线程去获取数据(称为G线程),如果此时CPU率先执行P线程,那么P线程拿到的数据是空的,此时P线程调用数据的wait方法让出CPU和数据的同步锁进入WAITING状态。那么问题来了,P线程如何知道数据已经被填充了?这就是notifynotifyAll的作用,当G线程成功拿到数据后并填充后,会执行notify或notifyAll方法来通知其它线程(数据被填充,可以使用了)。此时其它线程重新进入RUNNABLE或BLOCKED状态。这样P线程再次获取到数据时,则是G线程获取到的数据。

这里的wait和notify都是Object的方法,所以这使得我们理解WAITING状态会出现误区。
试想一下Object obj = newObject(); obj.wait();
这样的代码会不会有些无厘头,obj要去wait什么?不是线程wait么?所以这里我们要特别注意,调用obj.wait()后,指的是当前线程处于了等待状态,而非obj等待,obj只是一个数据存储对象而已。

  • 通过join方法,让主线程等待其它线程执行完毕,使主线程进入WAITING状态
public static void main(String[] args){
	// 建一个list,视为共享资源
	final List<String> list = new ArrayList<String>();
	
	// 创建一个主线程并开启十个子线程往list中写入数据
	final Thread mainThread = new Thread(new Runnable() {
		
		public void run() {
			// 循环创建线程并启动
			for(int i = 0; i < 10; i++) {
				final int j = i + 1;
				Thread t1 = new Thread(new Runnable() {
					
					public void run() {
						try {
							// 计算睡眠时间,让最先创建的线程睡眠时间最长。
							// 这是为了探讨join方法是否使得线程有处理顺序,如果有顺序,那么输出的结果应该是递增的
							double time = (1.0 / j) * 100;
							long intTime = Math.round(time);
							System.out.println("线程" + j + "睡眠时间: " + intTime + "毫秒");
							Thread.sleep(intTime);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						list.add("thread-" + j);
					}
				});
				// 先start,后join
				t1.start();
				try {
					t1.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(list);
		}
	});
	t.start();
}

上面的代码运行的结果是

线程1睡眠时间: 100毫秒
线程2睡眠时间: 50毫秒
线程3睡眠时间: 33毫秒
线程4睡眠时间: 25毫秒
线程5睡眠时间: 20毫秒
线程6睡眠时间: 17毫秒
线程7睡眠时间: 14毫秒
线程8睡眠时间: 13毫秒
线程9睡眠时间: 11毫秒
线程10睡眠时间: 10毫秒
[thread-1, thread-2, thread-3, thread-4, thread-5, thread-6, thread-7, thread-8, thread-9, thread-10]

在这里先让我们了解一下join,join表示将当前线程加入主线程的执行队列里面去。主线程需要等待执行队列都执行完毕,才能继续执行。并且是严格按照队列的先后顺序执行的,在代码中我们有对这方面的验证。所以,通过join方法会使得多个线程同步去运行,也就是说,在这里,join方法使得子线程失去了提高性能和效率的作用。
上面的代码中我们并没有查看主线程的状态,但可以告诉你,主线程在十个线程没有运行完的情况下都是WAITING。你可以采用前面的例子开启一个listener线程查看主线程状态的变更。
对于join,下面单独进行研究和总结。

第五种状态:TIMED_WAITING,非无限等待,而是有确切的等待时间,当线程等待了指定的时间,会回归到RUNNABLE或BLOCKED状态。当我们调用sleep(long millis)或者wait(long timeout)后进入TIMED_WAITING**

下面的代码将使得 t 线程和 main 线程进入TIMED_WAITING状态

public static void main(String[] args){
	// t 线程
	final Thread t = new Thread(new Runnable() {
		public void run() {
			try {
				Thread.sleep(1000); // 调用sleep,使线程进入TIMED_WATING状态
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	});
	t.start();
	
	// listener线程,不断输出 t 线程的状态
	new Thread(new Runnable() {
		
		@Override
		public void run() {
			while(!t.getState().name().equals("TERMINATED")){
				System.out.println(t.getState().name());
			}
		}
	}).start();
	
	Thread.sleep(1000); // 调用sleep,使 main 线程进入TIMED_WAITING状态
}

上面的代码输出大量的TIMED_WAITING。需要注意的是,sleep方法是一个静态方法,可能对你来说没啥注意的,但很长一段时间,我的记忆里都是一个实例方法。当我们调用sleep方法,意味着当前线程进入了TIMED_WAITING状态。比如说上面 t 线程调用了sleep使得 t 线程进入了TIMED_WAITING状态。main 方法最后也调用了sleep方法使得main线程进入了TIMED_WAITING状态。不信你可以亲自去试试。

前面有说过,调用wait(long timeout)也会进入TIMED_WAITING状态,代码就不贴了。我相信如果你从头到尾读到这,写一个这样的例子肯定是没问题的,后面还有很多东西要写。这里需要再次强调,wait和notify是Object的方法,是建立在同步块的基础上的,是为了让同步块也可以互相切换执行的一种实现。

既然wait和sleep都会使线程进入TIMED_WAITING状态,那么它们有什么区别呢?
wait跟sleep最大的区别,就是wait是基于同步块的,调用wait会释放资源同步锁和让出CPU,可以让别的线程去使用共享资源和CPU,而sleep在同步块的情况下,会继续占用CPU和资源同步锁。

第六种状态:TERMINATED,终止、结束,你还可以生动的理解为“死亡”,当线程执行完毕,则会进入该状态

这种状态不用多说,理解的浅显点,就是run方法执行完了,就进入了这个状态。这个状态就不写代码了,才疏学浅,不知道该如何去写。

(完)

猜你喜欢

转载自blog.csdn.net/u012627861/article/details/82904603