多线程基础(九):守护线程、yield、join及线程组

时间:2022-07-25
本文章向大家介绍多线程基础(九):守护线程、yield、join及线程组,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

文章目录

不经意间都已经在上一篇文章中聊到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所在的线程组。 个人觉得,线程组相对线程池来说,已经不是那么重要了。我们现在很少再用线程组来管理。而是使用线程池。 后面会对线程池单独进行介绍。