此篇博客为个人学习笔记,如有错误欢迎大家指正
本次内容
- 等待线程的终止
- 守护线程的创建和运行
- 线程中不可控异常的处理
- 线程局部变量的使用
- 线程的分组
- 线程组中不可控异常的处理
- 使用工厂类创建线程
1.等待线程的终止
在某些情况下,我们的程序需要等待线程终止后才能继续进行。例如:程序在执行一些任务时,需要加载资源。我们可以开启一个新的线程来完成加载资源的任务,这时我们就需要等待加载资源的线程执行完毕后,才能继续向下执行。这里的等待被称为挂起,当我们的线程A需要挂起直到线程B运行完毕时,我们可以在线程A中使用方法B.join(),这样的话,B线程运行结束后A线程才会继续向下执行。join()方法还有另外一种形式————join(long milliseconds),例如:B.join(1000)。这条代码的作用是:线程B执行完毕或者挂起时长到达1秒时,之前被挂起的线程将继续向下执行。
范例实现:
在这个范例中,我们将创建两个类并实现Runnable接口,两个类在run方法中分别休眠4秒和6秒。我们在main方法中分别创建并开启两个线程,并将主线程挂起直到两个线程执行完毕。代码如下
NetworkConnectionsLoader类:
package thread.part1.code;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class NetworkConnectionsLoader implements Runnable {
@Override
public void run() {
//打印线程开始执行的时间
System.out.printf("Beginning network connections loading: %s \n",new Date());
try {
//休眠6秒
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印线程结束运行的时间
System.out.printf("Network connections loading has finished: %s\n",new Date());
}
}
DataSourcesLoader类:
package thread.part1.code;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class DataSourcesLoader implements Runnable {
@Override
public void run() {
//打印线程开始执行的时间
System.out.printf("Beginning data sources loading: %s \n",new Date());
try {
//线程休眠4秒
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印线程结束运行的时间
System.out.printf("Data sources loading has finished: %s\n",new Date());
}
public static void main(String[] args) {
//创建对象并作为传入参数以此来创建一个线程
DataSourcesLoader dsl = new DataSourcesLoader();
Thread thread1 = new Thread(dsl, "DataSourcesLoader");
//创建对象并作为传入参数以此来创建一个线程
NetworkConnectionsLoader ncl = new NetworkConnectionsLoader();
Thread thread2 = new Thread(ncl, "NetworkConnectionsLoader");
//开启线程
thread1.start();
thread2.start();
try {
//主线程挂起,只有当thread1和thread2都执行完毕之后主线程才会继续向下执行
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印主线程结束的时间
System.out.printf("Main: Configuration has been loaded: %s\n",new Date());
}
}
2.守护线程的创建和运行
在Java中有两类线程,守护线程(Daemon Thread)和用户线程。只要当前JVM实例中存在任何一个非守护线程,守护线程就会工作。直到剩下的线程全部都是守护线程,JVM会直接退出,不管守护线程是否执行完毕。因此,我们不能在守护线程中编写重要的代码。守护线程并非只能由JVM内部提供,我们可以编写自己的守护线程。像创建其他线程一样,我们在执行线程之前使用setDaemon(true)这个方法即可设置线程为守护线程。
范例实现:
在这个实例中,我们将创建两种线程。一种为用户线程,它负责向容器中添加事件;另一种当然是守护线程,它负责将容器中存活时长超过10S的事件从容器中删掉。书上的源代码存在问题,以下是做了部分修改的代码,已经有较为详细的注释,具体细节不再赘述
首先创建事件类:
package thread.part1.code;
import java.util.Date;
public class Event {
//日期,之后用来判断创建的时间
private Date date;
//字符串类型的描述
private String event;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public String getEvent() {
return event;
}
public void setEvent(String event) {
this.event = event;
}
}
设计我们的守护线程:
package thread.part1.code;
import java.util.Date;
import java.util.Deque;
public class CleanerTask extends Thread {
//用来装载事件的容器,是一个双端队列,线程不安全类
private Deque<Event> deque;
//构造方法,在这里我们通过setDaemon方法将线程设置为守护线程
public CleanerTask(Deque<Event> deque) {
this.deque = deque;
this.setDaemon(true);
}
@Override
public void run() {
/*
* 设置死循环来让守护线程始终运行
* 创建一个日期类型并传入clean方法中用于比较
* */
while (true) {
Date date = new Date();
clean(date);
}
}
//clean方法用于清除容器中的过期事件
private void clean(Date date) {
//存储时间差
long difference;
//是否删除
boolean delete;
//因为deque是线程不安全类,所以我们在这里加锁,否则会出现错误
synchronized (deque) {
//如果容器为空,直接返回
if (deque.size() == 0) {
return;
}
//初始化
delete = false;
do {
//从队尾取出一个事件
Event e = deque.getLast();
//取出事件的创建时间并作差
difference = date.getTime() - e.getDate().getTime();
//如果时间差大于10s,删除事件、打印语句、delete置true
if (difference > 10000) {
System.out.printf("Cleaner: %s\n", e.getEvent());
deque.removeLast();
delete = true;
}
} while (difference > 10000);
//如果删除了事件,则打印容器当前的大小
if (delete) {
System.out.printf("Cleaner: Size of the queue: %d\n", deque.size());
}
}
}
}
设计我们的用户线程,并在main方法中创建、执行线程:
package thread.part1.code;
import java.util.ArrayDeque;
import java.util.Date;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
public class WriterTask implements Runnable {
//用来装载事件的容器,是一个双端队列,线程不安全类
private Deque<Event> deque;
//有参构造方法
public WriterTask(Deque<Event> deque) {
this.deque = deque;
}
@Override
public void run() {
//循环创建100个事件
for (int i = 0; i < 100; i++) {
Event event = new Event();
//设置事件创建时间和时间内容
event.setDate(new Date());
event.setEvent(String.format("The thread %s has generated an event", Thread.currentThread().getId()));
//加锁,向队列头添加刚创建好的事件
synchronized (deque) {
deque.addFirst(event);
}
try {
//休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//创建一个容器并作为参数传给构造函数
Deque<Event> deque = new ArrayDeque<>();
WriterTask writerTask = new WriterTask(deque);
//创建三个用户线程并运行
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(writerTask,"write");
thread.start();
}
//创建守护线程并运行
CleanerTask cleanerTask = new CleanerTask(deque);
cleanerTask.start();
}
}
3.线程中不可控异常的处理
在Java中有两种异常
- 非运行时异常:这种异常可以在方法声明处通过throws关键字抛出,也可以通过try、catch语句进行捕获
- 运行时异常:这种异常无须在方法中声明,也不必捕获。
在线程对象中,因为原有的
run()方法并没有通过throws向上抛异常,所以我们进行重写后的run()方法也不允许抛出异常。所以在run()方法中,我们只能捕获非运行时异常。当run()方法中出现运行时异常时,默认的行为是在控制台输出堆栈记录并退出程序。当然,Java为我们提供了一种机制,使得我们可以编写自己的运行时异常处理类。首先,我们需要实现UncaughtExceptionHandler这个接口并重写uncaughtException()方法。在我们创建线程对象后,通过setUncatghtExceptionHandler()这个方法设置此线程对应的运行时异常处理器。
范例实现:
未捕获异常处理类:
package thread.part1.code;
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
//打印提示语
System.out.printf("An exception has been captured\n");
//打印出现异常的线程id
System.out.printf("Thread: %s\n",t.getId());
//打印异常类和异常信息
System.out.printf("Exception: %s: %s\n",e.getClass().getName(),e.getMessage());
//打印堆栈记录信息
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
//打印线程状态
System.out.printf("Thread status: %s\n",t.getState());
}
}
main方法和会抛出异常的线程对象:
package thread.part1.code;
import java.util.concurrent.TimeUnit;
public class Task implements Runnable {
@Override
public void run() {
//编写可以抛出运行时异常的语句
int number = Integer.parseInt("TTT");
}
public static void main(String[] args) {
//创建Task类并创建线程
Task task = new Task();
Thread thread = new Thread(task);
//设置线程的未捕获异常处理器为我们自己编写的类
thread.setUncaughtExceptionHandler(new ExceptionHandler());
//Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());
//开启线程
thread.start();
}
}
我们还可以通过Thread类的静态方法Thread.setDefaultUncaughtExceptionHandler()来为所有的线程对象设置一个默认未捕获异常处理器。实际上,在线程对象抛出一个未捕获的异常时,JVM会按照以下顺序寻找异常处理器:线程对象的未捕获异常处理器,如不存在则寻找线程对象所在线程组的未捕获异常处理器,其次是默认未捕获异常处理器,如果三个都不存在,则打印堆栈记录信息并退出程序。
4.线程局部变量的使用
在Java中,如果创建的对象实现了Runnable接口并作为参数传入Thread类的构造函数中,那么由此创建的所有线程都将共享这个类中的属性。也就是说,当一个线程修改了此类中的成员变量,其他线程受这个改变影响。如果不希望某些属性被所有线程共享,可以使用Java为我们提供的线程局部变量ThreadLocal。
范例实现:
在这个实例中,我们会创建出3个线程。ThreadLocal中存放着线程创建时的时间,我们将会来观察线程结束时打印出的创建时间是否相同。如果不同,则代表ThreLocal中的数据是相互独立的。代码如下
package thread.part1.code;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class SafeTask implements Runnable {
//线程局部变量
private ThreadLocal<Date> startDate = new ThreadLocal<>();
@Override
public void run() {
//为线程局部变量设置日期
startDate.set(new Date());
//在休眠前打印日期并标明线程id
System.out.printf("Starting Thread: %s: %s\n",
Thread.currentThread().getId(), startDate.get());
try {
//休眠,给其他线程设置创建日期的时间
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//休眠后再次打印创建日期,观察三个线程的创建日期是否一样
System.out.printf("Thread Finished : %s: %s\n",
Thread.currentThread().getId(), startDate.get());
}
public static void main(String[] args) {
SafeTask safeTask = new SafeTask();
//创建三个线程,每创建一个休眠两秒
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(safeTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.线程的分组
我们可以将线程分组,这样能够更方便的管理它们。线程组不仅可以包含线程,还可以包含其他线程组。
范例实现:
在这个范例中,我们将创建10个线程,它们属于同一个线程组。在run()方法中,我们让线程休眠以此来模拟搜索文件所消耗的时间。每个线程的休眠时间都是随机的,以此来体现搜索速度的差异。当有一个线程成功搜索到文件后,它将自动结束。我们会通过线程组来一次性关闭其他线程。代码如下:
package thread.part1.code;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class SearchTask implements Runnable {
@Override
public void run() {
//获取当前线程的名字
String name = Thread.currentThread().getName();
try {
//执行doTask方法
doTask(name);
//在此捕获异常
} catch (InterruptedException e) {
System.out.printf("%s Interrupted\n",name);
return;
}
//打印结果,表示线程已结束运行
System.out.printf("%s End\n",name);
}
private void doTask(String name) throws InterruptedException {
//将当前时间的毫秒值作为种子来创建一个随机数发生器
Random random = new Random(new Date().getTime());
//通过发生器得到一个浮点数*100后强转为整型
int value = (int) (random.nextDouble()*100);
//打印,表示线程已经开启
System.out.printf("%s has started\n",name);
//休眠
TimeUnit.SECONDS.sleep(value);
}
public static void main(String[] args) {
//创建一个线程组
ThreadGroup searcher = new ThreadGroup("Searcher");
//创建一个searchTask类
SearchTask searchTask = new SearchTask();
for (int i = 0; i < 10; i++) {
//通过循环,以上面创建的searchTask类作为参数创建十个线程对象并开启
Thread thread = new Thread(searcher, searchTask);
thread.start();
try {
//每创建一个休眠一秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//打印线程组中存在的线程数量
System.out.printf("Number of Threads: %d\n",searcher.activeCount());
//通过list方法打印线程组内线程的信息
System.out.printf("Information about the Thread Group\n");
searcher.list();
Thread[] threads = new Thread[searcher.activeCount()];
//通过enumerate方法将线程组中的对象拷贝到名为threads的数组中
searcher.enumerate(threads);
for (int i = 0; i < threads.length; i++) {
//遍历数组输出当前线程的名字和状态
System.out.printf("Thread %s: %s\n",threads[i].getName(),threads[i].getState());
}
//调用waitFinish方法等待某个线程的苏醒
waitFinish(searcher);
//调用线程组的interrupt方法向线程组内的线程发起中断
searcher.interrupt();
}
private static void waitFinish(ThreadGroup searcher) {
/*
* 线程组内有十个线程
* 如果当前线程数量大于9,则表示所有线程均未运行结束
* 执行死循环,没检查一次状态休眠一秒
* */
while (searcher.activeCount() > 9) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
首先,线程组对象的activeCount()方法是返回存在于线程组内所有线程的数量,包括处于阻塞状态的线程。enumerate()方法需要传入一个数组,这个方法没有返回值,但是会将线程组内存在的线程拷贝到传入的数组对象中。线程组对象的interrupt()方法和前面我们使用到的interrupt()方法类似,都是向线程发起中断请求,也就是将线程的中断标志置为true,是否响应中断还要看线程本身。另外,当一个线程的阻塞是由wait(),sleep()和join()方法引起并且中断标志被置为true,此时会抛出InterruptedException异常,所以上面的程序中的线程最后会在控制台打印'Thread-XX Interrupted'
6.线程组中不可控异常的处理
之前我们学过了如何处理线程中的不可控异常,线程组中不可控的异常我们同样有办法处理,只需继承ThreadGroup类并重写uncaughtException()方法即可。
范例实现:
会抛出异常的错误类:
package thread.part1.code;
import java.util.Date;
import java.util.Random;
public class ErrorTask implements Runnable {
@Override
public void run() {
int result;
Random random = new Random(new Date().getTime());
while (true) {
//当除数为零时将抛出异常
result = 100/(int)(random.nextDouble()*100);
System.out.printf("%s: %d\n",
Thread.currentThread().getId(),result);
//检查中断标志是否为true
if (Thread.interrupted()) {
//打印中断信息后中断
System.out.printf("%d Interrupted\n",
Thread.currentThread().getId());
return;
}
}
}
}
线程组类和main方法:
package thread.part1.code;
public class MyThreadGroup extends ThreadGroup {
//必须给出带有参数的构造函数
public MyThreadGroup(String name) {
super(name);
}
//重写uncaughtException方法
@Override
public void uncaughtException(Thread t, Throwable e) {
//中断剩余的线程
this.interrupt();
System.out.println("Terminating the rest of the Threads\n");
//打印抛出异常的线程
System.out.printf("The thread %s has thrown an Exception\n",t.getId());
//打印堆栈记录
e.printStackTrace(System.out);
}
public static void main(String[] args) {
//创建线程组对象
MyThreadGroup threadGroup = new MyThreadGroup("MyThreadGroup");
//创建错误任务类
ErrorTask errorTask = new ErrorTask();
//创建并执行三个线程
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(threadGroup, errorTask);
thread.start();
}
}
}
7.使用工厂类创建线程
我们可以使用工厂类创建线程对象。创建我们自己的线程工厂类只需要实现java为我们提供的ThreadFactory接口并重写newThread()方法即可
范例实现:
范例的主要功能是使用工厂类创建线程,代码中已经有较为详细的注释了,就不在此赘述了
线程工厂类:
package thread.part1.code;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ThreadFactory;
public class MyThreadFactory implements ThreadFactory {
//创建线程类对象的数量
private int counter;
//线程工厂对象的名称
private String name;
//装有线程对象信息的容器
private List<String> stats;
//构造函数
public MyThreadFactory(String name) {
this.name = name;
this.counter = 0;
this.stats = new ArrayList<>();
}
public String getStats() {
//创建字符串缓冲区
StringBuffer stringBuffer = new StringBuffer();
for (String stat : stats) {
/*
* 将字符串添加进字符串缓冲区
* 相比于直接使用'+'拼接,这样可以减少垃圾常量的产生
* */
stringBuffer.append(stat);
stringBuffer.append("\n");
}
//返回字符串
return stringBuffer.toString();
}
@Override
public Thread newThread(Runnable r) {
//为新创建的线程对象添加名字
Thread thread = new Thread(r, name + "-Thread_" + counter);
//计数器的值增加
counter++;
//将新创建线程的信息装入容器
stats.add(String.format("Created thread %d with name %s on %s\n",
thread.getId(), thread.getName(),new Date()));
//返回创建好的线程对象
return thread;
}
}
产品类和main方法:
package thread.part1.code;
import java.util.concurrent.TimeUnit;
public class Product implements Runnable {
@Override
public void run() {
//这个类的任务只是休眠一秒钟
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建线程工厂
MyThreadFactory threadFactory = new MyThreadFactory("MyThreadFactory");
//创建产品对象
Product product = new Product();
Thread thread;
System.out.println("Starting the Threads");
for (int i = 0; i < 10; i++) {
//使用线程工厂对象循环创建十个线程对象并执行
thread = threadFactory.newThread(product);
thread.start();
}
//打印工厂的状态
System.out.println("Factory stats:");
System.out.printf("%s\n",threadFactory.getStats());
}
}