多线程基础(九):守护线程、yield、join及线程组
[toc]
不经意间都已经在上一篇文章中聊到ReentrantLock了,但是回头一看,关于多线程基础的内容还有很多没涉及2到,而ReentrantLock却是属于比较高级的线程应用了。今天统一回顾下这些基础的知识点。
守护线程
在前面《多线程基础(二): Thread源码分析》中,我们提到了诸如守护线程,join等概念,现在来看看什么是守护线程。
在java中,线程有两种,一种是用户线程,一种是守护线程。所谓守护线程,就是在线程创建之后,启动之前,通过setDaemon将其设置为true。
t2.setDaemon(true);
这样就能将一个线程设置为守护线程。
那么守护线程有什么作用呢,其主要目的是,当一个线程被设置为守护线程之后,jvm主线程再也不会关心这个线程的运行情况,不像用户线程,如果用户线程没有执行完,那么主线程是不会退出的,那么只要设置了守护线程,主线程再所有用户线程逻辑执行完之后就会退出。
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
public class DeamonTest {
public static int i = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(() -> {
try {
while (true) {
System.out.println(i++);
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.setDaemon(true);
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.setDaemon(true);
t1.start();
t2.start();
}
}
看如下代码:
T1被设置为了守护线程,每秒print一个数字,但是T2是用户线程,T2sleep10秒,在这10秒之后,T2结束了,t1也就会停止:
0
1
2
3
4
5
6
7
8
9
Process finished with exit code 0
实际上这个功能很好理解,有点类似于操作系统中的守护进程。那么这个线程可以做什么呢?这让我想到了之前写阻塞队列的时候,加了一个监控线程,定期输出队列的大小,之后在队列退出之后,监控线程也会自动停止。
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
public class DeamonTest1 {
private static final int MAX = 10;
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (count < MAX){
count ++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
System.out.println(count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.setDaemon(true);
t1.start();
t2.start();
}
}
正如上面代码那样,t2就是监控线程,定期将数字进行打印。而t1则是将数字累加到MAX后就会退出。
1
1
3
4
4
6
7
7
9
9
10
Process finished with exit code 0
那么守护线程就非常适合我们在各种池中用做监控线程来使用。
2.yield
Thread的yield方法是一个可以将当前线程的执行权限让出的方法。调用yield之后,当前执行的线程就会从RUNNING状态变为RUNNABLE状态。我们来看如下例子:
package com.dhb.reentrantlocktest;
import java.util.concurrent.atomic.AtomicInteger;
public class YieldTest {
private static final int MAX = 20;
private static volatile AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(() -> {
while (i.get()<MAX) {
System.out.println(Thread.currentThread().getName() + " "+(i.getAndIncrement()));
// Thread.yield();
}
},"T1");
Thread t2 = new Thread(() -> {
while (i.get()<MAX) {
System.out.println(Thread.currentThread().getName() + " "+(i.getAndIncrement()));
}
},"T2");
t1.start();
t2.start();
}
}
首先这两个线程启动,由于T1先执行,那么T1print的数据肯定会比T2多。
T1 0
T2 1
T1 2
T1 4
T1 5
T1 6
T1 7
T2 3
T1 8
T1 10
T1 11
T1 12
T1 13
T2 9
T1 14
T1 16
T1 17
T1 18
T1 19
T2 15
我们将yield打开,再看看执行结果:
T1 0
T2 1
T2 3
T1 2
T2 4
T2 6
T2 7
T1 5
T2 8
T2 10
T2 11
T2 12
T2 13
T2 14
T2 15
T2 16
T2 17
T2 18
T2 19
T1 9
可以看到调用yield之后,可能会让T2的次数增多。但是需要注意的是,这个情况不是绝对的。当线程从RUNNABLE状态变为RUNNING状态的时候,这个过程并不是类似公平锁那样先进先出,于synchronized导致的从BLOCK到RUNNABLE状态一样。
3. join
join方法是指将运行join方法的线程使其处于WAIT状态,待被运行的线程执行完之后再通知他进入RUNNABLE状态。如下所示:
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
public class JoinTest {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+" begin sleep!");
TimeUnit.SECONDS.sleep(10);
System.out.println(Thread.currentThread().getName()+" weak up!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+" begin sleep!");
TimeUnit.SECONDS.sleep(10);
System.out.println(Thread.currentThread().getName()+" weak up!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
// t1.join();
// t2.join();
System.out.println("main exit");
}
}
上述代码在没开启join的时候:
main exit
Thread-1 begin sleep!
Thread-0 begin sleep!
Thread-1 weak up!
Thread-0 weak up!
可以看到,main线程已经退出了,但是线程0和1都还在运行。
当我们打开join之后:
Thread-0 begin sleep!
Thread-1 begin sleep!
Thread-1 weak up!
Thread-0 weak up!
main exit
如果想让两个线程串行运行:
t1.start();
t1.join();
t2.start();
t2.join();
这样的执行结果:
Thread-0 begin sleep!
Thread-0 weak up!
Thread-1 begin sleep!
Thread-1 weak up!
main exit
4. 线程组
线程组是java中的一个已经不怎么被使用的概念,线程组ThreadGroup对象,可以在new Thread的时候,将线程组传入,之后能实现对线程组的统一interrupt和stop等。但是实际上我们在工作中已经不怎么使用。因为线程组只是提供了一个比较弱的管理机制,类似于在线程中打上标记,这种控制手段比较弱。而我们实际的工作中,大多数情况下是使用的线程池。
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
public class ThreadGroupTest {
public static void main(String[] args) throws InterruptedException {
ThreadGroup g1 = new ThreadGroup("G1");
Thread t1 = new Thread(g1,() -> {
while (true) {
System.out.println("*");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" Interrupt .");
e.printStackTrace();
}
}
},"T1");
Thread t2 = new Thread(g1,() -> {
while (true) {
System.out.println("-");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" Interrupt .");
e.printStackTrace();
}
}
},"T2");
t1.start();
t2.start();
System.out.println(g1.getName());
g1.interrupt();
TimeUnit.SECONDS.sleep(5);
System.out.println(g1.activeCount());
}
}
上述代码执行如下:
G1
-
*
T2 Interrupt .
T1 Interrupt .
*
-
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.dhb.reentrantlocktest.ThreadGroupTest.lambda$main$0(ThreadGroupTest.java:13)
at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.dhb.reentrantlocktest.ThreadGroupTest.lambda$main$1(ThreadGroupTest.java:25)
at java.lang.Thread.run(Thread.java:748)
*
-
-
*
-
*
*
-
2
可以看到可以对线程组的线程,统一实现打断方法,本来旧版本还可以实现stop方法。但是这个方法已经在新版本中被废弃。
此外默认情况下,Thread是使用的父类的线程组。
我们可以看Thread中的init方法:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
... ...
可以看到g为null的时候,使用的是parent.getThreadGroup()。默认情况下缺省的就是父线程的Group。
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
public class GroupTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"T1");
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"T2");
t2.start();
System.out.println("T1 :"+t1.getThreadGroup());
System.out.println("T2 :"+t2.getThreadGroup());
}
}
在缺省情况下,都会使用默认的父线程的组。而所有线程都是main创建,那么实际上就是main所在的线程组。
个人觉得,线程组相对线程池来说,已经不是那么重要了。我们现在很少再用线程组来管理。而是使用线程池。
后面会对线程池单独进行介绍。