首页 > 代码库 > 【Java并发系列01】Thread及ThreadGroup杂谈

【Java并发系列01】Thread及ThreadGroup杂谈

<style>img { border: solid black 1px }</style>

一、前言

  最近开始学习Java并发编程,把学习过程记录下。估计不是那么系统,主要应该是Java API的介绍(不涉及最基础的概念介绍),想要深入系统学习推荐看一本书《Java Concurrency in Practice 》(建议看英文,也可以看中文译本:《 Java 并发编程实战》)。

  并发编程的基础就是线程,所以这一篇对线程做初步了解。

二、Thread和ThredGroup的关系

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

技术分享

  其中有一点要明确一下:根线程组不需要创建,执行main方法就自动创建根线程组并将main线程放置其中

三、Thread API

技术分享

3.1 基本属性

  首先应该了解线程的基本属性:

  • 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:未捕获异常时的处理器,默认没有,线程出现错误后会立即终止当前线程运行,并打印错误。

3.2 字段摘要

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

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

3.3 构造方法

  现只介绍参数最多的一个:

  Thread(ThreadGroup group, Runnable target, String name, long stackSize) 

  • group:指定当前线程的线程组,未指定时线程组为创建该线程所属的线程组。
  • target:指定运行其中的Runnable,一般都需要指定,不指定的线程没有意义,或者可以通过创建Thread的子类并重新run方法。
  • name:线程的名称,不指定自动生成。
  • stackSize:预期堆栈大小,不指定默认为0,0代表忽略这个属性。与平台相关,不建议使用该属性。

3.4 方法摘要

3.4.1 静态方法

  • Thread Thread.currentThread() :获得当前线程的引用。获得当前线程后对其进行操作。
  • Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
  • int Thread.activeCount():当前线程所在线程组中活动线程的数目。
  • void dumpStack() :将当前线程的堆栈跟踪打印至标准错误流。
  • int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
  • Map<Thread,StackTraceElement[]> 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() :暂停当前正在执行的线程对象,并执行其他线程。意义不太大

3.4.2 普通方法

  • 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():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

3.4.3 作废方法

  • int countStackFrames() :没有意义不做解释。
  • void destroy() :
  • void resume() :
  • void stop() :
  • void stop(Throwable obj) :
  • void suspend() :

3.5 Thread知识方法讲解

3.5.1 setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 

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

public class ThreadTest{
    public static void main(String[] args) throws Exception{
        Thread t1=new Thread(new Runnable(){
            public void run(){
                int a=1/0;
            }
        });
        t1.start();
    }
}

  那么代码运行到int a=1/0;就会报错:

Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
    at yiwangzhibujian.ThreadTest$1.run(ThreadTest.java:11)
    at java.lang.Thread.run(Thread.java:662)

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

public class ThreadTest{
    public static void main(String[] args) throws Exception{
        Thread t1=new Thread(new Runnable(){
            public void run(){
                int a=1/0;
            }
        });
        t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){
            @Override
            public void uncaughtException(Thread t,Throwable e){
                System.out.println("线程:"+t.getName()+"出现异常,异常信息:"+e);
            }
        });
        t1.start();
    }
}

  那么当线程抛出异常后就可以对其抓取并进行处理,最终结果如下:

线程:Thread-0出现异常,异常信息:java.lang.ArithmeticException: / by zero

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

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

  因为stop()方法已经不建议使用了,所以如何中断一个线程就成了一个问题,一种简单的办法是设置一个全局变量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。

  这样就由程序来决定怎么对线程中断进行处理。因此,上面的代码可以改成:

@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()的方法名容易给人一种误解,它的真实含义是

3.5.3 yield()和sleep(0)

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

public class ThreadTest{
    public static void main(String[] args) throws Exception{
        Thread t1=new Thread(new Runnable(){
            @Override
            public void run(){
                while(true){
                    System.out.println(1);
                    Thread.yield();
                }
            }
        });
        Thread t2=new Thread(new Runnable(){
            public void run(){
                while(true){
                    System.out.println(2);
                    Thread.yield();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

  程序执行结果并不是121212而是有,有连续的1和连续的2。

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

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

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

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

  如果在调用stop时,代码运行到捕获异常需要回滚的地方,那么将会产出错误信息。

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

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

四、ThreadGroup API

技术分享

4.1 基本属性

  name:当前线程的名称。

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

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

4.2 构造方法

  只介绍一个构造方法:

  ThreadGroup(ThreadGroup parent, String name) :

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

4.3 常用方法摘要

  API详解(中文,英文)。

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

4.4 ThreadGroup作用

  这个线程组可以用来管理一组线程,通过activeCount() 来查看活动线程的数量。  

 

  这篇博客讲解了最基本的线程及线程组,这是并发编程的根基,应该全面了解。

  未经许可禁止转载。

【Java并发系列01】Thread及ThreadGroup杂谈