Thinking in java 之并发其一:如何实现多线程

一、基本的线程机制

java的并发编程可以将程序划分成多个分离并且能够独立运行的任务。每个独立任务都通过一个执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个并发执行的任务。在运行时,CPU将轮流给每个任务分配其占用时间。

二、定义任务

在java中,定义一个任务的方式是实现“Runnable”接口,并且实现 “Runnable” 接口的 run()方法,run() 内的就是任务。

在Thinking in Java 中的例子为 LiftOff 类,定义了一个倒计时发射的任务。

 

 1 public class LiftOff implements Runnable {
 2 
 3     protected int countDown = 10;
 4     private static int taskCount = 0;
 5     private final int id = taskCount++;
 6     public LiftOff() {}
 7     public LiftOff(int countDown) {
 8         this.countDown=countDown;
 9     }
10     public String status() {
11         return "#"+id+"("+(countDown > 0 ? countDown : "Liftoff!")+").";
12     }
13     @Override
14     public void run() {
15         // TODO Auto-generated method stub
16         while(countDown-- > 0) {
17             System.out.println(status());
18             Thread.yield();
19         }
20 
21     }
22 
23 }

 

为了确保任务能够不断执行,在run()方法类,往往都会出现某种形式的循环。

Thread.yield(),是线程机制的一部分,他可以发出一个建议,表示该进程已经完成主要任务,建议CPU切换到其他进程,但不一定100%切换。

二、通过Thread类将Runnable对象转变为工作任务

如果我们直接在 main 方法中创建一个 LiftOf f实例并且调用他的 run() 方法也是可以执行的,但该任务还是使用了和 main() 方法一样的线程。如果希望它能够独立于 main 有自己的线程,可以将 Runnable 对象提交给一个 Thread 构造器,Thread 对象的 start() 方法会新建一个线程,并利用该线程执行 run() 方法。

 1 public class BasicThread {
 2 
 3     public static void main(String[] args) {
 4         Thread t = new Thread(new LiftOff());
 5         t.start();
 6         System.out.println("Waiting for LiftOff");
 7     }
 8 }
 9 
10 /*
11  * 运行结果:
12 Waiting for LiftOff
13 #0(9).
14 #0(8).
15 #0(7).
16 #0(6).
17 #0(5).
18 #0(4).
19 #0(3).
20 #0(2).
21 #0(1).
22 #0(Liftoff!).
23  */

 

输出的结果显示,控制台首先打印出了 "Waitting for LiftOff"的字符串,然后是run() 方法里的输出,证明了main 和 run 不在同一个线程里运行。

 为了突出这一点,可以创建多个任务,就能够更明显的看出任务之间是独立的。

 1 public class MoreBasicThread {
 2 
 3     public static void main(String[] args) {
 4         // TODO Auto-generated method stub
 5         for(int i=0;i<5;i++)
 6         {
 7            Thread t = new Thread(new LiftOff());
 8            t.start();
 9         }
10         System.out.println("Waiting for LiftOff");
11     }
12 
13 }
14 /*output:
15 Waiting for LiftOff
16 #1(2).
17 #3(2).
18 #1(1).
19 #4(2).
20 #2(2).
21 #0(2).
22 #2(1).
23 #4(1).
24 #1(Liftoff!).
25 #3(1).
26 #4(Liftoff!).
27 #2(Liftoff!).
28 #0(1).
29 #3(Liftoff!).
30 #0(Liftoff!).*/

 

三、线程池

创建一个线程的消耗很大,且当一个线程完成任务时,它的资源并不会让给下一个需要的线程,新的线程又得继续进行线程的准备工作,既浪费了资源,又浪费了时间。为了避免这种问题,就需要用到线程池。
java.util.concurrent 包中的执行器(Executor)可以帮我们管理Thread对象。常用的线程池主要有三种:CachedThreadPool、FixedThreadPool以及SingleThreadPool。CachedThreadPool可以根据需要创建相应的线程,当某个任务完成之时,可以将空余出来的线程留给其他任务使用。如果线程数量不够,则会自动新建一个线程。并且,当某个线程在一定时间内没有使用时,会终止该线程,并且从线程池中移除。FixedThreadPool 会在一开始创建固定数量的线程,这些线程不会消失,当某个线程的任务完成时,该线程会一直存在等待新的任务,不会因为空闲时间过长而被清除,只能通过手动的方式去关闭。至于 SingleThreadPool 则是线程数量为 1 的 FixedThreadPool。
我们一般不会通过构造器来创建线程池的实例,而是用Executors来帮我们创建。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPool {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0;i<5;i++) {
            exec.execute(new LiftOff());
        }
        System.out.println("Waiting for LiftOff");
        exec.shutdown();
    }

}
/*output:
#0(2).
#2(2).
#3(2).
#1(2).
#4(2).
Waiting for LiftOff
#4(1).
#1(1).
#3(1).
#2(1).
#0(1).
#3(Liftoff!).
#1(Liftoff!).
#4(Liftoff!).
#0(Liftoff!).
#2(Liftoff!).*/

 

四、从任务中产生返回值

run() 方法是没有返回值的,通过实现 Runnable 创建的任务也就没有返回值。如果需要创建一个具有返回值的任务,可以通过实现 Callable 接口(而不是 Runnable)来完成。它是一种具有类型参数的泛型,它的类型参数表示的是从方法 call() (相对于 Runnable 的 run)中返回的值。Callable 需要配合 ExecutorService(上面三个线程池都是ExecutorService的具体实现) 的 submit 方法。该方法会产生 Feture对象,它用Callable返回结果的特定类型进行了参数化。

 

 1 import java.util.ArrayList;
 2 import java.util.concurrent.Callable;
 3 import java.util.concurrent.ExecutionException;
 4 import java.util.concurrent.ExecutorService;
 5 import java.util.concurrent.Executors;
 6 import java.util.concurrent.Future;
 7 
 8 
 9 class TaskWithResult implements Callable<String>{
10 
11     private int id;
12     public TaskWithResult(int id) {
13         this.id = id;
14     }
15     @Override
16     public String call() throws Exception {
17         // TODO Auto-generated method stub
18         return "Result of TaskWithResult "+ id;
19     }
20     
21 }
22 
23 public class CallableDemo {
24 
25     public static void main(String[] args) {
26         // TODO Auto-generated method stub
27         ExecutorService exec = Executors.newCachedThreadPool();
28         ArrayList<Future<String>> results = new ArrayList<Future<String>>();
29         for(int i=0;i<10;i++) {
30             results.add(exec.submit(new TaskWithResult(i)));
31         }
32         for(Future<String> item : results) {
33             try {
34                 System.out.println(item.get());
35             } catch (InterruptedException | ExecutionException e) {
36                 // TODO Auto-generated catch block
37                 e.printStackTrace();
38             }finally {
39                 exec.shutdown();
40         }
41         }
42     }
43 
44 }
45 /*output:
46 Result of TaskWithResult 0
47 Result of TaskWithResult 1
48 Result of TaskWithResult 2
49 Result of TaskWithResult 3
50 Result of TaskWithResult 4
51 Result of TaskWithResult 5
52 Result of TaskWithResult 6
53 Result of TaskWithResult 7
54 Result of TaskWithResult 8
55 Result of TaskWithResult 9*/

 

由于在不同的线程,我们在输出 item.get() 时并不能确定它对应的 call() 是否已经完成。get() 会一直阻塞直到 call 完成并将值返回,当然,也可以通过 isDone() 方法来判断是否完成。

五、进程的休眠

Thread.yield() 方法效果等同于降低线程的优先级,但不能保证该线程一定能暂停,确保线程暂停可以调用 TimeUnit.MILLISECONDS.sleep() 来实现。

 1 import java.util.concurrent.ExecutorService;
 2 import java.util.concurrent.Executors;
 3 import java.util.concurrent.TimeUnit;
 4 
 5 public class SleepingTask extends LiftOff {
 6   public void run() {
 7       try {
 8           while(countDown-- > 0) {
 9           System.out.println(status());
10           TimeUnit.MILLISECONDS.sleep(100);
11           }
12           } catch (InterruptedException e) {
13             // TODO Auto-generated catch block
14             System.err.println("Interupted");
15           }
16   }
17   
18   public static void main(String[] args) {
19       ExecutorService exec = Executors.newCachedThreadPool();
20       for(int i=0;i<5;i++) {
21           exec.execute(new SleepingTask());
22       }
23       exec.shutdown();
24   }
25 }
26 /*output:
27 #2(2).
28 #3(2).
29 #0(2).
30 #1(2).
31 #4(2).
32 #3(1).
33 #2(1).
34 #4(1).
35 #1(1).
36 #0(1).
37 #2(Liftoff!).
38 #3(Liftoff!).
39 #1(Liftoff!).
40 #4(Liftoff!).
41 #0(Liftoff!).*/

从输出结果没有一个 LiftOff 任务是连续倒计时两次可以看出,sleep 的确产生了作用。

值得注意的是,sleep() 可能会抛出 InterruptedException 异常,由于处在不同的线程中,该异常时无法传播给 main() 的 因此必须在本地(及 run() 方法里)处理所有任务内部产生的异常。

六、捕获异常

除了在 run() 内部去处理异常,是否还有其他更好的办法?

我们可以通过改变 Executors 产生线程的方式捕捉从 run() 中逃出来的异常。Thread.UncaughtExceptionHandler 是一个接口,它允许我们在每个Thread对象上都附着一个异常处理器。该处理器会在线程因未捕获的异常而临近死亡时被调用。

 1 import java.util.concurrent.ExecutorService;
 2 import java.util.concurrent.Executors;
 3 import java.util.concurrent.ThreadFactory;
 4 
 5 class ExceptionThread2 implements Runnable {
 6 
 7     @Override
 8     public void run() {
 9         // TODO Auto-generated method stub
10         Thread t = Thread.currentThread();
11         System.out.println("run() by" + t);
12         System.out.println("en = " + t.getUncaughtExceptionHandler());
13         throw new RuntimeException();
14         
15     }
16 
17 }
18 
19 class MyUncaughExceptionHandler implements Thread.UncaughtExceptionHandler{
20 
21     @Override
22     public void uncaughtException(Thread t, Throwable e) {
23         // TODO Auto-generated method stub
24         System.out.println("caught " + e);
25     }
26     
27 }
28 
29 class HandlerThreadFactory implements ThreadFactory{
30     @Override
31     public Thread newThread(Runnable r) {
32         // TODO Auto-generated method stub
33         System.out.println(this+"creating new Thread");
34         Thread t = new Thread(r);
35         System.out.println("create " + t);
36         t.setUncaughtExceptionHandler(new MyUncaughExceptionHandler());
37         System.out.println("eh = "+t.getUncaughtExceptionHandler());
38         return t;
39     }
40     
41 }
42 public class CaptureUncaughtException{
43     public static void main(String[] args) {
44         ExecutorService exc = Executors.newCachedThreadPool(new HandlerThreadFactory());
45         exc.execute(new ExceptionThread2());
46     }
47     
48     
49 }
50 
51 /*output:
52 ThreadTest.HandlerThreadFactory@16b4a017creating new Thread
53 create Thread[Thread-0,5,main]
54 eh = ThreadTest.MyUncaughExceptionHandler@2a3046da
55 run() byThread[Thread-0,5,main]
56 en = ThreadTest.MyUncaughExceptionHandler@2a3046da
57 ThreadTest.HandlerThreadFactory@16b4a017creating new Thread
58 create Thread[Thread-1,5,main]
59 eh = ThreadTest.MyUncaughExceptionHandler@1d93e3d8
60 caught java.lang.RuntimeException*/

在上述的例子中,run() 中出现的异常被捕捉并且作为参数传递给了 uncaughtException 方法。可以在该方法中对异常进行处理。

并且 UncaughtExceptionHandler 是作为线程池的构造参数使用的,它规定了线程池在给把任务包装成线程时需要绑定一个 UncaughtExceptionHandler 

七、线程的优先级

上文曾提到,Thread.yield()效果等同于降低线程的优先级(但并不是真的降低优先级),而真正对优先级进行操作的是 Thread.currentThread.setPriority()。

 

 1 import java.util.concurrent.ExecutorService;
 2 import java.util.concurrent.Executors;
 3 
 4 public class SimplePriorities implements Runnable {
 5     private int countDown = 5;
 6     private int priority;
 7     public SimplePriorities(int priority) {
 8         this.priority = priority;
 9     }
10     public String toString() {
11         return Thread.currentThread() + " : " + countDown;
12     }
13     public void run() {
14         Thread.currentThread().setPriority(priority);
15         System.out.println(toString() + "this thread's priority is "+priority);
16     }
17     
18     public static void main(String[] args) {
19           ExecutorService exec = Executors.newCachedThreadPool();
20           for(int i=0;i<5;i++) {
21               exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
22           }
23           exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
24           exec.shutdown();
25       }
26 }
27 /*output:
28 Thread[pool-1-thread-1,1,main] : 5this thread's priority is 1
29 Thread[pool-1-thread-2,1,main] : 5this thread's priority is 1
30 Thread[pool-1-thread-5,1,main] : 5this thread's priority is 1
31 Thread[pool-1-thread-6,10,main] : 5this thread's priority is 10
32 Thread[pool-1-thread-4,1,main] : 5this thread's priority is 1
33 Thread[pool-1-thread-3,1,main] : 5this thread's priority is 1
34 */

 

其中:Thread.MAX_PRIORITY 和 Thread.MIN_PRIORITY分别表示优先级的最大值和最小值。从输出结果来看,priotity 为10的线程是最后创建的,但是却不是最后执行的,

可以明显看出优先级的影响。

八、后台线程

后台线程和普通线程的区别是,后台线程无法保证程序的进行。即当所有前台线程结束时,无论后台线程是否结束,程序都会结束。将线程设置为后台线程的方式为 setDeamon 方法。

 1 import java.util.concurrent.TimeUnit;
 2 
 3 /*
 4  * 后台线程案例
 5  * 后台线程的特点是,一旦其他线程停止,程序停止
 6  */
 7 public class SimpleDaemons implements Runnable{
 8     @Override
 9     public void run() {
10         // TODO Auto-generated method stub
11         try {
12             while(true) {
13                 TimeUnit.MILLISECONDS.sleep(100);
14                 System.out.println(Thread.currentThread()+" " + this);
15             }
16         }catch(InterruptedException e) {
17             System.out.println("Sleep interrupt");
18         }
19     }
20     
21     public static void main(String[] args) throws InterruptedException {
22         for(int i=0;i<5;i++) {
23             Thread daemon=new Thread(new SimpleDaemons());
24             //设置为后台线程
25             daemon.setDaemon(true);
26             daemon.start();
27         }
28         System.out.println("All deamos start");
29         TimeUnit.MILLISECONDS.sleep(80);
30     }
31 }

程序几乎没有任何停留就结束了。

可以通过调整 sleep() 的参数值使效果更明显。

 

posted @ 2018-08-30 17:14  crazy_runcheng  阅读(391)  评论(0编辑  收藏  举报