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!).*/
三、线程池
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() 的参数值使效果更明显。