coding……
但行好事 莫问前程

Java编程拾遗『线程的使用』

上篇文章讲了一些关于并发编程的一些基本概念,也简单提到Java中线程的创建方式,本篇文章将详细介绍一下Java中线程使用的相关细节。

1. 创建线程

如果程序中只有一条执行流,程序从main方法的第一条语句逐条执行直到结束。如果在main线程中创建另外创建一个线程,并启动,则表示在main执行流之外另外开辟一条单独的执行流,它有自己的程序执行计数器,有自己的栈。Java中可以通过三种方式创建线程,分别为继承Thread类、实现Runnable接口、实现Callable接口。

1.1 继承Thread类

Java中java.lang.Thread这个类表示线程,一个类可以继承Thread并重写其run方法来实现一个线程,如下所示:

public class MyThread extends Thread {
   public void run(){
     System.out.println("MyThread running");
   }
}

MyThread这个类继承了Thread,并重写了run方法。run方法的方法签名是固定的,public,没有参数,没有返回值,不能抛出受检异常。run方法类似于单线程程序中的main方法,线程从run方法的第一条语句开始执行直到结束。

定义了这个类仅仅是定义了执行流需要执行的工作,线程需要被启动(执行流真正运行)后才能真正开始工作,启动需要先创建一个MyThread对象,然后调用Thread的start方法,如下所示:

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

我们在main方法中创建了一个线程对象,并调用了其start方法,调用start方法后,MyThread的run方法就会开始执行,屏幕输出:

MyThread running

为什么调用的是start,执行的却是run方法呢?start表示启动该线程,使其成为一条单独的执行流,背后,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行,执行的起点就是run方法

如果不调用start,而直接调用run方法呢?屏幕的输出并不会发生变化,但并不会启动一条单独的执行流,run方法的代码依然是在main线程中执行的,run方法只是main方法调用的一个普通方法

1.1.1 Thread类基本方法说明

S.N.方法说明
0.public synchronized void start()启动线程
1.public void run()定义线程的执行内容
2.public long getId()获取线程的id
3.public final String getName()获取线程的名称
4.public final void setPriority(int newPriority)设置线程的优先级
5.public final int getPriority()获取线程的优先级
6.public State getState()获取线程的状态
7.public final native boolean isAlive()判断线程是否存活,线程被启动后,run方法运行结束前,返回值都是true
8.public final boolean isDaemon()判断线程是否为守护线程
9.public final void setDaemon(boolean on)将线程设置为守护线程,对于运行中的线程,调用Thread.setDaemon()会抛出异常IllegalThreadStateException
10.public static native void sleep(long millis) throws InterruptedException让当前线程睡眠指定的时间,单位是毫秒
11.public static void sleep(long millis, int nanos) throws InterruptedException让当前线程睡眠指定时间
12.public static native void yield()调用该方法的线程让出CPU
13.public final synchronized void join(long millis) throws InterruptedException调用方线程线程等待join方法所属的线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
14.public final synchronized void join(long millis, int nanos) throws InterruptedException调用方线程线程等待join方法所属的线程终止的时间最长为 millis 毫秒 + nanos纳秒。如果在最长时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
15.public final void join() throws InterruptedException 调用方线程(调用join方法的线程)执行等待操作,直到被调用的线程(join方法所属的线程)结束,再被唤醒。比如线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B
  • run() & start()

run方法可以用来定义线程执行的内容,start方法用来启动线程

  • getId() & getName()

每个线程都有一个id和name,id是一个递增的整数,每创建一个线程就加一,name的默认值是”Thread-“后跟一个编号,name可以在Thread的构造方法中进行指定,也可以通过setName方法进行设置。

  • setPriority(int newPriority) & getPriority()

线程有一个优先级的概念,在Java中,优先级从1到10,默认为5,如下:

public final void setPriority(int newPriority)
public final int getPriority()

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。Java中线程的优先级会被映射到操作系统中线程的优先级,不过,因为操作系统各不相同,不一定都是10个优先级,Java中不同的优先级可能会被映射到操作系统中相同的优先级。另外,Java线程设置的优先级对操作系统而言更多的是一种建议和提示,而非强制,简单的说,在编程中,不要过于依赖优先级。

public class TestPriority {

    public static void main(String[] args) {
        try {
            test();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void test() {
        new MyThread("低级", 1).start();
        new MyThread("高级", 10).start();
    }

    static class MyThread extends Thread {
        MyThread(String name, int pro) {
            super(name);//设置线程的名称
            setPriority(pro);//设置线程的优先级
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(this.getName() + "线程第" + i + "次执行!");
            }
        }
    }
}

执行结果可以看到 ,一般情况下,高级线程更显执行完毕。

  • getState()

线程有一个状态的概念,Thread的方法getState就用于获取线程的状态,如下所示:

public State getState()

返回值类型为Thread.State,它是一个枚举类型,有如下值:

public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}
  • NEW: 没有调用start的线程状态为NEW
  • RUNNABLE: 调用start后线程在执行run方法且没有阻塞时状态为RUNNABLE,不过,RUNNABLE不代表CPU一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件
  • BLOCKED、WAITING、TIMED_WAITING:都表示线程被阻塞了,在等待一些条件
  • TERMINATED: 线程运行结束后状态为TERMINATED
  • isAlive()

isAlive()方法用于判断线程是否还存活,线程被启动后,run方法运行结束前,返回值都是true。

  • isDaemon() & setDeamon(boolean on)

之前讲过,启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出。但daemo线程是例外,当整个程序中剩下的都是daemo线程的时候,程序就会退出。

deamon线程一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。在我们运行一个即使最简单的”hello world”类型的程序时,实际上,Java也会创建多个线程,除了main线程外,至少还有一个负责垃圾回收的线程,这个线程就是daemo线程,在main线程结束的时候,垃圾回收线程也会退出。

  • sleep方法

sleep方法可以让线程休眠指定时间,该方法在指定的时间内无法被唤醒,同时也不会释放对象锁。有如下两种形式:

public static native void sleep(long millis) throws InterruptedException
public static native void sleep(long millis, int nanos) throws InterruptedException

比如,我们想要使主线程每休眠100毫秒,然后再打印出数字:

public static void main(String[] args) throws InterruptedException {  
    for(int i=0; i<100; i++){  
        Thread.sleep(100);  
        System.out.println("main"+i);  
    }  
}

需要注意的是:

1.sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。

public static void main(String[] args) throws InterruptedException {  
    System.out.println(Thread.currentThread().getName());  
    MyThread myThread=new MyThread();  
    myThread.start();  
    // 这里sleep的就是main线程,而非myThread线程 
    myThread.sleep(1000); 
    Thread.sleep(10);  
    for(int i=0;i<100;i++){  
        System.out.println("main"+i);  
    }  
}

2.使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

  • yield()

与sleep类似,yield方法也是Thread类提供的一个静态的方法它也可以让当前正在执行的线程暂停,让出CPU资源给其他的线程。但是和sleep()方法不同的是,yield方法不会让线程进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行

sleep()方法和yield()方的区别如下:

1. sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态;

2. sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常;

  • join()

join从字面的意思就是合并的意思,也就是将几个并行线程的线程合并为一个单线程执行。当一个线程必须等待另一个线程执行完毕才能执行时,可以使用join方法完成,join方法有以下几种形式:

//调用方线程(调用join方法的线程)执行等待操作,直到被调用的线程(join方法所属的线程)结束,再被唤醒
public final void join() throws InterruptedException
//调用方线程线程等待join方法所属的线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
public final synchronized void join(long millis) throws InterruptedException
//等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
public final synchronized void join(long millis, int nanos) throws InterruptedException

可以看到,join方法不是静态方法,必须通过Thread实例对象调用

/**
 * 在主线程中调用thread.join(); 就是将主线程加入到thread子线程后面等待执行
 */
public class Test {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread t=new MyThread();  
        t.start();  
        t.join(1);//主线程等待子线程t执行结束后继续执行
        for(int i=0;i<30;i++){  
            System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!");  
        }  
    }  
}  
  
class MyThread extends Thread {  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            System.out.println(this.getName() + "线程第" + i + "次执行!");  
        }  
    }  
}  

在JDK中,join通过如下方式实现:

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

join方法实现是通过调用wait方法实现。当main线程调用t.join时候,main线程会获得线程对象t的锁,调用该对象的wait方法,会让主线程进入等待池,直到该对象唤醒main线程,比如子线程t执行结束推出后。

  • suspend & resume (已过时)

suspend-线程进入阻塞状态,但不会释放锁。此方法已不推荐使用,因为同步时不会释放锁,会造成死锁的问题

resume-使线程重新进入可执行状态

为什么suspend和resume 被废弃了?

suspend天生容易引起死锁。如果目标线程挂起时在保护系统关键资源的监视器上持有锁,那么其他线程在目标线程恢复之前都无法访问这个资源。如果要恢复目标线程的线程在调用resume之前试图锁定这个监视器,死锁就发生了。这种死锁一般自身表现为“冻结( frozen )”进程。

  • stop(已过时)

stop因为其天生是不安全的。停止一个线程会导致解锁其上被锁定的所有监视器(监视器以在栈顶产生ThreadDeath异常的方式被解锁)。如果之前被这些监视器保护的任何对象处于不一致状态,其它线程看到的这些对象就会处于不一致状态。这种对象被称为受损的 (damaged)。当线程在受损的对象上进行操作时,会导致任意行为。这种行为可能微妙且难以检测,也可能会比较明显。

不像其他未受检的(unchecked)异常, ThreadDeath悄无声息的杀死及其他线程。因此,用户得不到程序可能会崩溃的警告。崩溃会在真正破坏发生后的任意时刻显现,甚至在数小时或数天之后。

1.1.2 Object方法wait&notify/notifyAll

上面讲Thread类的join方法时说到,join方法实际是通过wait方法实现的。这里单独介绍一下Object类的这三个方法

1. 调用wait方法后,释放持有的对象锁,线程状态有Running变为Waiting,并将当前线程放置到对象的等待队列

2. 调用notify或者notifyAll方法后,等待线程依旧不会从wait返回,需要调用noitfy的线程释放锁之后,等待线程才有机会从wait返回

3. notify方法会将等待队列的一个等待线程从等待队列种移到同步队列中 ,而notifyAll方法会将等待队列种所有的线程全部移到同步队列,被移动的线程状态由Waiting变为Blocked

这里单独讲一下,wait操作涉及的等待队列(等待池),同步队列(锁池)的区别:

1. 同步队列(锁池):假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的同步队列(锁池)中,这些线程状态为Blocked。

2. 等待队列(等待池):假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时 线程A就进入到了该对象的等待队列(等待池)中,此时线程A状态为Waiting。如果另外的一个线程调用了相同对象的notifyAll()方法,那么 处于该对象的等待池中的线程就会全部进入该对象的同步队列(锁池)中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么 仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的同步队列(锁池)

1.1.3 线程状态流转

前面在讲getState方法时,我们知道Java中定义了线程NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATE这六种状态。除了这六种状态,这里再另外介绍线程的一种状态RUNNING状态,这里就详细介绍一下这七种状态。

  • NEW

当程序使用new关键字创建了一个线程之后,该线程就处于 新建状态,此时的线程情况如下:

1. 此时JVM为其分配内存,并初始化其成员变量的值

2. 此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体

  • RUNNABLE

当线程对象调用了start()方法之后,该线程处于就绪(RUNNABLE)状态。此时的线程情况如下:

1. 此时JVM会为其 创建方法调用栈和程序计数器

2. 该状态的线程一直处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为CPU的调度不一定是按照先进先出的顺序来调度的),线程并没有开始运行

3. 此时线程等待系统为其分配CPU时间片,并不是说执行了start()方法就立即执行

  • RUNNING

当CPU开始调度处于就绪状态的线程时,此时线程获得了CPU时间片才得以真正开始执行run()方法的线程执行体,则该线程处于运行(RUNNING)状态。

1. 如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态

2. 如果在一个多处理器的机器上,将会有多个线程并行执行,处于运行状态

3. 当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象

处于运行状态的线程最为复杂,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。线程状态可能会变为 阻塞状态、就绪状态和死亡状态。比如:

1. 对于采用抢占式调度策略的系统而言,系统会给每个可执行的线程分配一个时间片来处理任务;当该时间片用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。线程就会又从运行状态变为就绪状态,重新等待系统分配资源

2. 对于采用协作式策略的系统而言,只有当一个线程调用了它的yield()方法后才会放弃所占用的资源—也就是必须由该线程主动放弃所占用的资源,线程就会又从运行状态变为就绪状态

  • BLOCKED

处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入阻塞(BLOCKED)状态,当发生如下情况时,线程将会进入阻塞状态:

1. 线程调用sleep()方法,主动放弃所占用的处理器资源,暂时进入中断状态(不会释放持有的对象锁),时间到后等待系统分配CPU继续执行

2. 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有

4. 程序调用了线程的suspend方法将线程挂起

5. 线程调用wait,等待notify/notifyAll唤醒时(会释放持有的对象锁)

阻塞状态分类:

1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态

2. 同步阻塞:线程在 获取synchronized同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态

3. 其他阻塞:通过调用线程的sleep()或join()或发出I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕 时,线程重新转入就绪状态

在阻塞状态的线程只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入阻塞状态但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。

  • WAITING

线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如:

1. 通过wait()方法进行等待的线程等待一个notify()或者notifyAll()方法

2. 通过join()方法进行等待的线程等待目标线程运行结束而唤醒

以上两种一旦通过相关事件唤醒线程,就会进入线程就进入了就绪(RUNNABLE)状态继续运行。

  • TIMED_WAITING

线程进入了一个时限等待状态,如:

sleep(3000),等待3秒后线程重新进行就绪(RUNNABLE)状态继续

wait(3000),线程等待3秒,或在3秒内被notify操作唤醒,进入等锁池,在获取锁之后,线程进入就绪状态

join(3000),调用方线程线程等待join方法所属的线程终止的时间最长为3秒,如果3秒后,join方法所属的线程还未执行结束,则调用方线程继续执行

其实可以发现WAITING和TIMED_WAITING都是BLOCKED的特殊情况,就是上面讲的阻塞的其他阻塞。

  • TERMINATE

线程会以如下3种方式结束,结束后就处于死亡状态

1. 线程正常结束(run()或call()方法执行结束)

2. 线程抛出一个未捕获的Exception或Error

3. 接调用该线程stop()方法来结束该线程,但是该方法容易导致死锁,不推荐使用

处于死亡状态的线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常

一旦线程通过start()方法启动后就再也不能回到新建(NEW)状态,线程终止后也不能再回到就绪(RUNNABLE)状态

1.2 实现Runnable接口

通过继承Thread来实现线程虽然比较简单,但是,Java中只支持单继承,每个类最多只能有一个父类,如果类已经有父类了,就不能再继承Thread,这时,可以通过实现java.lang.Runnable接口来实现线程。Runnable接口的定义很简单,只有一个run方法,如下所示:

public interface Runnable {
    public abstract void run();
}

一个类可以实现该接口,并实现run方法,如下所示:

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("hello");
    }
} 

仅仅实现Runnable是不够的,要启动线程,还是要创建一个Thread对象,但传递一个Runnable对象,如下所示:

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

之后的流程跟直接继承Thread类一致。

1.3 实现Callable接口

无论是继承Thread类还是实现Runnable接口创建线程,都有一个明显的缺陷:在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。所以在Java5之后,就提供了第三种线程创建方式,实现Callable接口。通过Callable和Future,可以在任务执行完毕之后得到任务执行结果。Callable位于java.util.concurrent包下,接口中只有一个call方法,如下:

public interface Callable<V> {
    V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。另外call方法声明中抛出了Exception,所以在方法中允许抛出受检查异常(Checked Exception)。Callable接口一般和FutureTask、Future、线程池配合使用,下面分别来介绍一下。

1.3.1 Future接口

Future类位于java.util.concurrent包下,用于对Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

在Future接口中声明了5个方法,下面依次解释每个方法的作用:

  • cancel

cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true

  • isCancelled

isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true

  • isDone

isDone方法表示任务是否已经完成,若任务完成,则返回true

  • get()

get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回

  • get(long timeout, TimeUnit unit)

用来获取执行结果,如果在指定时间内,还没获取到结果,抛TimeoutException

可以看到,Future提供了三种功能

  1. 判断任务是否完成
  2. 中断任务
  3. 异步获取能够获取任务执行结果

1.3.2 FutureTask

FutureTask在JDK中声明如下:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask类实现了RunnableFuture接口,RunnableFuture接口在JDK中声明如下:

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

可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

FutureTask提供了2个构造函数:

public FutureTask(Callable<V> callable)
public FutureTask(Runnable runnable, V result)

到这里,我们至少可以确认,可以将Callable实例通过FutureTask构造函数,构造FutureTask对象,然后通过FutureTask对象初始化Thread对象,继而达到通过Callable接口创建线程的目的。其实除了这种方法之外,Callable还可以和线程池配合使用,也能达到通过Callable创建线程的目的。

1.3.3 Callable使用示例

  • Callable + FutureTask
public class Test {
    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();
         
        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/
         
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
         
        System.out.println("主线程在执行任务");
         
        try {
            System.out.println("task运行结果" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
         
        System.out.println("所有任务执行完毕");
    }
}
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}

运行结果:

子线程在进行计算
主线程在执行任务
task运行结果4950
所有任务执行完毕
  • Callable + Future

Callable + Future这种使用方式,主要是通过线程池ExecutorService的submit方法返回Future对象,然后可以通过future对象获取线程的执行结果。ExecutorService类中submit方法有如下几种重载方式:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

可以看到,Runnable对象提交到线程池后,也可以获取一个Future对象,但是这个Future对象调用get方法获取结果恒为null,不过这个Future对象仍然可以用来判断任务是否完成、终端任务。

public class Test {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        executor.shutdown();
         
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
         
        System.out.println("主线程在执行任务");
         
        try {
            System.out.println("task运行结果"+result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
         
        System.out.println("所有任务执行完毕");
    }
}
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}

如果为了判断任务是否执行完成、中断任务而使用Future但又不提供可用的结果,则可以声明 Future<?>形式类型、并返回null作为底层任务的结果。

下面总结一下Callable和Runnable接口的不同之处:

  1. Callable中的callable方法可以返回一个泛型V类型的结果,而Runnable接口的run方法返回值类型是void
  2. Callable中的call方法能够抛出Checked Exception,而Runnable中的run方法不可以,必须显示处理Checked Exception
  3. Callable可以获取任务执行结果,而Runnable不可以

2. Java线程异常处理

引入线程之后,相当于引入了新的执行流程,与原来的异常处理也会有些不同。之前讲过,Java异常体系中有两种异常,一种是运行时异常(Runtime Exception)还有一种是受检查异常(Checked Exception),并且对于这两种异常有特定的处理方式,详见Java编程拾遗『异常体系』。这里就Runnable和Callable,分别讲一下上述两种异常的处理。先来看一个例子:

public class ThreadExceptionTest {
    static class ThreadExceptionRunner implements Runnable{
        @Override
        public void run() {
            throw new RuntimeException("error !!!!");
        }
    }

    public static void main(String[] args) {
        try {
            Thread thread = new Thread(new ThreadExceptionRunner());
            thread.start();
        } catch (Exception e) {
            System.out.println("========");
            e.printStackTrace();
        }
        System.out.println(123);
    }
}

运行结果:

可以看到子线程的异常,没有影响主线程的执行。另外,在主线程中的try-catch,并没有catch到子线程的异常,即之前的try-catch异常处理对Java线程不生效。

2.1 Runnable线程异常处理

2.1.1 Runnable线程内Checked Exception处理

由于Runnable接口中run方法没有显示声明异常,所以run方法中的Checked Exception必须手动try-catch捕获,这也是Runnable线程内Checked Exception的处理方式。

2.1.2 Runnable线程内Runtime Exception处理

对于run方法中的Runtime Exception,当然也可以通过手动try-catch的方式处理。但是我们知道,对于Runtime Exception,JVM没有强制要求我们手动处理,可以向上抛出,直到最后被JVM捕获,线程终止,像上面的例子展示的那样。如果一个线程因为Runtime Exception而终止,并最终被JVM捕获,那么我们如何处理JVM捕获的异常?

Java线程中使用Thread.UncaughtExceptionHandler处理Runtime Exception。当一个线程由于发生了Runtime Exception异常而终止时,JVM会使用Thread.getUncaughtExceptionHandler()方法获取该线程的UncaughtExceptionHandler成员变量实例,并调用他的uncaughtException()方法处理异常

public class ThreadExceptionTest {
    private static final AtomicInteger num = new AtomicInteger();

    private static final class ThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("One uncaught exception was got:");
            System.out.println("Thread id=" + t.getId());
            System.out.println("Thread name=" + t.getName());
            e.printStackTrace(System.out);
            // 当捕获到非受检异常时,可以断定原来的线程已经被JVM终止。
            // 此时可以新建一个线程继续执行原来的任务
            if (num.get() < 5) {
                new Thread(new ThreadTask()).start();
            }
        }
    }

    private static final class ThreadTask implements Runnable {
        @Override
        public void run() {
            num.incrementAndGet();
            Thread.currentThread().setUncaughtExceptionHandler(new ThreadExceptionHandler());//设置非受检异常的ExceptionHandler
            try {
                for (int i = 4; i >= 0; i--) {
                    TimeUnit.SECONDS.sleep(1);
                    // 当i=0时候抛出的非受检异常,将导致当前线程被JVM杀死
                    // 但异常会被在上面设置的ThreadExceptionHandler捕获到,进而被处理
                    System.out.println(12 / i);
                }
            } catch (InterruptedException e) {//受检异常直接在run方法体内部处理
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final Thread thread = new Thread(new ThreadTask());
        //此处使用thread.setUncaughtExceptionHandler(new ThreadExceptionHandler)可以代替ThreadTask中
        //的Thread.currentThread().setUncaughtExceptionHandler(new ThreadExceptionHandler(),但这样不好
        thread.start();
    }
}

在主函数中,我们执行了一个Thread线程,这个线程中执行ThreadTask任务。在ThreadTask run方法中,如果是Checked Exception,直接在run()方法体中try-catch处理。对于Runtime Exception,我们在run()方法体中设置了非受检异常处理类ThreadExceptionHandler,这个类的uncaughtException()方法就是处理线程内部Runtime Exception的具体执行者。该方法会判断任务执行的次数,如果没有超过5次,就会创建一个Thread线程来重新执行任务。(一旦线程抛出了Runtime Exception,JVM就会把它杀死,然后把捕获到的非受检异常传递给UncaughtExceptionHandler类对象类处理)。

如果线程中没有设置UncaughtExceptionHandler,那么JVM会怎么处理线程中的Runtime Exception?

如果一个线程没有显式的设置它的UncaughtExceptionHandler,JVM就会检查该线程所在的线程组是否设置了UncaughtExceptionHandler,如果已经设置,就是用该UncaughtExceptionHandler;否则查看是否在Thread层面通过静态方法setDefaultUncaughtExceptionHandler()设置了UncaughtExceptionHandler,如果已经设置就是用该UncaughtExceptionHandler;如果上述都没有找到,JVM会在对应的console中打印异常的堆栈信息。

public class ThreadExceptionTest {
    public static void main(String[] args) {
        Thread.setDefaultUncaughtExceptionHandler(new StaticExceptionHandler());
        final Thread t1 = new Thread(new ThreadTaskWithHandler(), "t1");
        t1.start();

        final Thread t2 = new Thread(new ThreadTaskNoHandler(), "t2");
        t2.start();

    }

    private static final class ExplicitExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("explicit exception handler -- " + t.getName());
        }
    }

    private static final class StaticExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("static thread exception handler -- " + t.getName());
        }
    }

    private static final class ThreadTaskWithHandler implements Runnable {

        @Override
        public void run() {
            Thread.currentThread().setUncaughtExceptionHandler(new ExplicitExceptionHandler());
            System.out.println(12 / 0);
        }
    }

    private static final class ThreadTaskNoHandler implements Runnable {
        @Override
        public void run() {
            System.out.println(12 / 0);
        }
    }
}

运行结果:

static thread exception handler -- t2
explicit exception handler -- t1

可以看到,t1线程run方法内设置了UncaughtExceptionHandler为ExplicitExceptionHandler,所以t1线程中抛出的Runtime Exception会被ExplicitExceptionHandler捕获到,而t2线程run方法内没有设置UncaughtExceptionHandler,所以会被Thread层面通过静态方法setDefaultUncaughtExceptionHandler()设置的StaticExceptionHandler捕获。

2.2 Callable线程异常处理

2.1.1 Callable线程内Checked Exception处理

由于Callable接口内call方法声明抛出了Exception,所以call方法内允许抛出Checked Exception。但是如果不通过FutureTask/Future获取线程的执行结果,那么抛出的异常就不会被JVM捕获,开发人员也感知不到异常的存在。如下:

public class CallableException {

    public static void main(String[] args) {

        /*如果不调用get方法,call方法中抛出的Checked Exception不会被JVM捕获*/
        try {
            FutureTask<Integer> futureTask = new FutureTask<>(new MyTask());
            new Thread(futureTask).start();
            //futureTask.get();
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    private static final class MyTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
        	//call方法中允许抛出Checked Exception
            throw new SQLException();
        }
    }
}

如果Callable中call方法内有Checked Exception异常抛出,建议还是通过try-catch处理,否则如果不通过FutureTask/Future获取线程的执行结果,线程中抛出的Checked Exception就被吞掉了。

2.1.1 Callable线程内Runtime Exception处理

Callable中call方法内有Runtime Exception异常抛出,JVM没有强制我们手动处理Runtime Exception,向上抛出,直到最后被JVM捕获,线程终止。但是与Runnable中Runtime Exception不同的是,Callable中call方法内如果抛出了Runtime Exception,并且没有通过FutureTask/Future获取线程的执行结果,JVM是捕获不到异常的,线程中抛出的Runtime Exception就会被吞掉。那么如果线程中抛出了Runtime Exception,并且被JVM捕获(代码中调用了get方法获取线程执行结果),那么如何处理JVM捕获的异常?是否也是通过Thread.UncaughtExceptionHandler?下面来看个例子:

public class CallableException {

    private static final ExecutorService executorService = Executors.newSingleThreadExecutor();

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyTask());
        new Thread(futureTask).start();
        try {
            futureTask.get();
        } catch (InterruptedException e) {
            //get方法声明的Checked Exception通过try-catch处理
            System.out.println("任务被中断!");
        } catch (ExecutionException e) {
            //get方法声明的Checked Exception通过try-catch处理
            System.out.println("任务内部抛出异常!");
        }
    }

    private static final class MyTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("unchecked exception happened:");
                    System.out.println(t.getId());
                    System.out.println(t.getName());
                    e.printStackTrace(System.out);
                }
            });
            int sum = 0;
            for (int i = 4; i >= 0; i--) {
                sum = sum + (12 / i);
            }
            return sum;
        }
    }
}

运行结果:

可以看到call方法中设置的UncaughtExceptionHandler并没有生效,不过可以通过Future/FutureTask的get方法,传递给调用者,get() 方法的ExecutionException异常就是Callable线程中抛出的异常(包括Checked Exception和Runtime Exception)。所以可以在调用方catch ExecutionException时,处理Callable内部抛出的异常。我们在catch ExecutionException时,打印一下异常信息,如下:

public static void main(String[] args) {
    FutureTask<Integer> futureTask = new FutureTask<>(new MyTask());
    new Thread(futureTask).start();
    try {
        futureTask.get();
    } catch (InterruptedException e) {
        //get方法声明的Checked Exception通过try-catch处理
        System.out.println("任务被中断!");
    } catch (ExecutionException e) {
        //get方法声明的Checked Exception通过try-catch处理
        System.out.println("任务内部抛出异常!");
        e.printStackTrace(System.out);
    }
}

运行结果:

可以看到,try-catch捕获到的ExecutionException异常就是Callable线程内部抛出的Runtime Exception。如果Callable内部抛出了CheckedException,也会被ExecutionException捕获。

2.2 线程池异常处理

在Java中使用线程时,很多情况下都是和线程池配合使用,那么当使用线程池时,如何处理线程中抛出的异常?是否可以通过UncaughtExceptionHandler处理任务中抛出的Runtime Exception?看一个简单的例子,如下:

public class CallableException {

    private static final ExecutorService executorService = Executors.newSingleThreadExecutor();

    public static void main(String[] args) {
        Future<Integer> future = executorService.submit(new MyTask());
        /*跟使用FutureTask一样,如果没有调用future.get()获取结果,Callable线程内部抛出的异常也不会抛出*/
        try {
            System.out.println("myTask任务执行结果为" + future.get());
        } catch (InterruptedException e) {
            System.out.println("任务被中断!");
        } catch (ExecutionException e) {
            System.out.println("任务内部抛出异常!");
            e.printStackTrace(System.out);
        } catch (CancellationException e){
            System.out.println("任务被取消!");
        }
        executorService.shutdown();
    }

    private static final class MyTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("unchecked exception happened:");
                    System.out.println(t.getId());
                    System.out.println(t.getName());
                    e.printStackTrace(System.out);
                }
            });
            int sum = 0;
            for (int i = 4; i >= 0; i--) {
                sum = sum + (12 / i);
            }
            return sum;
        }
    }
}

运行结果:

可以看到,在线程池中,线程内部抛出的异常通过UncaughtExceptionHandler也不能处理Callable线程内部抛出的异常,相应的,在使用future的get方法获取任务执行的结果时,调用者捕获的ExecutionException异常就是Callable线程内部抛出的异常,可以对此异常进行处理,就是Callable内部抛出的异常。

另外,上面提到,ExecutorService的submit方法对于Runnable也有一种重载方法:

Future<?> submit(Runnable task);

这种情况下,跟Callable + ExecutorService的异常处理方式一样,在不使用Futur对象获取任务执行结果时(虽然Runnable任务执行结果未null),异常也是不会被调用者感知的。当使用Future的get方法获取任务执行结果时,必须显示处理get方法声明的异常InterruptedException和ExecutionException,其中ExecutionException就是Runnable内部抛出的Runtime Exception(Checked Exception已经在Runnable内部处理)。

除了在调用方通过Future对象获取任务执行结果时,处理任务内部内部抛出的异常,有没有其他方式来处理任务内部抛出的异常?比如任务在线程池中执行时抛出异常,就在线程池内部处理,而不用上传到调用方并且显示的通过Future对象get结果时才能获取到异常?答案时肯定的,我们可以通过修改线程池框架的异常处理机制来达到该目的,但此时我们需要使用ThreadPoolExecutor自定义线程池。ThreadPoolExecutor类提供了很多可调整的参数和钩子函数,可以很方便的构造一个线程池。其中有一个afterExecute(Runnable, Throwable)方法,在该方法中可以定义当前线程池中执行的线程发生内部异常时的处理方案。如下:

public class CustomerExceptionHandle {
    private static final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10)) {
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            printException(r, t);
        }

        private void printException(Runnable r, Throwable t) {

            if (t == null && r instanceof Future<?>) {
                try {
                    Object result = ((Future<?>) r).get();
                } catch (CancellationException ce) {
                    t = ce;
                } catch (ExecutionException ee) {
                    t = ee.getCause();
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // ignore/reset
                }
            }
            if (t != null) {
                t.printStackTrace(System.out);
                executeTask();
            }
        }
    };

    public static void main(String[] args) {
        executeTask();
        executorService.shutdownNow();
    }

    private static void executeTask() {
        executorService.submit(new MyTask());
    }

    private static final class MyTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 4; i >= 0; i--) {
                sum = sum + (12 / i);
            }
            return sum;
        }
    }
}

运行结果:

可以看到,在调用方并没有显示的通过Future对象去get任务执行结果,但是异常正常抛出,并被ThreadPoolExecutor的afterExecute处理,自定义异常处理成功。

参考链接:

1. 《Java编程的逻辑》

2. 啃碎并发(二):Java线程的生命周期

3. Java Thread的join() 之刨根问底

4. UncaughtExceptionHandler—处理Runnable线程内的非受检异常

5. 处理Callable线程内部的非受检异常

6. Java并发编程:Callable、Future和FutureTask

赞(2) 打赏
Zhuoli's Blog » Java编程拾遗『线程的使用』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址