并发编程基础 之 Thread 与ThreadGroup的介绍(api介绍)

一.Thread 和ThreadGroup的关系

因为Thread的构造函数中有关于ThradGroup的,所以了解它们之间的关系是有必要的。ThradGroup之间的关系是树的关系而Thread与ThradGroup的关系就像元素与集合的关系。关系如下:

├─ ThreadGroup[name=system,maxpri=10]
│  ├─Thread[Reference Handler,10,system]
│  ├─Thread[Finalizer,8,system]
│  ├─Thread[Signal Dispatcher,9,system]
│  ├─Thread[Attach Listener,5,system]
│  ├─ThreadGroup[name=main,maxpri=10]
│  │  ├─Thread[main,5,main]
│  │  ├─Thread[Monitor Ctrl-Break,5,main]
│  │  ├─Thread[Thread1,5,main]                     // 这个线程是自定义的线程
│  │  ├─ThreadGroup[name=MyThreadGroup,maxpri=10]  // 自定义线程组
│  │      └─Thread[MyThread2,5,MyThreadGroup]      // 自定义线程,添加到指定组

贴上代码:

    @Test
    public void test02(){
        // 创建一个线程,不指定线程组,由系统自动分配
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Object local = new Object();
                synchronized (local){
                    try {
                        local.wait();
                    } catch (InterruptedException e) {
                        //e.printStackTrace();
                    }

                }
            }
        });
        // 设置线程名称
        thread1.setName("Thread1");

        // 创建一个线程组
        ThreadGroup threadGroup = new ThreadGroup("MyThreadGroup");
        // 指定创建一个线程,并且指定线程组
        Thread thread2 = new Thread(threadGroup, new Runnable() {
            @Override
            public void run() {
                Object local = new Object();
                try {
                    local.wait();
                } catch (InterruptedException e) {
                    // 不处理异常
                }
            }
        }, "MyThread2");

        // 启动两个线程
        thread1.start();
        thread2.start();

        // 获取主线程的父线程组
        ThreadGroup parentThreadGroup = mainThreadGroup.getParent();
        // 将有关此线程组的信息打印到标准输出。此方法仅对调试有用。
        parentThreadGroup.list();
    }
/*
以下是打印的结果
java.lang.ThreadGroup[name=system,maxpri=10]
    Thread[Reference Handler,10,system]
    Thread[Finalizer,8,system]
    Thread[Signal Dispatcher,9,system]
    Thread[Attach Listener,5,system]
    java.lang.ThreadGroup[name=main,maxpri=10]
        Thread[main,5,main]
        Thread[Monitor Ctrl-Break,5,main]
        Thread[Thread1,5,main]
        java.lang.ThreadGroup[name=MyThreadGroup,maxpri=10]
            Thread[MyThread2,5,MyThreadGroup]
*/

其中要明确一下: main方法执行后,将自动创建system线程组合main线程组,main方法所在线程存放在main线程组中

二.Thread API

并发编程基础 之 Thread 与ThreadGroup的介绍(api介绍)_第1张图片

2.1 基本属性

thread 基本属性
属性 基本说明
name 线程名称,可以重复,若没有指定会自动生成
id 线程ID,一个正long值,创建线程时指定,终生不变,线程终结时ID可以复用。
priority 线程优先级,取值为1到10,线程优先级越高,执行的可能越大,若运行环境不支持优先级分10级,如只支持5级,那么设置5和设置6有可能是一样的。
state 线程状态,Thread.State枚举类型,有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 5种。
ThreadGroup 所属线程组,一个线程必然有所属线程组
UncaughtExceptionHandler 未捕获异常时的处理器,默认没有,线程出现错误后会立即终止当前线程运行,并打印错误。

2.2 基本属性

Thread类有三个字段,设置线程优先级时可使用:

  • MIN_PRIORITY:1,最低优先级
  • NORM_PRIORITY:5,普通优先级
  • MAX_PRIORITY:10,最高优先级

2.3 构造方法

public Thread(ThreadGroup group,
              Runnable target,
              String name,
              long stackSize)
创建一个新的Thread对象,以便它具有target作为其运行对象,将指定的name,以及属于该线程组group ,并具有指定的堆栈大小 。
这个构造函数与Thread(ThreadGroup,Runnable,String)相同,除了它允许指定线程栈大小之外。 堆栈大小是虚拟机为该线程的堆栈分配的大致的地址空间字节数。 stackSize参数的影响(如果有的话)与平台有关。

在某些平台上,指定了一个较高的值stackSize参数可以允许抛出一个前一个线程来实现更大的递归深度StackOverflowError 。 类似地,指定较低的值可能允许更多数量的线程同时存在,而不会抛出OutOfMemoryError (或其他内部错误)。 所述stackSize参数的值和最大递归深度和并发水平之间的关系的细节是依赖于平台的。 在某些平台上,该值stackSize参数可能没有任何效果。

虚拟机可以自由地对待stackSize参数作为建议。 如果平台的指定值不合理地低,虚拟机可能会改为使用一些平台特定的最小值; 如果指定的值不合理地高,虚拟机可能会使用一些平台特定的最大值。 同样,虚拟机可以自由地按照合适的方式向上或向下舍入指定的值(或完全忽略它)。

对于指定的值为零stackSize参数将使这种构造的行为酷似Thread(ThreadGroup, Runnable, String)构造。

由于此构造函数的行为依赖于平台依赖性质,因此在使用时应特别小心。 执行给定计算所需的线程栈大小可能会因JRE实现而异。 鉴于这种变化,可能需要仔细调整堆栈大小参数,并且可能需要对要运行应用程序的每个JRE实现重复调整。

实现注意事项:鼓励Java平台实现者的记录其实施的行为stackSize参数。

参数
group     - 线程组。 如果null并且有一个安全管理器,该组由SecurityManager.getThreadGroup()确定 。 如果没有安全管理员或SecurityManager.getThreadGroup()返回null ,该组将设置为当前线程的线程组。
target    - 启动此线程时调用其run方法的对象。 如果null ,这个线程的run方法被调用。
name      - 新线程的名称
stackSize - 新线程所需的堆栈大小,或为零表示此参数将被忽略。
异常
SecurityException - 如果当前线程无法在指定线程组中创建线程
从以下版本开始:
1.4

2.4 静态方法摘要

  • Thread Thread.currentThread() :获得当前线程的引用。获得当前线程后对其进行操作。
  • Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
  • int Thread.activeCount():当前线程所在线程组中活动线程的数目。
  • void dumpStack() :将当前线程的堆栈跟踪打印至标准错误流。
  • int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
  • Map getAllStackTraces() :返回所有活动线程的堆栈跟踪的一个映射。
  • boolean holdsLock(Object obj) :当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
  • boolean interrupted() :测试当前线程是否已经中断。
  • void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) :设置当线程由于未捕获到异常而突然终止,并且没有为该线程定义其他处理程序时所调用的默认处理程序。
  • void sleep(long millis) :休眠指定时间
  • void sleep(long millis, int nanos) :休眠指定时间
  • void yield() :暂停当前正在执行的线程对象,并执行其他线程。意义不太大

2.5 普通方法摘要

  • void checkAccess() :判定当前运行的线程是否有权修改该线程。
  • ClassLoader getContextClassLoader() :返回该线程的上下文 ClassLoader。
  • long getId() :返回该线程的标识符。
  • String getName() :返回该线程的名称。
  • int getPriority() :返回线程的优先级。
  • StackTraceElement[] getStackTrace() :返回一个表示该线程堆栈转储的堆栈跟踪元素数组。
  • Thread.State getState() :返回该线程的状态。
  • ThreadGroup getThreadGroup() :返回该线程所属的线程组。
  • Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() :返回该线程由于未捕获到异常而突然终止时调用的处理程序。
  • void interrupt() :中断线程。
  • boolean isAlive() :测试线程是否处于活动状态。
  • boolean isDaemon() :测试该线程是否为守护线程。
  • boolean isInterrupted():测试线程是否已经中断。
  • void join() :等待该线程终止。
  • void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。
  • void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
  • void run() :线程启动后执行的方法。
  • void setContextClassLoader(ClassLoader cl) :设置该线程的上下文 ClassLoader。
  • void setDaemon(boolean on) :将该线程标记为守护线程或用户线程。
  • void start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
  • String toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

2.6 作废方法

  • int countStackFrames() :没有意义不做解释。
  • void destroy() :破坏线程,不释放锁,已经不能再使用,使用会抛出NoSuchMethodError。 
  • void suspend() :挂起线程,不要使用。
  • void resume() :恢复线程,不要使用。
  • void stop() :停止线程释放锁,不要使用。
  • void stop(Throwable obj) :同上。

2.7 Thread 方法的讲解

2.7.1 setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 

  首先要了解什么是Thread.UncaughtExceptionHandler,默认来说当线程出现未捕获的异常时,会中断并抛出异常,抛出后的动作只有简单的堆栈输出。如:

   @Test
    public void thread01(){
        /**
         * 1. 了解Thread.UncaughtExceptionHandler
         */
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 1 / 0;
            }
        });
        // 启动t1线程
        t1.start();
        /*
        那么代码运行到int a=1/0;就会报错:
        Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
            at dreamhai.thread.MyThread01$1.run(MyThread01.java:25)
            at java.lang.Thread.run(Thread.java:745)
         */
    }

这时候如果设置了Thread.UncaughtExceptionHandler,那么处理器会将异常进行捕获,捕获后就可以对其进行处理:

    @Test
    public void thread02() {

        /**
         *  自定义UncaughtExceptionHandler
         *  测试,当线程在运行时,出现了异常,会根据我们自己的逻辑来处理异常
         */
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 1 / 0;
            }
        });
        // 设置线程的名称
        t2.setName("t2线程");

        // 设置UncaughtExceptionHandler
        t2.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("线程 : [" + t2.getName() + "] 出现了异常, 异常信息:" + e);
            }
        });
        // 启动t2线程
        t2.start();
        /*
        线程 : Thread-0出现了异常, 异常信息:java.lang.ArithmeticException: / by zero
         */
    }

如果自己写线程,那么完全可以在run方法内,将所有代码进行try catch,在catch里做相同的操作。UncaughtExceptionHandler的意义在于不对(或者不能对)原有线程进行修改的情况下,为其增加一个错误处理器。

2.7.2 interrupt() 、interrupted() 、isInterrupted()作用

  因为stop()方法已经不建议使用了,下面的3.5.4进行详解,所以如何中断一个线程就成了一个问题,一种简单的办法是设置一个全局变量needStop,如下:

@Override
public void run(){
    while(!needStop){
        //执行某些任务
    }
}

或者需要操作耗时较长的方法内,每一步执行之前进行判断:

@Override
public void run(){
    //耗时较长步骤1
    if(needStop) return;
    //耗时较长步骤2
    if(needStop) return;
    //耗时较长步骤3
}

这样在其他的地方将此线程停止掉,因为停止是在自己的预料下,所以不会有死锁或者数据异常问题(当然你的程序编写的时候要注意)。

  其实Thread类早就有类似的功能,那就是Thread具有中断属性。可以通过调用interrupt()方法对线程中断属性设置为true,这将导致如下两种情况:

  • 当线程正常运行时,中断属性设置为true,调用其isInterrupted()方法会返回true。
  • 当线程阻塞时(wait,join,sleep方法),会立即抛出InterruptedException异常,并将中断属性设置为false。此时再调用isInterrupted()会返回false。

  这样就由程序来决定当检测到中断属性为true时,怎么对线程中断进行处理。因此,上面的代码可以改成:

@Override
public void run(){
    while(!Thread.currentThread().isInterrupted()){
        //执行某些任务
    }
}
---------------------------------------------------------
@Override
public void run(){
    //耗时较长步骤1
    if(Thread.currentThread().isInterrupted()) return;
    //耗时较长步骤2
    if(Thread.currentThread().isInterrupted()) return;
    //耗时较长步骤3
}

interrupted()的方法名容易给人一种误解,看似和interrupt()方法一样,但是其实际含义是,返回当前中断状态,并将其设置为false。

2.7.3 yield()和sleep(0)

  yield()方法的API容易给人一种误解,它的实际含义是停止执行当前线程(立即),让CPU重新选择需要执行的线程,因为具有随机性,所以也有可能重新执行该线程,通过下面例子了解:

    /**
     * yield()
     * 含义: 指的是,停止当前的线程(立即),让CPU重新选择需要执行的线程,
     * 因为具有随机性,所有也有可能重新执行该线程
     */
    @Test
    public void thread03(){
        Thread t1 = new Thread(() -> {
            while (true){
                System.out.println("当前线程id:["+Thread.currentThread().getId()+
                        "] 当前线程名称: "+Thread.currentThread().getName());
                Thread.yield();
            }
        });

        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("当前线程id:["+Thread.currentThread().getId()+
                        "] 当前线程名称: "+Thread.currentThread().getName());
                Thread.yield();
            }
        });

        // 分别给t1, t2 线程设置名字,
        t1.setName("t1--青海");
        t2.setName("t2--Dreamhai");

        // 启动两个线程
        t1.start();
        t2.start();

        /*
            打印结果:
                当前线程id:[11] 当前线程名称: t1--青海
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[11] 当前线程名称: t1--青海
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[11] 当前线程名称: t1--青海
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[11] 当前线程名称: t1--青海
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[11] 当前线程名称: t1--青海
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[11] 当前线程名称: t1--青海
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[12] 当前线程名称: t2--Dreamhai
                当前线程id:[11] 当前线程名称: t1--青海
            由此可见,并不是交替打印的,出现了连续的Dreamhai
            (多执行几次更容易看到效果)
         */
    }

经过测试yield()和sleep(0)的效果是一样的,sleep(0)底层要么是和yield()一样,要么被过滤掉了(纯靠猜测),不过sleep(0)没有任何意义。要是真打算让当前线程暂停还是应该使用sleep(long millis,int nanos)这个方法,设置几纳秒表示下诚意,或者找到想要让步的线程,调用它的join方法更实际一些。

2.7.4 stop()、suspend()、resume()为什么不建议使用

   stop方法会立即中断线程,虽然会释放持有的锁,但是线程的运行到哪是未知的,假如在具有上下文语义的位置中断了,那么将会导致信息出现错误,比如:

@Override
public void run(){
    try{
        //处理资源并插入数据库
    }catch(Exception e){
        //出现异常回滚
    }
}

如果在调用stop时,代码运行到捕获异常需要回滚的地方,那么将会因为没有回滚,保存了错误的信息。

  而suspend会将当前线程挂起,但是并不会释放所持有的资源,如果恢复线程在调用resume也需要那个资源,那么就会形成死锁。当然可以通过你精湛的编程来避免死锁,但是这个方法具有固有的死锁倾向。所以不建议使用。其他暂停方法为什么可用:

  • wait方法会释放锁,所以不会有死锁问题
  • sleep方法虽然不释放锁,但是它不需要唤醒,在使用的时候已经指定想要的睡眠时间了。

  jdk的文章详细介绍了方法禁用的原因:文章地址,有空可以看一看,如果你足够大胆,也是可以使用的。

三. ThreadGroup API

并发编程基础 之 Thread 与ThreadGroup的介绍(api介绍)_第2张图片

3.1 基本属性

  name:当前线程的名称。

  parent:当前线程组的父线程组。

  MaxPriority:当前线程组的最高优先级,其中的线程优先级不能高于此。

3.2 构造方法

  只介绍一个构造方法:

  ThreadGroup(ThreadGroup parent, String name) :

  • parent:父线程组,若为指定则是创建该线程组的线程所需的线程组。
  • name:线程组的名称,可重复。

3.3 常用方法摘要

  API详解(中文,英文)。

  • int activeCount():返回此线程组中活动线程的估计数。 
  • void interrupt():中断此线程组中的所有线程。 
  • void uncaughtException(Thread t, Throwable e) :设置当前线程组的异常处理器(只对没有异常处理器的线程有效)。

3.4 ThreadGroup作用

  这个线程组可以用来管理一组线程,通过activeCount() 来查看活动线程的数量。其他没有什么大的用处。

 

你可能感兴趣的:(并发编程,并发编程)