超详细的Java多线程教程

本文详细讲解Java多线程,主要有:概述、定义任务,将任务交给线程,简单的线程执行:Executor,让线程有返回值,后台线程(daemon)、更方便的线程,多线程的异常捕捉,线程声明及启动多线程其他概念。
<script>ec(2);</script>

<一>概述、定义任务

一、概述

为什么使用线程?从c开始,任何一门高级语言的默认执行顺序是“按照编写的代码的顺序执行”,日常开发过程中写的业务逻辑,但凡不涉及并发的,都是让一个任务顺序执行以确保得到想要的结果。但是,当你的任务需要处理的业务比较多时,且这些业务前后之间没有依赖(比如, a执行的过程中b也可以执行,b没有必要必须等待a执行完毕再去执行),那么此时,我们可以将一个任务拆分成多个小任务。

例如,任务a负责接收键盘的输入,b负责将一些参数及计算提前做好(假设计算量比较大),c负责将a的输入和b的结果做和。此时,abc顺序执行的话,如果a的输入被阻塞了即正在等待用户输入,b就无法执行,而此时cpu处于空置状态(假设是单cpu且单核),明显效率不高。

换一个思路,假如:abc分开成为三个任务,a的输入被阻塞了,那么此时就把b交给cpu去执行,待用户输入结果之后,b已经将计算结果输出给了c,此时,用户提交后,c便立即计算出了结果。

综上:多线程解决的是并发的问题,目的是使任务执行效率更高,实现前提是“阻塞”。它们看上去时同时在执行的,但实际上只是分时间片试用cpu而已。

二、java中的多线程

1.定义任务

任务:简单来说,就是一序列工作的集合,这些工作之间有前后顺序,这一系列过程执行过后将实现一个结果或达到一个目的。

首先,思考一个问题,为什么要定义任务?作为java程序员,我们不关心底层的多线程机制是如何执行的,只关心我写个怎样的任务,java的底层的多线程机制才能认识,才能调用你的任务去执行。java是定义了Runnable接口让你去实现,意思就是:你实现Runnable接口类定义一个类,该类的对象就是我能识别的任务,其他方式定义的程序,我都不会将它认为是任务。

好,到这里要明确一点,我们此时只谈论任务,不说多线程。任务和你平时在一个类中编写的代码并无区别,只是按照java的要求实现了一个接口,并在该接口的run方法中编写了你的代码。也就是说,你平时想编写一个类,该类能够完成一些功能,这个类里的任何方法、变量由你自己来定义,而编写任务时,你需要实现Runnable接口,把你想让该任务实现的代码写到run方法中,当然,你可以在你定义的任务类中再定义其他变量、方法以在run中调用。

2.代码实现

public class Task implements Runnable {
protected int countDown = 10;
private static int taskCount = 0 ;
private final int id = taskCount ;
public Task(){}
public Task(int countDown){
    this.countDown = countDown;
}
public String status(){
    return "#" id "(" (countDown>0?countDown:"Task!") ").    ";
}
@Override
public void run() {
    while(countDown-->0){
        System.out.print(status());
        Thread.yield();
    }
}
}



注:此处代码源于《thinking in java》

定义了任务,此时并不涉及多线程,所以,任务本身就是一个类,它的对象我们可以在任意试用到的地方调用,例如:

public class TaskMain {
public static void main(String[] args){
    Task task = new Task();
    task.run();
}
}


就是在main中声明了该实例的对象,并调用了它的run方法,同我们平时创建类一样来调用对象的方法即可。

至此,一个任务定义完了。也就是说按照java的要求,我们实现了一个简单的任务。然而,实现任务的目的不只是为了实现任务,而是为了让多线程机制能够调用该任务去执行。请看:Java多线程——<二>将任务交给线程,线程声明

<二>将任务交给线程,线程声明及启动

一、任务和线程

《thinking in java》中专门有一小节中对线程和任务两个概念进行了具体的区分,这也恰好说明任务和线程是有区别的。

正如前文所提到的,任务只是一段代码,一段要达成你目的的代码,这段代码写在哪,怎么写其实无所谓,只是因为你希望java的多线程机制能够识别并调用你编写的任务,所以规定了Runnable接口,让你的任务来实现该接口,把你想做的工作在实现该接口的run方法中实现。

那么,已经定义了任务类,那任务和线程有什么关系呢?

java的线程是用来驱动任务执行的,也就是说你得把任务挂载到一个线程上,这样该线程才能驱动你定义的任务来执行。

二、定义线程

1.显示的定义线程的过程就是将任务附着到线程的过程。线程Thread自身并不执行任何操作,它只是用来被多线程机制调用,并驱动赋予它的任务。

如前次文章提到的任务类定义如下:

public class Task implements Runnable {
protected int countDown = 10;
private static int taskCount = 0 ;
private final int id = taskCount ;
public Task(){}
public Task(int countDown){
    this.countDown = countDown;
}
public String status(){
    return "#" id "(" (countDown>0?countDown:"Task!") ").    ";
}
@Override
public void run() {
    while(countDown-->0){
        System.out.print(status());
        Thread.yield();
    }
}
}


声明线程并将任务附着到该线程上:

Thread t = new Thread(new Task());

这样,任务就附着给了线程,下面就是让线程启动,只需要如下的调用:

t.start();

至此,线程声明ok。

有时,我会想,是不是像任务和线程的概念分离一样,此时只是声明了线程,而java的线程机制并不会调用该线程运行,还需要特殊的调用才能实现多线程执行。但是下面的一段代码告诉我,Thread类的start方法就是触发了java的多线程机制,使得java的多线程能够调用该线程

public static void main(String[] args){
    Thread t = new Thread(new Task());
    t.start();
    System.out.println("Waiting for Task");
}

输出结果如下:

Waiting for Task
#0(9).    #0(8).    #0(7).    #0(6).    #0(5).    #0(4).    #0(3).    #0(2).    #0(1).    #0(Task!).    

先输出“Waiting for Task”证明调用完start()方法后,立即返回了主程序,并开始执行下面的语句。而你声明的t线程已经去被java的多线程机制调用,并驱动着它的任务运行了。

2.补充

想看到更多的线程任务运行,可以用下面的这段代码

public static void main(String[] args){
    for(int i = 0 ; i < 5 ; i ){
        new Thread(new Task()).start();
    }
    System.out.println("Waiting for Task");
}

输出如下:

Waiting for Task
#0(9).    #2(9).    #4(9).    #0(8).    #2(8).    #4(8).    #0(7).    #2(7).    #4(7).    #0(6).    #2(6).    #4(6).    #0(5).    #2(5).    #4(5).    #0(4).    #2(4).    #4(4).    #3(9).    #2(3).    #4(3).    #2(2).    #4(2).    #2(1).    #4(1).    #2(Task!).    #4(Task!).    #1(9).    #0(3).    #0(2).    #0(1).    #0(Task!).    #3(8).    #1(8).    #3(7).    #1(7).    #3(6).    #1(6).    #3(5).    #3(4).    #3(3).    #3(2).    #3(1).    #3(Task!).    #1(5).    #1(4).    #1(3).    #1(2).    #1(1).    #1(Task!).    

上面的输出说明不同任务的执行在线程被换进换出时混在了一起——由线程调度器自动控制。不同版本的jdk线程调度方式不同,所以产生的结果也不相同。

这里涉及了垃圾回收器的一个问题,每个Thread都注册了它自己,因此确实有一个对它的引用,而且在它的任务退出其run并死亡之前,垃圾回收器无法清除它。

<三>简单的线程执行:Executor

一、概述

按照《Java多线程——<一><二>》中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法。多线程并行,明显我们需要声明多个线程然后都调用他的start方法,这么一看,似乎有些问题:第一、线程一旦多了,声明势必是个问题;第二、多线程启动如果通过手动执行的话,那可能一个线程已经跑完了,另外一个还没起来(我推测可能会出现这个问题)。所以,我们在想,如果有个管家,能够帮我们管理这么多线程,只需要把我们定义的任务交给管家,管家就能够帮我们把任务附着到线程上,并且当我们给管家发送指令让所有的线程开始并发执行时,他也能够帮助我们开启所有线程执行。

二、java多线程管家——Executor

Executor允许你管理异步任务的执行,而无须显示地管理线程的生命周期。ExecutorService知道如何构建恰当的上下文来执行Runnable对象。

1.创建ExecutorService 

通过Executors能够创建两种方式的ExectorService。第一种、CachedThreadPool会为每个传入的任务新创建一个线程

ExecutorService exec = Executors.newCachedThreadPool();

第二种、FixedThreadPool可以一次性预先执行代价高昂的线程分配,所以可以用来限制线程的数量。这可以节省时间,因为你不必为每个任务都固定的付出创建线程的开销。

ExecutorService exeService = Executors.newFixedThreadPool(5);

2.把任务附着给ExecutorService

有了executor,你只需要定义任务并将任务对象传递给executor即可。

exeService.execute(new Task());

3.让所有任务开始执行

这两个方法会让之前提交到该exectuor的所有任务开始执行。为了避免启动后,会被注入新的任务,必须在你将所有线程注入后,执行关闭操作以保证这一点。

exeService.shutdown();

总结:


|——原来:想执行任务
|            |            |——1.定义任务
|            |            |——2.创建任务对象交由Thread对象操纵
|            |            |——3.显示的调用Thread对象的start()方法
|            |——遇到问题:比较繁琐,总得自己启动线程调用;本质上是由main函数调用的
|——现在:使用java.util.concurrent.Executor(执行器)来管理Thread对象
|            |            |——1.ExecutorService exec = Executors.newAcahedThreadPool();
|            |            |——2.exec.execute(new Task());
|            |            |——3.exec.shutdown();
|            |——在客户端(显示调用线程执行)和执行任务之间提供了一个间接层,用以执行任务;撇开了main函数,由executor直接进行了调用
|            |——允许你管理异步任务的执行,而无须显示地管理线程的生命周期



三、其他

1.何时使用哪种线程池呢?
CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选
只有当这种方式会引起问题时,才需要切换到FixedThreadPool。

2.SingleThreadExecutor

SingleThreadExecutor就像是线程数量为1的FixedThreadPool
   |——在另一个线程中连续运行的任何事务来说都很有用(重点是连续运行,因为这样可以顺序接受处理),故SingleThreadPool会序列化所有提交给它的任务,       并会维护它自己隐藏的悬挂任务队列
   |——例如:向SingleThreadExecutor提交多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有任务使用相同线程。    

3.自定义线程工厂

每个静态的ExecutorService创建方法都被重载为接受一个ThreadFactory对象,该对象将被用来创建新的线程。例如:

public class TaskDaemonFactory implements ThreadFactory{
public Thread newThread(Runnable r){
    Thread t = new Thread(r);return t;
}
}

想使用自己定义的线程工厂

ExecutorService exec = Executors.newCachedThreadPool(new TaskDaemonFactory());

这样可以通过具体的要求来改造线程。

<四>让线程有返回值

一、概述

到目前为止,我们已经能够声明并使一个线程任务运行起来了。但是遇到一个问题:现在定义的任务都没有任何返回值,那么加入我们希望一个任务运行结束后告诉我一个结果,该结果表名任务执行成功或失败,此时该怎么办呢?

答案是使用Callable。之前定义的任务都直接实现了Runnable,该接口的run方法并无返回值。而Callable的call方法可以根据你传入的泛型参数返回对应类型的数据。

二、实现

1.实现Callable接口,定义可返回结果的线程任务

public class TaskCallable implements Callable<String>{
private int id;
public TaskCallable(int id){
    this.id = id;
}
@Override
public String call() throws Exception {
    
    return "result of taskWithResult " id;
}
}

注意,泛型参数String表示的是该任务执行之后返回结果的类型。

2.将该任务交给线程执行者executor,让他来代理执行这些线程

ExecutorService exec = Executors.newCachedThreadPool();//工头
ArrayList<Future<String>> results = new ArrayList<Future<String>>();//
for(int i = 0 ; i < 10 ;i ){
results.add(exec.submit(new TaskCallable(i)));//submit返回一个Future,代表了即将要返回的结果
}

注意,此时需要使用executor的submit方法来调用Callable的call。

该方法将返回一个Future接口的对象,它的泛型参数代表了call方法要返回的参数类型。

3.Future类型

简单的了解了下Future类型:按照名字判断该类型对象代表了线程执行完成后的结果,所以叫Future。那么在获取该类型存放的线程运行结果时,可能该线程并未运行完毕,所以称其为“将来的结果”。

首先,可以用isDone()方法来查询Future是否已经完成,任务完成后,可以调用get()方法来获取结果
如果不加判断直接调用get方法,此时如果线程未完成,get将阻塞,直至结果准备就绪

<五>后台线程(daemon)

一、后台线程(守护线程)

学一个东西,最重要的一点就是,为什么要用它?

后台线程区别于普通线程,普通线程又可以称为用户线程,只完成用户自己想要完成的任务,不提供公共服务。而有时,我们希望编写一段程序,能够提供公共的服务,保证所有用户针对该线程的请求都能有响应。

仔细来看下后台线程的定义:指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。

二、实现后台线程

1.我们先定义任务及响应的线程

定义任务:Thread.yield();让线程暂停一段时间

class DaemonSpawn implements Runnable{
public void run(){
    while(true)
        Thread.yield();
}
}

定一个线程,有一个属性Thread[] t,用于存放子线程

class Daemon implements Runnable{
//该任务下创建很多子线程
private Thread[] t = new Thread[10];
public void run(){
    //为线程池填充线程,并将所有线程启动
    for(int i = 0 ; i < t.length ; i ){
        t[i] = new Thread(new DaemonSpawn());
        t[i].start();
        System.out.println("DaemonSpawn " i "started, ");
    }
    for(int i = 0 ; i < t.length ; i ){
        System.out.println("t[" i "].isDaemon()=" t[i].isDaemon() ", ");
    }
    /*
     * Daemon进入了无线循环,并在循环里调用yield方法把控制权交给其他进程
     */
    while(true)
        Thread.yield();
}
}


讲定义的线程设定为后台线程

public static void main(String[] args) throws InterruptedException{
    /*
     * Daemon被设置为了后台线程,它的所有子线程也自然就是后台线程了
     */
    Thread d = new Thread(new Daemon());
    d.setDaemon(true);
    d.start();
    System.out.println("d.isDaemon()=" d.isDaemon() ",");
    TimeUnit.SECONDS.sleep(1);
}

至此,后台线程已定义并跑起来了。输出结果:

DaemonSpawn 0started, 
DaemonSpawn 1started, 
DaemonSpawn 2started, 
DaemonSpawn 3started, 
DaemonSpawn 4started, 
DaemonSpawn 5started, 
DaemonSpawn 6started, 
DaemonSpawn 7started, 
DaemonSpawn 8started, 
DaemonSpawn 9started, 
t[0].isDaemon()=true, 
t[1].isDaemon()=true, 
t[2].isDaemon()=true, 
t[3].isDaemon()=true, 
t[4].isDaemon()=true, 
t[5].isDaemon()=true, 
t[6].isDaemon()=true, 
t[7].isDaemon()=true, 
t[8].isDaemon()=true, 
t[9].isDaemon()=true, 
d.isDaemon() true,

2.有一点要指出:所有的“非后台线程”结束时,程序也就终止了,同时会杀死进程中所有后台线程:main就是一个非后台线程

首先,如何证明main是非后台线程,还是用是上面那段程序;

 

其次,如何证明非后台线程退出后,后台线程会被杀死呢?


public class TaskDaemon implements Runnable{

@Override
public void run() {
    try{
        while(true){
            TimeUnit.MILLISECONDS.sleep(100);
            System.out.println(Thread.currentThread() "  " this);
        }
    }catch(InterruptedException e){
        System.out.println("sleep() interrupted");
    }
}
public static void main(String[] args) throws InterruptedException{
    /*
     * 可以通过查看该程序的结果理解后台线程
     * 创建了9个线程,都声明为后台线程,然后启动他们,在非后台线程结束之前,后台线程会被线程调用器调用
     * main就是一个非后台线程,for循环结束之后输出了"All daemons started"证明main快要结束了,但是你让它睡眠了一会保证main不退出
     * 这样后台线程就会跑着,于是有了后面的打印结果
     */
    for(int i = 0 ; i < 10 ; i ){
        //后台线程本质上也是一个线程,通过任务来创建该线程
        Thread daemon = new Thread(new TaskDaemon());
        //想将创建的线程声明为后台线程 ,必须在启动前将其设置为true
        daemon.setDaemon(true);
        daemon.start();
    }
    System.out.println("All daemons started");
    TimeUnit.MILLISECONDS.sleep(175);
}

}

3.通过isDaemon()方法来判断一个线程是否是一个后台线程

一个后台线程创建的任何线程都将被自动设置成后台线程,例如:Daemons中所示。

4.后台线程在不执行finally字句的情况下就会终止其run()方法,例如:DaemonsDontRunFinally

class ADaemon implements Runnable{
@Override
public void run() {
    try{
        System.out.println("Starting ADaemon");
        TimeUnit.SECONDS.sleep(1);
    }catch(InterruptedException e){
        System.out.println("Exiting via InterruptedException");
    }finally{
        System.out.println("Thie should always run?");
    }
}
}

public static void main(String[] args){
    //当最后一个非后台线程终止时,后台线程会“突然”终止
    //故一旦main退出,jvm就会立即关闭所有的后台进程,而不会有任何你希望出现的确认形式
    Thread t = new Thread(new ADaemon());
    //如果注释掉下面这句话,finally将会执行
    t.setDaemon(true);
    t.start();
}

可以看到输出结果,finally中结果并没有执行

三、自定义后台线程工厂

1.自定义后台线程工厂

public class TaskDaemonFactory implements ThreadFactory{
public Thread newThread(Runnable r){
    Thread t = new Thread(r);
    t.setDaemon(true);
    return t;
}
}

2.创建线程时试用该工厂

/*
* Executors.newCachedThreadPool();方法用来接受一个ThreadFactory对象,而这个对象将被用来创建新的线程
* 所以,你的Facotry重写了ThreadFacotry方法之后,要去实现他的创建线程方法,方法里默认将线程声明为后台线程
*/
ExecutorService exec = Executors.newCachedThreadPool(new TaskDaemonFactory());
for(int i = 0 ;i < 10 ;i ){
exec.execute(new TaskDaemonFromFactory());//这个是一个自定义任务
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(500);

四、总结

后台线程(daemon)
  |——定义:指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分
  |       |——所有的“非后台线程”结束时,程序也就终止了,同时会杀死进程中所有后台线程:main就是一个非后台线程
  |——声明并试用后台线程
  |       |——传统方式:通过声明线程,操作线程来定义后台线程
  |       |         |——Thread daemon = new Thread(new TaskDaemon());//将任务交给线程也叫声明线程
  |       |         |—— daemon.setDaemon(true);//将线程设置为后台线程
  |       |         |——daemon.start();//启动后台线程
  |       |——由executor调用线程工厂:通过编写定制的ThreadFactory,可以定制由Executor创建的线程的属性
  |           |——1.实现ThreadFactory接口
  |           |——2.重写newThread方法
  |           |—— public Thread newThread(Runnable r){
  |           |—— Thread t = new Thread(r);
  |           |—— t.setDaemon(true);
  |           |—— return t;
  |           |—— }
  |           |——3.将定制的TaskDaemonFactory传递给Executor,它将用此来生成对应的线程,并操纵他们
  |           |—— 每个静态的ExecutorService创建方法都被重载为接受一个ThreadFactory对象,该对象将被用来创建新的线程
  |           |—— ExecutorService exec = Executors.newCachedThreadPool(new TaskDaemonFactory());
  |           |——4.将任务传递给exec,它会帮你执行线程操作
  |           |—— exec.execute(new TaskDaemonFromFactory());

注:以上代码均来自《Thinking in java》,总结内容均是个人理解,如有错误请大家批评指正,谢谢

<六>更方便的线程

一、概述

标题很抽象,什么叫更方便?更是相比谁来说的呢?

原来,我们定义任务,都是实现自Runnable或者Callable接口,但是这样必然需要你将新定义的任务附着给线程,然后再调用线程启动。在不考虑Executor的情况下,你会感觉这么做很呆板,如果直接new一个任务对象,他自己就能启动的话,是不是更好?这就是更方便的线程这个标题的由来。

二、实现

1.继承自Thread

因为Thread实现了Runnable接口,所以继承自Thread的类,实现其run方法,其中定义的也就是任务。

这样做的好处是,可以直接调用父类的start方法,可以实现概述中要实现的效果

public class TaskExtendsThread extends Thread{
private int countDown = 5;
private static int threadCount = 0;
/*
 * 可以在它的构造方法里直接调用run方法
 */
public TaskExtendsThread(){
    super(Integer.toString( threadCount));
    start();
}
public String toString(){
    return "#" getName() "(" countDown ").";
}
public void run()    {
    while(true){
        System.out.println(this);
        if(--countDown == 0 )
            return ;
    }
}
public static void main(String[] args){
    for(int i = 0 ;i < 5 ;i )
        new TaskExtendsThread();
}
}

这样,在new一个对象时就会让你的任务自动附着给线程并启动。

2.自管理的Runnable,实现Runnable接口。例如SelfManagedThread

既然已经有了Thread类的实现,为什么还要有自管理Runnable呢?这样做和继承Thread没有区别,只是这样做会让你的类更灵活,实现了该类还可以继承其他你想让他继承的类。

public class SelfManagedThread implements Runnable{
private int countDown = 5;
private Thread t = new Thread(this);
/*
 * 在构造器中启动线程可能会变得很有问题
 * 因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态的对象
 */
public SelfManagedThread(){
    t.start();
}
public String toString(){
    return Thread.currentThread().getName() "(" countDown ").";
}
@Override
public void run() {
    while(true){
        System.out.println(this);
        if(--countDown==0)
            return;
    }
}
public static void main(String[] args){
    for(int i = 0; i < 5 ;i )
        new SelfManagedThread();
}
}

三、然并卵

这个大标题起的很好,因为目前为止,只知道这两种方式,都是在构造器中启动线程的,这可能会变的很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务可能访问处于不稳定状态的对象.故:优选Executor而不是显示地创建Thread对象的一个原因

但是也不能说这样的解决方式完全没有用,待有朋友看到文章后给些提示……

四、线程的术语

至此,关于线程的实现,大部分总结完了,剩下的就是竞争资源、线程同步等并发方面的问题了。所以,在此总结下线程的术语

术语
|——线程并不是任务,线程不做任何事情只是驱动附着给它的任务执行
|——Runnable接口理解:可以理解为该方法的反型封装,即它执行能做的事情
|——其实任务应该实现一个名为Task的接口,这样更容易理解java中的线程
|——从概念上讲:我们希望创建独立于其他任务运行的任务,因此我们应该能够定义任务,然后说“开始”,并且不用操心细节。
|——java的线程机制基于来自c的低级的p线程方式,这事你必须深入研究,并且需要完全理解其所有事务的所有细节的方式,这种低级特性部分地渗入           了java的实现中,因此为了处于更高的抽象级别,在编写代码时,你必须遵循规则
|——任务:在描述将要执行的工作时使用术语“任务”;在引用到驱动任务的具体机制时,使用属于“线程”

注:以上代码均来自《Thinking in java》,总结及理解均来自个人,如有错误请大家批评指正。

<七>多线程的异常捕捉

一、概述

为什么要单独讲多线程的异常捕捉呢?先看个例子:

public class ThreadException implements Runnable{
@Override
public void run() {
    throw new RuntimeException();
}
//现象:控制台打印出异常信息,并运行一段时间后才停止
public static void main(String[] args){
    //就算把线程的执行语句放到try-catch块中也无济于事
    try{
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new ThreadException());
    }catch(RuntimeException e){
        System.out.println("Exception has been handled!");
    }
}
}


在run中手动抛出了一个运行时异常,在main中启动线程,catch语句块中捕捉下异常,捕捉到打印一句话。运行结果如下图:

 


发现异常被抛到了控制台,没有打印catch块中的语句。

结论:多线程运行不能按照顺序执行过程中捕获异常的方式来处理异常,异常会被直接抛出到控制台(由于线程的本质,使得你不能捕获从线程中逃逸的异常。一旦异常逃逸出任务的run方法,它就会向外传播到控制台,除非你采用特殊的形式捕获这种异常。),这样会让你很头疼,无法捕捉到异常就无法处理异常而引发的问题。

于是,我们一定会想如何在多线程中捕捉异常呢?

二、多线程中捕捉异常

我们来按照下面的步骤完成这次实验:

1.定义异常处理器

要求,实现 Thread.UncaughtExceptionHandler的uncaughtException方法,如下:

/*
* 第一步:定义符合线程异常处理器规范的“异常处理器”
* 实现Thread.UncaughtExceptionHandler规范
*/
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
/*
 * Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用
 */
@Override
public void uncaughtException(Thread t, Throwable e) {
    System.out.println("caught    " e);
}
}

2.定义使用该异常处理器的线程工厂

/*
* 第二步:定义线程工厂
* 线程工厂用来将任务附着给线程,并给该线程绑定一个异常处理器
*/
class HanlderThreadFactory implements ThreadFactory{
@Override
public Thread newThread(Runnable r) {
    System.out.println(this "creating new Thread");
    Thread t = new Thread(r);
    System.out.println("created " t);
    t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());//设定线程工厂的异常处理器
    System.out.println("eh=" t.getUncaughtExceptionHandler());
    return t;
}
}

3.定义一个任务,让其抛出一个异常

/*
* 第三步:我们的任务可能会抛出异常
* 显示的抛出一个exception
*/
class ExceptionThread implements Runnable{
@Override
public void run() {
    Thread t = Thread.currentThread();
    System.out.println("run() by " t);
    System.out.println("eh = " t.getUncaughtExceptionHandler());
    throw new RuntimeException();
}
}

4.调用实验

/*
* 第四步:使用线程工厂创建线程池,并调用其execute方法
*/
public class ThreadExceptionUncaughtExceptionHandler{
public static void main(String[] args){
    ExecutorService exec = Executors.newCachedThreadPool(new HanlderThreadFactory());
    exec.execute(new ExceptionThread());
}
}


运行结果如下图:

 


三、结论

在java中要捕捉多线程产生的异常,需要自定义异常处理器,并设定到对应的线程工厂中(即第一步和第二步)。

四、拓展

如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获处理器。

这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。

public static void main(String[] args){
    Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
    ExecutorService exec =Executors.newCachedThreadPool();
    exec.execute(new ExceptionThread());
}

<八>多线程其他概念

1.休眠

休眠,其实就是让线程暂停一段时间,也就是阻塞一段时间。例如:每个打印之后,每个线程都要睡眠(即阻塞),这使得线程调度器可以切换到另一个线程,进而驱动另一个任务。这导致了线程在执行顺序上得到了保证,可以通过查看打印输出消息证实这一点。

public class TaskSleepingTask extends Task{
@Override
public void run() {
    try{
        while(countDown-- > 0){
            System.out.println(status());
            /*
             * 每个打印之后,每个线程都要睡眠(即阻塞),这使得线程调度器可以切换到另一个线程,进而驱动另一个任务
             */
            TimeUnit.MICROSECONDS.sleep(100000);
        }    
    /*
     * 因为异常不能跨线程传回给main,所以你必须在线程里处理该异常
     */
    }catch(InterruptedException e){
        System.out.println();
    }
}
public static void main(String[] args){
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0 ; i < 5 ; i ){
        exec.execute(new TaskSleepingTask());
    }
    exec.shutdown();
}
}


但是,这不代表你就可以通过休眠的方式来保证任务的执行顺序(顺序行为依赖于底层的线程机制,这种机制在不同的操作系统之间是有差异的

要想控制顺序的解决办法:1、同步控制,2、协作线程(以后再做序数)

2.优先级

优先级代表了该线程的重要性,线程调度器将倾向于让优先级高的线程先执行(但这不意味着优先权较低的线程将得不到执行,即优先权不会导致死锁)。优先级较低的线程仅仅是执行频率较低,在绝大多数时间里,所有线程都应该以默认优先级运行,试图操纵线程优先级通常是一种错误。

通过Thread.currentThread()方法来获得对驱动该任务的Thread对象的引用,然后设置线程优先级。


public class TaskPriority implements Runnable{
private int countDown = 5;
/*
 * 变量d用来确保不进行任何编译器优化
 */
@SuppressWarnings("unused")
private volatile double d;//no optimization
private int priority;
public TaskPriority(int priority){
    this.priority = priority;
}
/*
 * 打印线程的名称、线程的优先级以及线程所属的“线程组”
 *在线程内部,通过调用Thread.currentThread()来获得对驱动该任务的Thread对象的引用
 */
public String toString(){
    return Thread.currentThread() ":" countDown;
}
@Override
public void run() {
    /*
     * 优先级应该在run的开头部分设定,在构造器中设置他们不会有任何好处,因为Executor在此刻还没有开始执行任务
     */
    Thread.currentThread().setPriority(priority);
    /*
     * 执行了10000次浮点运算,数学 运算是可以中断的,这里运算时间足够长,
     * 因此线程调度机制才来得及介入,交换任务并关注优先级,使得最高优先级的线程被优先选择
     */
    while(true){
        for(int i = 1 ;i < 10000;i ){
            d =(Math.PI Math.E)/(double)i;
            if(i00==0)
                Thread.yield();
            System.out.println(this);
            if(--countDown == 0 ) 
                return;
        }
    }
}
public static void main(String[] args){
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0 ; i < 5;i ){
        exec.execute(new TaskPriority(Thread.MIN_PRIORITY));//1
    }
    exec.execute(new TaskPriority(Thread.MAX_PRIORITY));//10
    exec.shutdown();
}
}


尽管jdk有10个优先级,但是它与多数操作系统都不能映射得很好(windows7个,solaris2个),唯一可移植的方法是当调整优先级的时候,只试用MAX_PRIORITY/NORM_PRIORITY/MIN_PRIORITY三种声明

3.让步

在你已经确认完成了在run方法的循环的一次迭代过程中所需的工作,就可以给线程调度机制一个暗示:你的工作已经做的差不多了,可以让别的线程试用cpu了。(让相同优先级的其他线程可以运行)。

 

本文转自:http://www.jquerycn.cn/a_17894

posted @ 2017-07-05 11:08  陈扬天  阅读(667)  评论(0编辑  收藏  举报