代码理解java多线程 (一) - 基础篇(1)

 

目录

第一章 进程与线程的基本概念

1.1 进程产生的背景

1.2 上下文切换

第二章 Java多线程入门类和接口

2.1 Thread类和Runnable接口

2.1.1 继承Thread类

2.1.2 实现Runnable接口

2.1.3 Thread类构造方法

2.1.4 Thread类的几个常用方法

2.1.5 Thread类与Runnable接口的比较:

2.2 Callable、Future与FutureTask

2.2.1 Callable接口

2.2.2 Future接口

2.2.3 FutureTask类

2.2.4 FutureTask的几个状态

第三章 线程组和线程优先级

3.1 线程组(ThreadGroup)

3.2 线程的优先级

3.3 线程组的常用方法及数据结构

3.3.1 线程组的常用方法

3.3.2 线程组的数据结构


 

第一章 进程与线程的基本概念

1.1 进程产生的背景

最初的计算机只能接受一些特定的指令,用户每输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。这样效率非常低下,在很多时候,计算机都处在等待状态。

批处理操作系统

后来有了批处理操作系统,把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

批处理操作系统在一定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高

进程的提出

人们对于计算机的性能要求越来越高,现有的批处理操作系统并不能满足人们的需求,而批处理操作系统的瓶颈在于内存中只存在一个程序,那么内存中能不能存在多个程序呢?这是人们亟待解决的问题。

于是,科学家们提出了进程的概念。

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

程序:用某种编程语言(java、python等)编写,能够完成一定任务或者功能的代码集合,是指令和数据的有序集合,是一段静态代码

此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。

当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的状态进行恢复,接着继续执行。

使用进程+CPU时间片轮转方式的操作系统,在宏观上看起来同一时间段执行多个任务,换句话说,进程让操作系统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源。

对操作系统的要求进一步提高

虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。

比如杀毒软件在检测用户电脑时,如果在某一项检测中卡住了,那么后面的检测项也会受到影响。或者说当你使用杀毒软件中的扫描病毒功能时,在扫描病毒结束之前,无法使用杀毒软件中清理垃圾的功能,这显然无法满足人们的要求。

线程的提出

那么能不能让这些子任务同时执行呢?于是人们又提出了线程的概念,让一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。

使用线程之后,事情就变得简单多了。当用户使用扫描病毒功能时,就让扫描病毒这个线程去执行。同时,如果用户又使用清理垃圾功能,那么可以先暂停扫描病毒线程,先响应用户的清理垃圾的操作,让清理垃圾这个线程去执行。响应完后再切换回来,接着执行扫描病毒线程。

注意:操作系统是如何分配时间片给每一个线程的,涉及到线程的调度策略,有兴趣的同学可以看一下《操作系统》,本文不做深入详解。

总之,进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

多进程的方式也可以实现并发,为什么我们要使用多线程?

多进程方式确实可以实现并发,但使用多线程,有以下几个好处:

  • 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
  • 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。

进程和线程的区别

进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。

  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。

  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

1.2 上下文切换

上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。

寄存器是cpu内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。

程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。

举例说明 线程A - B

1.先挂起线程A,将其在cpu中的状态保存在内存中。

2.在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。

3.当B执行完,根据程序计数器中指向的位置恢复线程A。

CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。

但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。

 

第二章 Java多线程入门类和接口

2.1 Thread类和Runnable接口

上一章我们了解了操作系统中多线程的基本概念。那么在Java中,我们是如何使用多线程的呢?

首先,我们需要有一个“线程”类。JDK提供了Thread类和Runnalble接口来让我们实现自己的“线程”类。

  • 继承Thread类,并重写run方法;
  • 实现Runnable接口的run方法;

2.1.1 继承Thread类

先学会怎么用,再学原理。首先我们来看看怎么用ThreadRunnable来写一个Java多线程程序。

首先是继承Thread类:

 
  1. public class Demo {
  2. public static class MyThread extends Thread {
  3. @Override
  4. public void run() {
  5. System.out.println("MyThread");
  6. }
  7. }
  8.  
  9. public static void main(String[] args) {
  10. Thread myThread = new MyThread();
  11. myThread.start();
  12. }
  13. }

注意要调用start()方法后,该线程才算启动!

我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。

注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出异常。

2.1.2 实现Runnable接口

接着我们来看一下Runnable接口(JDK 1.8 +):

 
  1. @FunctionalInterface
  2. public interface Runnable {
  3. public abstract void run();
  4. }

可以看到Runnable是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。

示例代码:

 
  1. public class Demo {
  2. public static class MyThread implements Runnable {
  3. @Override
  4. public void run() {
  5. System.out.println("MyThread");
  6. }
  7. }
  8.  
  9. public static void main(String[] args) {
  10. new MyThread().start();
  11.  
  12. // Java 8 函数式编程,可以省略MyThread类
  13. new Thread(() -> {
  14. System.out.println("Java 8 匿名内部类");
  15. }).start();
  16. }
  17. }

2.1.3 Thread类构造方法

Thread类是一个Runnable接口的实现类,我们来看看Thread类的源码。

查看Thread类的构造方法,发现其实是简单调用一个私有的init方法来实现初始化。init的方法签名:

 
  1. // Thread类源码
  2.  
  3. // 片段1 - init方法
  4. private void init(ThreadGroup g, Runnable target, String name,
  5. long stackSize, AccessControlContext acc,
  6. boolean inheritThreadLocals)
  7.  
  8. // 片段2 - 构造函数调用init方法
  9. public Thread(Runnable target) {
  10. init(null, target, "Thread-" + nextThreadNum(), 0);
  11. }
  12.  
  13. // 片段3 - 使用在init方法里初始化AccessControlContext类型的私有属性
  14. this.inheritedAccessControlContext =
  15. acc != null ? acc : AccessController.getContext();
  16.  
  17. // 片段4 - 两个对用于支持ThreadLocal的私有属性
  18. ThreadLocal.ThreadLocalMap threadLocals = null;
  19. ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

我们挨个来解释一下init方法的这些参数:

  • g:线程组,指定这个线程是在哪个线程组下;

  • target:指定要执行的任务;

  • name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,见片段2;

  • acc:见片段3,用于初始化私有变量inheritedAccessControlContext

    这个变量有点神奇。它是一个私有变量,但是在Thread类里只有init方法对它进行初始化,在exit方法把它设为null。其它没有任何地方使用它。一般我们是不会使用它的,那什么时候会使用到这个变量呢?可以参考这个stackoverflow的问题:Restrict permissions to threads which execute third party software

  • inheritThreadLocals:可继承的ThreadLocal,见片段4,Thread类里面有两个私有属性来支持ThreadLocal,我们会在后面的章节介绍ThreadLocal的概念。

实际情况下,我们大多是直接调用下面两个构造方法:

 
  1. Thread(Runnable target)
  2. Thread(Runnable target, String name)

2.1.4 Thread类的几个常用方法

这里介绍一下Thread类的几个常用的方法:

  • currentThread():静态方法,返回对当前正在执行的线程对象的引用;
  • start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
  • yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
  • sleep():静态方法,使当前线程睡眠一段时间;
  • join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;

2.1.5 Thread类与Runnable接口的比较:

实现一个自定义的线程类,可以有继承Thread类或者实现Runnable接口这两种方式,它们之间有什么优劣呢?

  • 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
  • Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
  • Runnable接口出现,降低了线程对象和线程任务的耦合性。
  • 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。

2.2 Callable、Future与FutureTask

通常来说,我们使用RunnableThread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。

JDK提供了Callable接口与Future类为我们解决这个问题,这也是所谓的“异步”模型。

2.2.1 Callable接口

CallableRunnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型

 
  1. @FunctionalInterface
  2. public interface Callable<V> {
  3. V call() throws Exception;
  4. }

那一般是怎么使用Callable的呢?Callable一般是配合线程池工具ExecutorService来使用的。我们会在后续章节解释线程池的使用。这里只介绍ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Futureget方法得到结果。

这里可以看一个简单的使用demo:

 
  1. // 自定义Callable
  2. class Task implements Callable<Integer>{
  3. @Override
  4. public Integer call() throws Exception {
  5. // 模拟计算需要一秒
  6. Thread.sleep(1000);
  7. return 2;
  8. }
  9. public static void main(String args[]){
  10. // 使用
  11. ExecutorService executor = Executors.newCachedThreadPool();
  12. Task task = new Task();
  13. Future<Integer> result = executor.submit(task);
  14. // 注意调用get方法会阻塞当前线程,直到得到结果。
  15. // 所以实际编码中建议使用可以设置超时时间的重载get方法。
  16. System.out.println(result.get());
  17. }
  18. }

输出结果:

 
  1. 2

2.2.2 Future接口

Future接口只有几个比较简单的方法:

 
  1. public abstract interface Future<V> {
  2. public abstract boolean cancel(boolean paramBoolean);
  3. public abstract boolean isCancelled();
  4. public abstract boolean isDone();
  5. public abstract V get() throws InterruptedException, ExecutionException;
  6. public abstract V get(long paramLong, TimeUnit paramTimeUnit)
  7. throws InterruptedException, ExecutionException, TimeoutException;
  8. }

cancel方法是试图取消一个线程的执行。

注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean类型的返回值是“是否取消成功”的意思。参数paramBoolean表示是否采用中断的方式取消线程执行。

所以有时候,为了让任务有能够取消的功能,就使用Callable来代替Runnable。如果为了可取消性而使用 Future但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回 null作为底层任务的结果。

2.2.3 FutureTask类

上面介绍了Future接口。这个接口有一个实现类叫FutureTaskFutureTask是实现的RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口:

 
  1. public interface RunnableFuture<V> extends Runnable, Future<V> {
  2. /**
  3. * Sets this Future to the result of its computation
  4. * unless it has been cancelled.
  5. */
  6. void run();
  7. }

FutureTask类有什么用?为什么要有一个FutureTask类?前面说到了Future只是一个接口,而它里面的cancelgetisDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。

示例代码:

 
  1. // 自定义Callable,与上面一样
  2. class Task implements Callable<Integer>{
  3. @Override
  4. public Integer call() throws Exception {
  5. // 模拟计算需要一秒
  6. Thread.sleep(1000);
  7. return 2;
  8. }
  9. public static void main(String args[]){
  10. // 使用
  11. ExecutorService executor = Executors.newCachedThreadPool();
  12. FutureTask<Integer> futureTask = new FutureTask<>(new Task());
  13. executor.submit(futureTask);
  14. System.out.println(futureTask.get());
  15. }
  16. }

使用上与第一个Demo有一点小的区别。首先,调用submit方法是没有返回值的。这里实际上是调用的submit(Runnable task)方法,而上面的Demo,调用的是submit(Callable<T> task)方法。

然后,这里是使用FutureTask直接取get取值,而上面的Demo是通过submit方法返回的Future去取值。

在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。这块有兴趣的同学可以参看FutureTask源码。

2.2.4 FutureTask的几个状态

 
  1. /**
  2. *
  3. * state可能的状态转变路径如下:
  4. * NEW -> COMPLETING -> NORMAL
  5. * NEW -> COMPLETING -> EXCEPTIONAL
  6. * NEW -> CANCELLED
  7. * NEW -> INTERRUPTING -> INTERRUPTED
  8. */
  9. private volatile int state;
  10. private static final int NEW = 0;
  11. private static final int COMPLETING = 1;
  12. private static final int NORMAL = 2;
  13. private static final int EXCEPTIONAL = 3;
  14. private static final int CANCELLED = 4;
  15. private static final int INTERRUPTING = 5;
  16. private static final int INTERRUPTED = 6;

state表示任务的运行状态,初始状态为NEW。运行状态只会在set、setException、cancel方法中终止。COMPLETING、INTERRUPTING是任务完成后的瞬时状态。

以上就是Java多线程几个基本的类和接口的介绍。可以打开JDK看看源码,体会这几个类的设计思路和用途吧!

package thread;

/**
 * 类说明
 *
 * @author Zeng zhiqiang
 * @version V1.0 创建时间: 2021/3/31 17:36
 * Copyright 2021 by WiteMedia
 */
public class Demo {

    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("hello thread");
        }
    }

    public static  class MyRunnableThread implements Runnable{
        @Override
        public void run() {
            System.out.println("hello runnable thread");
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();

        MyRunnableThread myRunnableThread = new MyRunnableThread();
        myRunnableThread.run();
        //为啥没有Start()方法
new Thread(myRunnableThread).start();
        //run 与start 的区别

        new Thread(()->{
            System.out.println("inner class");
        }).start();
    }


}
package thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

/**
 * 类说明
 *
 * @author Zeng zhiqiang
 * @version V1.0 创建时间: 2021/3/31 18:21
 * Copyright 2021 by WiteMedia
 */
public class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("count now ... ");
        Thread.sleep(2000);
        return 2;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new CallableTask());
        executorService.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

 

https://blog.csdn.net/sinat_39634657/article/details/81449122

1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
2.run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

 

第三章 线程组和线程优先级

3.1 线程组(ThreadGroup)

Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。

ThreadGroup和Thread的关系就如同他们的字面意思一样简单粗暴,每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。

示例代码:

 
  1. public class Demo {
  2. public static void main(String[] args) {
  3. Thread testThread = new Thread(() -> {
  4. System.out.println("testThread当前线程组名字:" +
  5. Thread.currentThread().getThreadGroup().getName());
  6. System.out.println("testThread线程名字:" +
  7. Thread.currentThread().getName());
  8. });
  9.  
  10. testThread.start();
  11. System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
  12. }
  13. }

输出结果:

 
  1. 执行main方法线程名字:main
  2. testThread当前线程组名字:main
  3. testThread线程名字:Thread-0

ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用的树状结构,这样设计的原因是防止”上级”线程被”下级”线程引用而无法有效地被GC回收

3.2 线程的优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。

Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。

通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread类的setPriority()实例方法来设定线程的优先级。

 
  1. public class Demo {
  2. public static void main(String[] args) {
  3. Thread a = new Thread();
  4. System.out.println("我是默认线程优先级:"+a.getPriority());
  5. Thread b = new Thread();
  6. b.setPriority(10);
  7. System.out.println("我是设置过的线程优先级:"+b.getPriority());
  8. }
  9. }

输出结果:

 
  1. 我是默认线程优先级:5
  2. 我是设置过的线程优先级:10

既然有1-10的级别来设定了线程的优先级,这时候可能有些读者会问,那么我是不是可以在业务实现的时候,采用这种方法来指定一些线程执行的先后顺序?

对于这个问题,我们的答案是:No!

Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的

我们通过代码来验证一下:

 
  1. public class Demo {
  2. public static class T1 extends Thread {
  3. @Override
  4. public void run() {
  5. super.run();
  6. System.out.println(String.format("当前执行的线程是:%s,优先级:%d",
  7. Thread.currentThread().getName(),
  8. Thread.currentThread().getPriority()));
  9. }
  10. }
  11.  
  12. public static void main(String[] args) {
  13. IntStream.range(1, 10).forEach(i -> {
  14. Thread thread = new Thread(new T1());
  15. thread.setPriority(i);
  16. thread.start();
  17. });
  18. }
  19. }

某次输出:

 
  1. 当前执行的线程是:Thread-17,优先级:9
  2. 当前执行的线程是:Thread-1,优先级:1
  3. 当前执行的线程是:Thread-13,优先级:7
  4. 当前执行的线程是:Thread-11,优先级:6
  5. 当前执行的线程是:Thread-15,优先级:8
  6. 当前执行的线程是:Thread-7,优先级:4
  7. 当前执行的线程是:Thread-9,优先级:5
  8. 当前执行的线程是:Thread-3,优先级:2
  9. 当前执行的线程是:Thread-5,优先级:3

Java提供一个线程调度器来监视和控制处于RUNNABLE状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有一个默认的主线程,就是通过JVM启动的第一个线程main线程。

还有一种线程称为守护线程(Daemon),守护线程默认的优先级比较低。

如果某线程是守护线程,那如果所有的非守护线程结束,这个守护线程也会自动结束。

应用场景是:当所有非守护线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。

一个线程默认是非守护线程,可以通过Thread类的setDaemon(boolean on)来设置。

在之前,我们有谈到一个线程必然存在于一个线程组中,那么当线程和线程组的优先级不一致的时候将会怎样呢?我们用下面的案例来验证一下:

 
  1. public static void main(String[] args) {
  2. ThreadGroup threadGroup = new ThreadGroup("t1");
  3. threadGroup.setMaxPriority(6);
  4. Thread thread = new Thread(threadGroup,"thread");
  5. thread.setPriority(9);
  6. System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority());
  7. System.out.println("我是线程的优先级"+thread.getPriority());
  8. }

输出:

我是线程组的优先级6
我是线程的优先级6

所以,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。

3.3 线程组的常用方法及数据结构

3.3.1 线程组的常用方法

获取当前的线程组名字

 
  1. Thread.currentThread().getThreadGroup().getName()

复制线程组

 
  1. // 复制一个线程数组到一个线程组
  2. Thread[] threads = new Thread[threadGroup.activeCount()];
  3. TheadGroup threadGroup = new ThreadGroup();
  4. threadGroup.enumerate(threads);

线程组统一异常处理

 
  1. package com.func.axc.threadgroup;
  2.  
  3. public class ThreadGroupDemo {
  4. public static void main(String[] args) {
  5. ThreadGroup threadGroup1 = new ThreadGroup("group1") {
  6. // 继承ThreadGroup并重新定义以下方法
  7. // 在线程成员抛出unchecked exception
  8. // 会执行此方法
  9. public void uncaughtException(Thread t, Throwable e) {
  10. System.out.println(t.getName() + ": " + e.getMessage());
  11. }
  12. };
  13.  
  14. // 这个线程是threadGroup1的一员
  15. Thread thread1 = new Thread(threadGroup1, new Runnable() {
  16. public void run() {
  17. // 抛出unchecked异常
  18. throw new RuntimeException("测试异常");
  19. }
  20. });
  21.  
  22. thread1.start();
  23. }
  24. }

3.3.2 线程组的数据结构

线程组还可以包含其他的线程组,不仅仅是线程。

首先看看 ThreadGroup源码中的成员变量

 
  1. public class ThreadGroup implements Thread.UncaughtExceptionHandler {
  2. private final ThreadGroup parent; // 父亲ThreadGroup
  3. String name; // ThreadGroupr 的名称
  4. int maxPriority; // 线程最大优先级
  5. boolean destroyed; // 是否被销毁
  6. boolean daemon; // 是否守护线程
  7. boolean vmAllowSuspension; // 是否可以中断
  8.  
  9. int nUnstartedThreads = 0; // 还未启动的线程
  10. int nthreads; // ThreadGroup中线程数目
  11. Thread threads[]; // ThreadGroup中的线程
  12.  
  13. int ngroups; // 线程组数目
  14. ThreadGroup groups[]; // 线程组数组
  15. }

然后看看构造函数:

 
  1. // 私有构造函数
  2. private ThreadGroup() {
  3. this.name = "system";
  4. this.maxPriority = Thread.MAX_PRIORITY;
  5. this.parent = null;
  6. }
  7.  
  8. // 默认是以当前ThreadGroup传入作为parent ThreadGroup,新线程组的父线程组是目前正在运行线程的线程组。
  9. public ThreadGroup(String name) {
  10. this(Thread.currentThread().getThreadGroup(), name);
  11. }
  12.  
  13. // 构造函数
  14. public ThreadGroup(ThreadGroup parent, String name) {
  15. this(checkParentAccess(parent), parent, name);
  16. }
  17.  
  18. // 私有构造函数,主要的构造函数
  19. private ThreadGroup(Void unused, ThreadGroup parent, String name) {
  20. this.name = name;
  21. this.maxPriority = parent.maxPriority;
  22. this.daemon = parent.daemon;
  23. this.vmAllowSuspension = parent.vmAllowSuspension;
  24. this.parent = parent;
  25. parent.add(this);
  26. }

第三个构造函数里调用了checkParentAccess方法,这里看看这个方法的源码:

 
  1. // 检查parent ThreadGroup
  2. private static Void checkParentAccess(ThreadGroup parent) {
  3. parent.checkAccess();
  4. return null;
  5. }
  6.  
  7. // 判断当前运行的线程是否具有修改线程组的权限
  8. public final void checkAccess() {
  9. SecurityManager security = System.getSecurityManager();
  10. if (security != null) {
  11. security.checkAccess(this);
  12. }
  13. }

这里涉及到SecurityManager这个类,它是Java的安全管理器,它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否是在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。

比如引入了第三方类库,但是并不能保证它的安全性。

其实Thread类也有一个checkAccess()方法,不过是用来当前运行的线程是否有权限修改被调用的这个线程实例。(Determines if the currently running thread has permission to modify this thread.)

总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值