JAVA多线程:守护线程 setDaemon全方位剖析| 守护线程是线程吗 |thread.isAlive()反思(五)

前言

本文目的,通过短小精悍的实例,让你在最短时间,全面揭晓 thread.setDaemon(true)守护线程的使用,及其使用场景。一看就懂,一学就会!

概述

守护线程的作用

用来让这里暂称之为子线程随着调用它的主线程这里暂称之为main方法的结束而结束,不管该线程任务是否圆满完成,只要调用它的主线程结束了,它(子线程)就跟随着结束。

拿老板和员工举例:

给员工设置了守护线程,就意味着只要老板(主线程)“休假”(工作完成,没事可做了),员工(子线程)也跟着休假不用上班,无论员工手头的活干完没有;

没给员工设置守护线程,就意味着,即使老板“休假”了,员工依然要在后台继续完成它可能永远都完不成的工作!

一、代码示例

/**
 * 类描述:了解守护线程的用法
 * 守护线程作用:为了确保调用它的主方法结束的同时子线程也一并结束(并不一定是进入死亡,有可能仅仅进入了等待),就需要设置为守护线程。否则,可能会产出主线程结束后,子线程依然在继续运行
 * 例如:thread1.setDaemon(true);默认是false,没有开启守护线程
 */
public class ThreadDaemon {
	public static void main(String[] args) {
		Thread thread = new Thread(() -> {
			while (true) {
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("----我是子线程,每隔一秒打印一次,就是玩儿-----");
			}
		});
		thread.setDaemon(true);//true 确保主方法结束时候,子线程随之结束(默认是false)
		thread.start();
		try {
			TimeUnit.SECONDS.sleep(3);//让主线程休息一会儿,给子线程充足的执行时间,否则可能造成因主线程执行速度过快,主线程结束了子线程还没来得及执行
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("主线程已结束");
		//只有new(新创建)和terminated(死亡)状态的线程下的isAlive()返回值才是false
		//其他waiting、blocked、runable、running状态下,isAlive()返回值都是true
		System.out.println("子线程状态:"+thread.getState());
		System.out.println("是否依然存活:"+thread.isAlive());
		System.out.println("是否是守护线程:"+thread.isDaemon());
	}
}

二、运行结果

1. thread.setDaemon(true)时的运行结果

thread.setDaemon(true);//true 确保主方法结束时候,子线程随之结束(默认是false)

可以看到,main方法的主线程结束的同时,子线程就跟着屁颠的结束了(没有嚷嚷着说,我是无限for循环,我就不停) ,这个也就是thread.setDaemon(true)的意义所在。

1.需要注意,子线程休息的状态可能是TIMED_WAITING、RUNNING、BLOCKED三种状态,具体进入那种状态是有jvm来决定的

 

 经过反复实验,如上图所示:

设置守护线程后,线程随着主线程运行结束后,不是每次都会进入wait等待队列(包含:BLOCKED、TIMED_WAITING)有时候也会直接进入runnable就绪队列

注:等待(wait)队列就是阻塞(blocked)队列,没有被CPU直接执行的权利,只有在runnable就绪队列就绪队列才会有被CPU执行的权利(貌似在说我准备好了,CPU你来执行我吧)。

2.设置为守护线程可能存在的问题

设置守护线程为true后,子线程(内部有短时间难以干完的活)可能会因主线程的结束而进入等待(大部分情况下,子线程活如果干完了,会直接进入terminated死亡状态可能会直接导致子线程自己的活儿没干完,而被暂停!(有时候它的活可能永远干不完,不能休息进入waiting,本例的for死循环就是这个意思)。

简而言之:被设置守护的子线程,如果子线程内部是简单的工作,不会造成影响,直接会随着主线程的结束而彻底结束,如果子线程内部是死循环,子线程会随着主线程的结束而暂停,进入等待队列或者就绪队列,但是有可能剩余的工作就无法再继续。 

2. thread.setDaemon(false)时的运行结果

//thread.setDaemon(true);//true 确保主方法结束时候,子线程随之结束(默认是false)

主线程main方法在休息3秒后就宣告结束了,但是,子线程自己依然在不知疲倦的一直在执行自己的业务代码 。

这个就是默认情况下,thread多线程默认thread.setDaemon(false)的样子,当然之所以是这样因为子线程run方法内部是死循环,如果是简单的一行打印,则子线程直接跟着主线程彻底消亡,不会持续打印

3.关于thread.isAlive()返回值的一点反思

当子线程被设置为守护线程时,子线程随着主线程的结束而结束,子线程的isAlived()不应该是false吗?

通过thread.getState(),得到的结果是TIMED_WAITING,可以看出,子线程运行结束后,它并没有真正意义上的死亡,仅仅是进入了等待状态(本例比较特殊,是死循环,其他正常情况下,子线程会进入terminated死亡状态)

1、线程的isAlived(),只有在new新建状态和terminated死亡状态的返回值才是false;

2、线程的isAlived(),在waiting、blocked、runnable、running状态下,返回值都是true。 

1.什么时候线程的state状态会变成TERMINATEDisAlive()返回值变为false

看完上面的示例,你可能会有疑惑,在本例中,无论设置守护线程与否,thread.isAlive()调用的最终结果一直都是ture

心里一万个草拟马!!!这是为什么呢?下面一起来彻底揭秘一下。

public class ThreadStateDemo implements Runnable{
	@Override
	public void run() {
		try {
			TimeUnit.SECONDS.sleep(1);
			System.out.println("哈哈哈");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		ThreadStateDemo demo=new ThreadStateDemo();
		Thread t=new Thread(demo,"demo");
		System.out.println(t.getState());
		t.start();
		System.out.println(t.getState());
		TimeUnit.SECONDS.sleep(3);
		System.out.println(t.getState());
		System.out.println(t.isAlive());
	}
}

运行结果如下:

反思

第一个示例中,之所以无论设置守护线程与否,子线程的最终isAlive()返回值都是true,意味着线程并没有消亡,依然是活的。

这究竟是什么原因呢?因为第一个示例中的run方法内部是一个死循环,会无休止的每隔一秒冒个泡,打印一句话。

所以,设置为守护线程后,它虽然暂时随着主线程歇业了,但是子线程并不死心,依然惦记着自己的死循环,自己的任务,所以只是暂时进入了假死,进入TIMED_WAITING或者RUNNABLE状态,并没有进入TERMINATED死亡状态。

第二个示例中,无论设置守护线程与否,子线程的最终归宿的是TERMINATED状态,这又是为什么呢?是因为子线程内部的run方法,任务完成了,没有遗留工作需要完成,也没有什么死循环,所以生命就此就截止了,进入了TERMINATED状态。

至此,心中的所有疑惑解开!! 

三、thread.setDaemon(true)的业务场景

1、当且仅当你希望调用它的线程可能是主线程或其他线程结束而结束,不在意子线程的任务是否圆满完成时,此时可以使用守护线程。

2、当你希望在调用它的线程结束时,被调用的子线程继续运行的时候,就不需要设置守护线程。

四、守护线程到底是不是一个线程

在学习过程中,发现有人在博文中说守护线程也是一个线程,甚至示例也存在问题,误认为守护线程会开启一个新线程,独自作为一个新线程而存在,这样是不严谨,甚至是错误的。

守护线程不要误认为它是一个线程,它的存在需要依附于一个已存在的线程(暂时叫他thread),然后给这个已存在的线程通过thread.setDaemon(true),把这个已存在的线程设置为守护线程。  

守护线程并不能脱离已有线程,单独存在,所以严格意义上,它并不是一个严格意义上的线程。

也就是说,人家本来就是一个线程,不能因为你把它设置为守护线程,或者没有设置为守护线程,它就是一个线程或者不是一个线程了,对吧!

总之,你理解就可以,在理解的基础上,你一定要继续说它是线程,我也不反驳,理解就好。

反例:如果守护线程是一个线程,那么给一个已存在的thread,设置为守护线程后,就应该多出一个线程,存在2个线程,对吧!继续看关于守护线程的实验

4.1 守护线程是否会开启新的线程示例

1.代码片段

public class ThreadActiveCountTest {

	public static void main(String[] args) throws InterruptedException {
		System.out.println("活跃线程的数量:"+Thread.activeCount());	//此时打印的1,是main方法的线程
		System.out.println(Thread.currentThread().getName());;
		
		Thread thread=new Thread();
		
		System.out.println("活跃线程的数量:"+Thread.activeCount());	//因为new的线程没有start,所以活跃的线程仍然是1
		
		thread.setDaemon(true);//该参数设置为true或false,并不影响活跃线程的数量,它仅影响主线程退出时,子线程是否跟随停止运行(进入等待或就绪队列)
		System.out.println("活跃线程的数量:"+Thread.activeCount());	//因为new的线程没有start,所以活跃的线程仍然是1
		thread.start();
		
		System.out.println("活跃线程的数量:"+Thread.activeCount());	//因为new的线程已start,所以活跃的线程变为2
		Thread.sleep(5*1000);//给足子线程充足的执行时间
		System.out.println("最终活跃线程的数量:"+Thread.activeCount());
	}
}

2.运行结果

经过反复实验,无论是把thread.setDaemon(true)还是thread.setDaemon(false),最终的运行结果,可以发现,一个线程无论是否开启守护线程,并不会增加线程的数量,所以守护线程不是一个真正意义上的线程它仅起到给一个已存在线程锦上添花的作用(把已存在线程,标记为守护线程)。 

五、 关于Thread.activeCount()个数的反思

你可能很好奇,为什么会问守护线程是线程吗?这样的傻逼问题。根源在下面这段代码!有人把它当做会开启新的线程,而写出错误的代码,导致代码不会执行,相信也有其他人会犯这个错误。故在此分享一下


/**
 * 关于死锁的一个测试
 * 如何避免:尽可能共用一把锁;或者两把锁不要混在一起使用,把两把锁lock/unlock分开写,不要混在一起
 */
public class ReentrantLockDeadLockTest {
	static Lock lock1 = new ReentrantLock();
	static Lock lock2 = new ReentrantLock();

	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
		Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
		thread1.start();
		thread2.start();
		
		Thread.sleep(5 * 1000);//主线程休息一会儿,给子线程充足的执行时间
        if (Thread.activeCount() >=4) {//注意:这个是4是错误的
            thread1.interrupt();//让thread1线程中断
        }
        Thread.sleep(5 * 1000);//主线程再休息一会儿,给子线程充足的阻断时间
        System.out.println("活跃线程的个数:"+Thread.activeCount());//最后打印活跃个数
	}

	static class DeadLockDemo implements Runnable {
		Lock lockA;
		Lock lockB;

		public DeadLockDemo(Lock lockA, Lock lockB) {
			this.lockA = lockA;
			this.lockB = lockB;
		}

		@Override
		public void run() {
			try {
				//lockA.lock();
				lockA.lockInterruptibly();
				System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockA + "\t 尝试获得:" + lockB);
				TimeUnit.SECONDS.sleep(2);
				//lockB.lock();
				lockB.lockInterruptibly();
				System.out.println(Thread.currentThread().getName() + "\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lockA.unlock();
				lockB.unlock();
				System.out.println(Thread.currentThread().getName() + "正常结束!");
			}
		}
	}
}

相信会有人,不知道Thread.activeCount()后面的数字,到底该怎么断定!!

先说答案,上面的代码是错误的Thread.activeCount()>=4,永远不成立,因为最多只有3个线程,永远也不会>=4,所以其中的thread1.interrupt()永远也不会执行,就会造成死锁。

这里判断的目的是判断是否还有子线程在运行,那么这里直接写>1就可以了,那个1就是main方法的活跃线程,除了main剩下的都是子线程了,所以应该是>1,而不是>=4。 

引申:如何避免死锁? 

1.尽可能共用一把锁;或者两把锁不要混在一起使用,把两把锁lock/unlock分开写,不要混在一起。

2.通过把lock.lock(),修改为lock.lockInterruptibly(),然后通过一定条件判断是否死锁,进而决定是否调用interrupt()来进行阻断。当然这样处理死锁,并不是特别好的方法,万一线程真的是执行了很久,而不是死锁了,如果贸然中断,可不是一个明智的处理方法。

3.ReentrantLock 提供了一个tryLock(参数) 方法,可以指定获取锁的等待时间。
tryLock()如果拿到锁就返回true,否则返回false,不会像lock那样无限等待。

if (!lockA.tryLock(2, TimeUnit.SECONDS)) {
    System.out.println(Thread.currentThread().getName() + " 正在等待锁......");
} else {
    System.out.println(Thread.currentThread().getName() + " 拿到了锁");
}

5.1 日常开发中,怎么断定Thread.activeCount()的个数呢?

通过上述案例,已经可以简单的判断活跃线程的个数了,下面继续深入探讨一下

首先main方法身算一个;

其次,每new一个Thread对象,并且让这个thread调用start()后,这个活跃个数就会增加1。如果一个for循环创建了100个线程,都开启了start(),那么activeCount()就会逐步增加到1+100。

如果这些新建的线程的run方法内部,很简单没有死循环,那么他们都会跟随这主线程main方法的运行结束,而消亡,最终activeCount()的数量,只剩下main方法的1

如果这些新建的线程run方法内部是死循环,则无论你是否给新建的线程设置守护线程,这些线程都不会消亡,最终activeCount()的数量,就是main方法的1+这些线程的总个数100。

预告:

活跃线程的数量和是否设置为守护线程无关,守护线程不会增加线程,乃至活跃线程的数量。最终活跃线程的多少,和锁运行子线程是否结束有关,换言之和子线程内部是否有大量的难以在很短内执行完毕的代码或者是否有死循环有直接关系。 

上示例,让示例说话吧!

5.2 示例1:run内部没有死循环,会随着主线程的结束而消亡

1.代码片段

public class ThreadActiveCountA extends Thread {
	@Override
	public void run() {
		try {
			TimeUnit.SECONDS.sleep(1);//为了防止打印过快,让其休息1秒打印一次
			System.out.println("我是测试类A,没有死循环,就是玩儿");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws InterruptedException {
		System.out.println("活跃线程的个数:" + Thread.activeCount());
		Thread thread = null;
		for (int i = 0; i < 5; i++) {
			thread = new ThreadActiveCountA();
			thread.setDaemon(true);
			thread.start();
			System.out.println("内部打印--活跃线程的个数:" + Thread.activeCount());
		}
		//注:这里也可以通过判断Thread.activeCount()个数,来让主线程Thread.yield();为了更好演示效果,使用下面的sleep()
		Thread.sleep(10 * 1000);//让主线程多休息会,保障子线程执行完毕
		System.out.println("活跃线程的个数:" + Thread.activeCount());//最终的个数只会是1,那就是主线程的
	}
}

2.运行结果

肉眼可见,随着子线程的start(),活跃个数也是随着逐个增加的! 

子线程执行完毕,进入死亡状态,最后仅仅剩下了主线程,活跃个数1

3.小节 

在本案例中,无论你在main方法中是否把new的线程,设置为thread.setDaemon(true)/thread.setDaemon(false)守护线程,如上图所示,都不会影响最终的打印结果,更不会因为设置了守护线程,而多出一些线程!

主线程1+for循环生成的5个线程,活跃线程个数的最高峰是1+(1+1+1+1+1)=6,而不是1+5+5=11,最终子线程game over 进入terminated死亡状态(不再活跃),所以只剩下了主线的1。

注:算式中,前一个5是main方法中new的5个子线程,后一个5是意淫的设置为守护线程中会分别多出5个守护线程(这种想法明细是错误的) 

在实际的开发中,往往不会这样new出多个Thread,而是使用线程池(ThreadPoolExecutor、ForkJoinPool、Executors、自定义线程池等)。 

5.3.示例2:run内部有死循环子线程不会随着主线程的结束而消亡

1.代码片段

import java.util.concurrent.TimeUnit;

public class ThreadActiveCountB extends Thread  {
	@Override
	public void run() {
		synchronized (this) {
			while (true) {
				try {
					TimeUnit.SECONDS.sleep(3);//为了防止打印过快,让其休息3秒打印一次
					System.out.println("我是测试类B,我的run方法是一个死的for循环,就是打印着玩儿");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		System.out.println("活跃线程的个数:" + Thread.activeCount());
		Thread thread = null;
		//在main中开启5个子线程
		for (int i = 0; i < 5; i++) {
			thread = new ThreadActiveCountB();//注意:这里是B类
			thread.setDaemon(true);//true设置为守护线程,默认是false
			thread.start();
			System.out.println("内部打印--活跃线程的个数:" + Thread.activeCount());
		}
		//注:这里也可以通过判断Thread.activeCount()个数,来让主线程Thread.yield();为了更好演示效果,使用下面的sleep()
		Thread.sleep(10 * 1000);//让主线程多休息会,保障子线程执行完毕
		System.out.println("活跃线程的个数:" + Thread.activeCount());//无论设置守护线程与否,最终打印结果都是1+5=6
	}
}

2.运行结果

因为内部有死的for循环,是否设置守护线程,并不影响最终线程的活跃个数,区别仅仅是死循环是否在主线程结束后,依然在持续打印。 

3.小节

在该案例中,线程类B的内部是一个死循环,无论在main方法中是否给子线程设置守护线程,最终活跃线程的个数依然是1+5=6,并没有出现1+5+5=11。

或许有人疑惑,为什么是6?

因为上面一开始在Thread.activeCount()>=4,引发的血案中,已经提及,设置守护线程只能让子线程随着主线程的停止而停止运行,决定子线程是否死亡的(是否在主线程结束后,依然是存活),并不是主线程,而是子线程所归属类中的run方法内部,是否有一个死循环或者难以在短时间内执行完毕的代码块。

如果存在“短时间内难以执行完毕的代码,包括死循环”无论是否设置为守护线程,无论主线程是否结束,其子线程都会处于alive状态,而不是死亡状态。

5.4 小节

经过一番论述+案例展示,发现是否设置守护线程,并不会影响原线程的个数,也就是说守护线程它仅仅是一个概念的存在,需要依附于已有线程,并不会因为是否设置守护线程,就会在已有线程基础上,多了或者少了线程。

至此,我只是说,守护线程不是严格意义上的线程,不会因为它的存在(是否设置守护线程)而开启新的线程。你能懂就好,你说他就是线程,我依然不反驳,理解、会用就好

至此,我想我应该把守护线程怎么用、是否是线程,说清楚了。相信此时的你已经会使用Thread.activeCount()了, 如有疑问一起留言探讨吧!

尾言

多线程的学习,就像一次修行,知识点甚多,想学好多线程不是一蹴而就的,需要耐心慢慢深耕,还好有我作伴,一起来探索吧!

附注

猜你还可能会以下内容感兴趣

1、JAVA多线程:synchronized理论和用法 | Lock和ReentrantLock Volatile 区别和联系(一)

2、JAVA多线程:yield/join/wait/notify/notifyAll等方法的作用(二)

3、JAVA多线程:狂抓!join()方法到底会不会释放锁,给你彻底介绍清楚(三)

4、JAVA多线程:sleep(0)、sleep(1)、sleep(1000)的区别(四) 

5、JAVA多线程:揭秘thread.setDaemon(true) | thread.isAlive守护线程的使用(五)

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值