Java并发---①线程管理

一.线程管理

1.1 线程的创建方式

  • 继承Thread类 (Thread类实现了Runnable接口)
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("使用继承的方式实现一个线程");
    }
}
  • 实现Runnable接口
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("使用实现Runnable接口的方式来实现一个线程");
    }
}

1.2 线程的运行

  • 如果使用的是继承Thread类的方式,直接调用start()方法即可运行一个线程
  • 如果是使用实现Runnable接口的方式,那么需要创建一个Thread,将自身作为参数传入,然后调用创建的Thread对象的start()方法才可以运行。
public class Main {
    public static void main(String[] args){
        //第一种方式
        MyThread myThread = new MyThread();
        myThread.start();
        //第二种方式
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

调用线程的start方法后,将会创建一个新的线程,并执行线程中的run方法

如果直接调用该对象的run方法,那么不会创建一个新的线程,而是会被主线程当成一个普通方法来执行。

1.3 线程信息的获取和设置

Thread类有一些保存信息的属性,这些属性可以用来标识线程,显示线程的状态或者控制线程的优先级。

  • ID: 保存了线程的唯一标识符。
  • Name:保存了线程的名字。
  • Priority:保存了线程对象的优先级。线程的优先级从1到10,其中1是最低优先级,10是最高优先级。
  • Status:保存了线程的状态。在Java中,线程的状态有6种:new,runnable,blocked,waiting,time waiting 或者 terminated。
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("***************************");
        System.out.println("id = " + Thread.currentThread().getId());
        System.out.println("name = " + Thread.currentThread().getName());
        System.out.println("priority = " + Thread.currentThread().getPriority());
        System.out.println("state = " + Thread.currentThread().getState());
        System.out.println("***************************");
    }
}
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        //设置线程名
        thread.setName("线程名字");
        //设置线程优先级
        thread.setPriority(10);
        thread.start();
    }
}

输出信息如下:

***************************
id = 14
name = 线程名字
priority = 10
state = RUNNABLE
***************************

1.4 线程的中断

如果一个Java程序存在不止一个线程在执行,那么必须要等到所有的线程都执行结束后, 这个Java程序才能运行结束。更准确的说,要等到所有的非守护线程运行结束后(守护线程的概念后面会介绍),或者其中某一个线程执行了System.exit()方法时,这个Java程序才运行结束。

如果一个线程执行时间过于久,那么我们有没有什么办法让它停止呢?当然是存在的,Java为我们提供了中断机制。这个机制要求线程检测它是否被中断了,然后再决定是否响应这个中断请求。线程允许忽略中断请求并且继续执行。

public class MyThread extends Thread {
    @Override
    public void run(){
        int result = 0;
        while (true){
            System.out.println("result = " + result ++);
            if (this.isInterrupted()){
                System.out.println("线程已经被中断");
                return;
            }
        }
    }
}
public class Main {
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        //启动线程
        myThread.start();
        try {
            //让主线程休息一秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断线程
        myThread.interrupt();
    }
}

通过上面这个例子我们可以发行,当我们调用这个线程的interrupt方法时,线程并不会被强制中断。这个方法只会给这个线程打上一个标记,而是否中断,则由我们在 run方法中的代码所决定。

  • isInterrupted 方法:获取线程的interrupted属性的值 (推荐使用此方法)
  • interrupted 方法:静态方法,获取线程interrupted属性的值,并将值置为false

1.5 线程中断的控制

我们已经知道了调用线程的interrupt方法,并不能真正的让线程中断,那么我们如何才能控制线程的中断呢?

  • 方法一:stop方法

stop方法能让运行中的线程强制停止下来,但是该方法已经被作废了,因为强行停止一个线程,会让很多清理工作无法进行,也会让线程获得的锁立刻释放,从而带来数据不一致等等问题。

  • 方法二:interrupt + return

在外面调用线程的interrupt方法,在线程内部进行判断,当线程的interrupted属性为true时,直接return停止该线程。该方法在线程run方法的逻辑极为简单时,可以使用。但是如果线程实现了复杂的算法并分布在几个方法中,或者线程中有递归调用的方法,我们就得提供一个更好的机制来控制线程的中断。

  • 方法三:interrupt + InterruptedException

下面这个示例是实现了在一个目录下,寻找某个文件的路径,里面涉及到递归调用。但是不管方法递归到了那一层,只要线程被中断了,就会抛出InterruptedException异常,并被run方法捕获到,从而进行其他的工作。

public class FileSearch implements Runnable {

    private String initPath;
    private String fileName;

    public FileSearch(String initPath, String fileName) {
        this.initPath = initPath;
        this.fileName = fileName;
    }

    @Override
    public void run() {
        File file = new File(initPath);
        if (file.isDirectory()){
            try {
                directoryProcess(file);
            }catch (InterruptedException e){
                System.out.printf("%s: The search has been interrupted"
                        , Thread.currentThread().getName());
            }
        }
    }

    private void directoryProcess(File file) throws InterruptedException {
        File[] listFiles = file.listFiles();
        if (listFiles != null){
            for(File sonFile : listFiles){
                if (sonFile.isDirectory()){
                    directoryProcess(sonFile);
                }else {
                    fileProcess(sonFile);
                }
            }
        }
        if (Thread.interrupted()){
            throw new InterruptedException();
        }
    }

    private void fileProcess(File file) throws InterruptedException {
        if (file.getName().equals(fileName)){
            System.out.printf("%s : %s\n", Thread.currentThread().getName(), file.getAbsolutePath());
        }
        if (Thread.interrupted()){
            throw new InterruptedException();
        }
    }
public class Main {
    public static void main(String[] args){
        FileSearch fileSearch = new FileSearch("C://", "test.txt");
        Thread thread = new Thread(fileSearch);
        thread.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断线程
        thread.interrupt();
    }
}

1.6 线程的休眠与恢复

有些时候,我们需要在某一个预期时间让线程进行休眠,以释放资源给其他线程使用。例如,程序的一个线程每隔一分钟就会检查一下传感器的状态,其余时间不做任何操作。在这段空间时间,我们希望线程可以不占计算机的任何资源,当它继续执行的直接到来时,JVM会选中它让它继续执行。我们可以通过sleep()方法来实现这个需求。

  • sleep方法接受整型数值作为参数,以表明线程挂起执行的毫秒数。
  • 通过TimeUnit枚举元素也存在一个sleep方法,这个方法也是使用Thread类的sleep方法,但是它接受的单位由选择的枚举类型而定。
public class Main {
    public static void main(String[] args){
        System.out.println("before sleep : " + System.currentTimeMillis());
        try {
            //休眠一秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after sleep : " + System.currentTimeMillis());
        System.out.println("before sleep : " + System.currentTimeMillis());
        try {
            //休眠一分钟
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after sleep : " + System.currentTimeMillis());
    }
}

大家可能发现了,每次执行sleep方法,都要显示的捕获一个InterruptedException异常,这是为什么呢?与下面两种场景有关

  • 处于休眠状态的线程,如果调用了interrupt方法,该线程会立马从休眠状态唤醒,并抛出InterruptException异常,并且将线程的interrupted属性置为false
  • 被标记了中断的线程,如果调用了sleep方法,同样也会抛出InterruptException异常,并且将线程的interrupted属性置为false
public class MyThread1 extends Thread{
    @Override
    public void run(){
        try {
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " interrupted : " + this.isInterrupted());
            System.out.println(Thread.currentThread().getName() + " 处于休眠中的线程被调用了 interrupt方法");
            System.out.println("******************");
            return;
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run(){
        while (true){
            try {
                if (this.isInterrupted()){
                    System.out.println(Thread.currentThread().getName() + "线程已经被中断");
                    Thread.sleep(1000);
                }
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName() + " interrupted : " + this.isInterrupted());
                System.out.println(Thread.currentThread().getName() + " 中断的线程调用了 sleep方法");
                return;
            }

        }
    }
}
public class Main {
    public static void main(String[] args){
        MyThread1 myThread1 = new MyThread1();
        myThread1.start();
        myThread1.interrupt();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        MyThread2 myThread2 = new MyThread2();
        myThread2.start();
        myThread2.interrupt();
    }
}

/*
结果如下:
Thread-0 interrupted : false
Thread-0 处于休眠中的线程被调用了 interrupt方法
******************
Thread-1线程已经被中断
Thread-1 interrupted : false
Thread-1 中断的线程调用了 sleep方法
*/

此外,还有另外一个方法也可以使当前线程释放暂用的CPU等资源,就是yield()方法,它将通知JVM这个线程对象可以释放CPU了。但是JVM并不保证遵循这个要求。通常来说,yield()方法只做调试使用。

1.7 等待线程的终止

在一些情况下,我们必须等待某个线程的执行结果,才能进行接下来的操作。例如,我们的程序在执行其他的任务时,必须先初始化一些必要的资源。可以使用线程来完成这些资源的加载,等待线程终止,再执行程序的其他任务。

为了达到这个目的, 我们可以使用Thread类的join()方法。当一个线程对象的join()方法被调用时,调用这个方法的线程将被挂起,直到这个线程对象完成它的任务。

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("myThread 线程开始执行: " + System.currentTimeMillis() );
        try {
            //休眠一分钟
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("myThread 线程执行结束: " + System.currentTimeMillis() );
    }
}
public class Main {
    public static void main(String[] args){
        System.out.println("main 线程开始执行: " + System.currentTimeMillis() );
        MyThread myThread = new MyThread();
        myThread.start();
        try {
            //执行这段代码的线程,会等待 myThread线程的中止
            myThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main 线程执行结束: " + System.currentTimeMillis() );
    }
}
/*
执行结果:
main 线程开始执行: 1593227052702
myThread 线程开始执行: 1593227052703
myThread 线程执行结束: 1593227112704
main 线程执行结束: 1593227112704
*/

join方法的使用方式有两种

  • 不带参数:join()
  • 带参数:join(long milliseconds) 以及 join(long milliseconds, long nanos)

假设thread1中调用了thread2的join方法,如果是第一种方式调用的话,那么thread1将会被挂起,直到thread2执行完后才会继续运行。

如果是第二种方式调用的话,那么thread1继续运行的条件变成了两个:

①thread2执行完

②等待时间超过了指定的时间

1.8 守护线程的创建和运行

Java中存在一种特殊的线程叫做守护(Daemon)线程。这种线程的优先级很低,通常来说,当同一个程序中没有其他线程运行的时候,守护线程才会运行。

因为这种特性,守护线程通常被用来做为同一程序中的普通线程(也称为用户线程)的服务提供者。它们通常是无限循环的,以等待服务请求或者执行线程的任务,它们不能做重要的工作,因为我们不可能知道守护线程什么时候能获得CPU时钟。守护线程的主要作用就是为用户线程提供便利。

当同一个程序中已经没有用户线程在运行了,守护线程会随着JVM一起结束工作。一个典型的守护线程就是Java的垃圾回收器(Garbage Collector)

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("MyThread线程已经启动" + System.currentTimeMillis());
        try {
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("MyThread线程已经结束" + System.currentTimeMillis());
    }
}
public class MyDaemon extends Thread {

    public MyDaemon(){
        this.setDaemon(true);
    }

    @Override
    public void run(){
        System.out.println("守护线程已经启动" + System.currentTimeMillis());
        while (true){
            //do noting
        }
    }
}
public class Main {
    public static void main(String[] args){
        System.out.println("main线程已经启动" + System.currentTimeMillis());
        MyThread myThread = new MyThread();
        myThread.start();
        MyDaemon myDaemon = new MyDaemon();
        myDaemon.start();
        System.out.println("main线程已经结束" + System.currentTimeMillis());
    }
}
/*
运行结果:
main线程已经启动1593245216446
MyThread线程已经启动1593245216447
main线程已经结束1593245216447
守护线程已经启动1593245216447
MyThread线程已经结束1593245276448
*/

通过上面的例子我们可以发现,当main线程和myThread线程都结束后,守护线程也会随之结束。

此外,setDaemon()方法只能在start()方法被调用之前设置,一旦线程已经开始运行了,那么将不能再修改它的守护状态。如果在启动后,再修改其守护状态,会抛出IllegalThreadStateException异常。

我们可以通过isDaemon()方法来判断一个线程是不是守护线程。

1.9 线程中不可控制异常的处理

在Java中存在两种异常。

  • 非运行时异常(Checked Exception):这种异常必须在方法声明的throws中抛出,并且在调用该方法时继续往上抛出或者使用try...catch块捕获。例如,IOExcption 和 ClassNotFoundException。
  • 运行时异常(Unchecked Exception):这种异常不需要在方法声明中指定,也不需要在调用该方法时捕获。例如:NumberFormatException。

因为run()方法不支持throws语句,也就是说run()方法不能将异常往上传递,那么在run()方法中调用非运行时异常时,我们必须捕获并进行处理。那么当run()方法中抛出运行时异常时,jvm默认的行为是会在控制台输出堆栈信息,并退出程序。

但是这样也会带来问题,如果发生了运行时异常,线程就直接退出的话,那么一些清理工作无法进行,有没有什么方法能够让我们处理运行时抛出的异常呢?Java提供了这样的机制。

public class MyThread extends Thread {
    @Override
    public void run(){
        System.out.println("MyThread 线程开始执行, " + System.currentTimeMillis());
        //此处会抛出运行时异常,NumberFormatException
        Integer num = Integer.valueOf("ttt");
        System.out.println("MyThread 线程执行结束, " + System.currentTimeMillis());
    }
}
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("捕获到了一个运行时异常");
        System.out.println("线程id : " + t.getId());
        System.out.println("异常错误信息如下:" + e.getMessage());
        System.out.println("堆栈信息如下: ");
        e.printStackTrace(System.out);
        System.out.println("线程的状态为:" + t.getState());
    }
}
public class Main {
    public static void main(String[] args){
        ExceptionHandler exceptionHandler = new ExceptionHandler();
        MyThread myThread = new MyThread();
        //为该线程设置一个运行时异常处理器
        myThread.setUncaughtExceptionHandler(exceptionHandler);
        myThread.start();
    }
}
/*
运行结果如下:
MyThread 线程开始执行, 1593247344833
捕获到了一个运行时异常
线程id : 14
异常错误信息如下:For input string: "ttt"
堆栈信息如下: 
java.lang.NumberFormatException: For input string: "ttt"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.valueOf(Integer.java:766)
	at ex.MyThread.run(MyThread.java:7)
线程的状态为:RUNNABLE
*/

当一个线程抛出了运行时异常,jvm会去检测这个线程是否预设了运行时异常处理器,如果存在,那么jvm会调用这个处理器,并将线程对象和异常传入作为参数。

Thread类还有另一个方法可以处理运行时异常,就是静态方法setDefaultUncaughtExceptionHandler(),该方法将为所有的线程设置一个默认的运行时异常处理器。

public class Main {
    public static void main(String[] args){
        ExceptionHandler exceptionHandler = new ExceptionHandler();
        //为所有线程设置一个默认的运行时异常处理器
        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
        MyThread myThread = new MyThread();
//        myThread.setUncaughtExceptionHandler(exceptionHandler);
        myThread.start();
    }
}
/*
该方法运行结果与上面相同
*/

jvm调用运行时异常处理器的顺序:

  • 首先查找该线程对象自己的运行时异常处理器
  • 如果不存在,查找线程对象所在的线程组(ThreadGroup)的运行时异常处理器
  • 如果还不存在,则使用默认的运行时异常处理器

1.10 线程局部变量的使用

共享数据是并发编程中最核心的问题之一。如果多个线程共享了同一个成员变量,那么你在一个线程中改变了这个值,所有的线程都会被这个改动影响。(同样,这些改动还将引来线程安全问题,下一篇再描述这个问题)

public class MyRunnable implements Runnable {

    private Date startTime;

    @Override
    public void run() {
        startTime = new Date();
        System.out.println(Thread.currentThread().getId() + " 线程开始,线程的startTime的值为:" + startTime);
        //让线程随机休眠0-100秒
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getId() + " 线程结束,线程的startTime的值为:" + startTime);
    }
}
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 3; i++){
            Thread thread = new Thread(myRunnable);
            thread.start();
            //启动一个线程,然后main休眠两秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/*
运行结果:
14 线程开始,线程的startTime的值为:Sat Jun 27 17:47:40 CST 2020
15 线程开始,线程的startTime的值为:Sat Jun 27 17:47:42 CST 2020
16 线程开始,线程的startTime的值为:Sat Jun 27 17:47:44 CST 2020
15 线程结束,线程的startTime的值为:Sat Jun 27 17:47:44 CST 2020
16 线程结束,线程的startTime的值为:Sat Jun 27 17:47:44 CST 2020
14 线程结束,线程的startTime的值为:Sat Jun 27 17:47:44 CST 2020
*/

通过上面这个例子我们可以发现,三个线程最后的startTime都变成了同一个值。这说明了它们共享的是同一个变量。

那么有没有什么方法,能够让每个线程不受其他线程的影响,也就是拥有自己的线程局部变量呢?Java提供的线程局部变量机制就解决了这个问题。

public class MyRunnable implements Runnable {
	//定义线程局部变量,并重写初始化方法
    private ThreadLocal<Date> startTime = new ThreadLocal<Date>(){
        @Override
        protected Date initialValue(){
            return new Date();
        }
    };

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getId()
                + " 线程开始,线程的startTime的值为:" + startTime.get());
        //让线程随机休眠0-100秒
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getId()
                + " 线程结束,线程的startTime的值为:" + startTime.get());
    }
}
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 3; i++){
            Thread thread = new Thread(myRunnable);
            thread.start();
            //启动一个线程,然后main休眠两秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/*
运行结果如下:
14 线程开始,线程的startTime的值为:Sat Jun 27 18:21:13 CST 2020
15 线程开始,线程的startTime的值为:Sat Jun 27 18:21:15 CST 2020
16 线程开始,线程的startTime的值为:Sat Jun 27 18:21:17 CST 2020
16 线程结束,线程的startTime的值为:Sat Jun 27 18:21:17 CST 2020
14 线程结束,线程的startTime的值为:Sat Jun 27 18:21:13 CST 2020
15 线程结束,线程的startTime的值为:Sat Jun 27 18:21:15 CST 2020
*/

通过上面的示例,我们可以发现,每个线程的startTime值都不一样。

线程局部变量的工作原理:

  • 线程局部变量为每个线程存储了各自的属性值,并提供给每个线程使用
  • 通过get()方法读取这个值,如果线程局部变量没有存储该线程的值,那么会调用initialValue()方法进行初始化
  • 通过set()方法可以设置这个值
  • 通过remove()方法可以删除已经存储的值
public class MyRunnable implements Runnable {

    private ThreadLocal<Date> startTime = new ThreadLocal<Date>(){
        @Override
        protected Date initialValue(){
            return new Date();
        }
    };

    @Override
    public void run() {
        //由于线程局部变量存储的值为空,此处调用initialValue()
        System.out.println("第一次获取线程局部变量" + startTime.get());
        //休眠一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //调用set()方法,设置一个新的值
        Date date = new Date();
        startTime.set(date);
        System.out.println("set的date为 :" + date);
        //本次获取的值应该和set一样
        System.out.println("第二次获取线程局部变量" + startTime.get());
        //休眠一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //调用remove()方法,该方法会删除线程局部变量中保存的值
        startTime.remove();
        //本次获取和第一个次一样,由于线程局部变量存储的值为空,此处调用initialValue()
        System.out.println("第三次获取线程局部变量" + startTime.get());
    }
}
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
/*
运行结果:
第一次获取线程局部变量Sat Jun 27 20:58:50 CST 2020
set的date为 :Sat Jun 27 20:58:51 CST 2020
第二次获取线程局部变量Sat Jun 27 20:58:51 CST 2020
第三次获取线程局部变量Sat Jun 27 20:58:52 CST 2020
*/

线程局部变量实际上是将共享变量拷贝了一份到线程的threadLocals变量(ThreadLocalMap类型)中,看这个类型名字也知道,类似于一个map结构,一个线程的所有的局部变量都缓存在其中。

InheritableThreadLocal类

InheritableThreadLocal类是ThreadLocal的子类,主要扩展了以下两个功能:

  • 值继承:A线程创建了B线程,那么B线程就会继承A线程的InheritableThreadLocal类型的线程局部变量
  • 值扩展:如果B线程实现了 childValue()方法,那么会在继承的同时,将值进行扩展
public class FatherThread extends Thread {

    @Override
    public void run(){
        System.out.println("第一次获取父线程局部变量: " + ThreadLocalExt.str.get());
        //修改父线程局部变量的值
        ThreadLocalExt.str.set("test");
        System.out.println("第二次获取父线程局部变量: " + ThreadLocalExt.str.get());
        SonThread sonThread = new SonThread();
        sonThread.start();
    }
}
public class SonThread extends Thread{

    @Override
    public void run(){
        System.out.println("子线程局部变量: " + ThreadLocalExt.str.get());
    }
}
public class Main {
    public static void main(String[] args){
        FatherThread fatherThread = new FatherThread();
        fatherThread.start();
    }
}
/*
执行结果:
第一次获取父线程局部变量: 初始值
第二次获取父线程局部变量: test
子线程局部变量: test, 子线程追加的值
*/

工作原理其实与ThreadLocal类似,InheritableThreadLocal是将线程局部变量拷贝了一份到Thread对象的inheritableThreadLocals中,它与threadLocals一样,都是ThreadLocalMap类型的。在父线程中创建一个子线程时,默认的构造函数会将inheritableThreadLocals变量的值拷贝到子线程中,拷贝的方法就是childValue()方法,所以重写了childValue()就能改变子线程中的值。

1.11 线程的分组

Java还提供了一个有趣的功能,可以将线程进行分组。在同一个组内的线程,可以进行统一的访问和操作。

public class MyRunnable implements Runnable {

    @Override
    public void run(){
        System.out.println("Thread start, name = " + Thread.currentThread().getName());
        //随机休眠0-100秒
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
            return;
        }
        System.out.println("Thread end, name = " + Thread.currentThread().getName());
    }
}
public class Main {
    public static void main(String[] args){
        ThreadGroup threadGroup = new ThreadGroup("group");
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 5; i++){
            Thread thread = new Thread(threadGroup, myRunnable);
            thread.start();
            //main线程休眠两秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //ThreadGroup的功能有很多,这里演示几个重要的
        //①打印组内所有线程
        threadGroup.list();
        //②获取组内所有活跃线程的数量
        int count = threadGroup.activeCount();
        System.out.println("active thread : " + count);
        //③将组内所有活跃线程复制到线程数组中
        Thread[] threads = new Thread[count];
        threadGroup.enumerate(threads);
        for (Thread thread : threads){
            System.out.println("name = " + thread.getName() + " , status = " + thread.getState());
        }
        //④中断组中所有线程,由于线程处于休眠状态,会抛出InterruptedException异常
        threadGroup.interrupt();
    }
}
/*
运行结果:
Thread start, name = Thread-0
Thread start, name = Thread-1
Thread start, name = Thread-2
Thread start, name = Thread-3
Thread start, name = Thread-4
java.lang.ThreadGroup[name=group,maxpri=10]
    Thread[Thread-0,5,group]
    Thread[Thread-1,5,group]
    Thread[Thread-2,5,group]
    Thread[Thread-3,5,group]
    Thread[Thread-4,5,group]
active thread : 5
name = Thread-0 , status = TIMED_WAITING
name = Thread-1 , status = TIMED_WAITING
name = Thread-2 , status = TIMED_WAITING
name = Thread-3 , status = TIMED_WAITING
name = Thread-4 , status = TIMED_WAITING
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 group.MyRunnable.run(MyRunnable.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 group.MyRunnable.run(MyRunnable.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 group.MyRunnable.run(MyRunnable.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 group.MyRunnable.run(MyRunnable.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 group.MyRunnable.run(MyRunnable.java:13)
	at java.lang.Thread.run(Thread.java:748)
*/

除了上面显示的这些接口外,线程组还可以设置线程的优先级,设置守护线程等等操作,线程组除了可以包含线程外,还可以包含其他的线程组,它是一个树形结构。

线程组是通过将组内所有的线程对象和线程组对象都存储起来,并且可以访问它们的信息,将统一的操作执行到所有的成员上。

1.12 线程组中不可控制异常的处理

之前我们学习了,线程中如何处理不可控制的异常(也就是运行时异常),当时也提到了线程组中也可以处理运行时异常。现在就来看看,线程组中是如何处理运行时异常的。

在覆盖线程组中的uncaughtException方法,即可在其中定义运行时异常的处理,组内成员出现了运行时异常,如果没有定义自身的异常处理器,都会进入该方法中。

public class MyRunnable implements Runnable {

    @Override
    public void run(){
        System.out.println("Thread start, name = " + Thread.currentThread().getName());
        //此处将会抛出运行时异常,NumberFormatException
        Integer num = Integer.valueOf("ttt");
        System.out.println("Thread end, name = " + Thread.currentThread().getName());
    }
}
public class Main {
    public static void main(String[] args){
        ThreadGroup threadGroup = new ThreadGroup("group"){
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("捕获到了一个运行时异常");
                System.out.println("线程id : " + t.getId());
                System.out.println("异常错误信息如下:" + e.getMessage());
                System.out.println("堆栈信息如下: ");
                e.printStackTrace(System.out);
                System.out.println("线程的状态为:" + t.getState());
            }
        };
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(threadGroup, myRunnable);
        thread.start();
    }
}
/*
Thread start, name = Thread-0
捕获到了一个运行时异常
线程id : 14
异常错误信息如下:For input string: "ttt"
堆栈信息如下: 
java.lang.NumberFormatException: For input string: "ttt"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.valueOf(Integer.java:766)
	at group.MyRunnable.run(MyRunnable.java:12)
	at java.lang.Thread.run(Thread.java:748)
线程的状态为:RUNNABLE
*/

jvm调用运行时异常处理器的顺序:

  • 首先查找该线程对象自己的运行时异常处理器
  • 如果不存在,查找线程对象所在的线程组(ThreadGroup)的运行时异常处理器
  • 如果还不存在,则使用默认的运行时异常处理器

1.13 使用工厂类创建线程

工程模式是设计模式中最常用的模式之一。它是一个建造者模式,使用一个类为其他的一个类或者多个类创建对象。当我们为这些类创建对象时,不需要再使用new构造器,而直接使用工厂。

使用工厂模式,可以将对象的创建集中化,有以下几点好处:

  • 更容易修改创建对象的方式
  • 更容易为有限资源限制创建对象的数目。例如,我们可以限制一个类只能创建n个对象。
  • 更容易为创建的对象生产统计数据。

Java本身已经为我们提供了线程的工厂类,那就是ThreadFactory接口,这个接口实现了线程对象工厂。Java并发API的高级工具类也使用了线程工厂创建线程。

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //do noting
    }
}
public class MyThreadFactory implements ThreadFactory {
    /**
     * 统计创建的线程数量
     */
    private int count;
    /**
     * 统计线程创建的信息
     */
    private List<String> stats = new ArrayList<>();

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        count++;
        stats.add("Create thread: id = " + thread.getId() + " , name = " + thread.getName());
        return thread;
    }

    public String getStatus(){
        StringBuffer stringBuffer = new StringBuffer();
        Iterator<String> iterator = stats.iterator();
        while (iterator.hasNext()){
            stringBuffer.append(iterator.next() + "\n");
        }
        return stringBuffer.toString();
    }
}
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        MyThreadFactory myThreadFactory = new MyThreadFactory();
        for (int i = 0; i < 5; i++){
            Thread thread = myThreadFactory.newThread(myRunnable);
            thread.start();
        }
        System.out.println(myThreadFactory.getStatus());
    }
}
/*
运行结果:
Create thread: id = 14 , name = Thread-0
Create thread: id = 15 , name = Thread-1
Create thread: id = 16 , name = Thread-2
Create thread: id = 17 , name = Thread-3
Create thread: id = 18 , name = Thread-4
*/

ThreadFactory接口只有一个方法,就是newThread,它以Runnable接口作为入参,返回一个创建好的线程。当实现ThreadFactory接口时,必须实现这个方法。

posted @ 2020-06-28 00:03  litterCoder  阅读(181)  评论(0编辑  收藏  举报