不理解多线程程序设计, 就无法真正理解 JavaSE 和 JDK 源码; 因为线程特性已经与Java 语言紧密地融合在一起。
如何学习多线程程序设计呢? 那看上去似乎总是显得有些神秘。首先, 必须透彻理解并发程序设计的基本原理和机制, 否则, 只是学习使用那些关键字、类的招式,恐怕只能获得Superficial 的认识, 因为多线程程序设计的难点就在于,在任何情况下都能正确工作, easily writing programs that appear to work but will fail at any time. 必须对线程机制有更深入的认识, 才能知道自己到底在做什么; 其次, 多线程程序也并不神秘, 几个基本要点是: 1. 如何定义、执行和终止任务; 2. 如何让并发任务安全地访问共享资源;3. 如何让任务之间更好地协作和通信; 4. 避免饥饿和死锁。明白这几个要点之后, 就要从语言中寻找对应的机制。
接下来两篇文章将对 java 内建线程机制做个总结, 其中的代码示例是我自己写的, 有错漏之处, 恳请指出。
1. 术语:
概念层面: 任务: 并发是指多个任务并发。
技术层面: 线程: 线程是多个任务并发执行的实现机制;线程驱动任务执行。
在单线程环境中,只有一个任务在执行,即从 main方法入口的单一任务实体,通过分配给 main的线程来驱动和执行, 可称之为“主线程”。
2. 定义任务:
① 实现 Runnable接口:
public class Xxx implements Runnable {
public void run() { // codes }
// codes
}
② 继承 Thread类:
public class Xxx extends Thread {
public void run() { // codes }
// codes
}
尽量使用第一种方式: ① 继承的机会只有一次; ② 避免创建显式的线程对象,而是交给执行器去管理任务的启动、执行和终止。
启动任务: new Thread(Runnable).start().
3. 启动、执行和管理任务:使用执行器ExecutorService
Java 提供了一些高层工具, 以便更好地启动、执行和管理任务。 理解底层机制, 使用高层工具; 就好比, 汇编语言有利于理解C语言的内部细节, 但总是应当使用 C 来编程。 任何事物的存在都有其最具价值的场合, 有些东西的存在就是为了支撑更好的东西; 因此, 根据不同的场合选用最合适的工具。
通过以下四种方式之一创建执行器,例如ExecutorService es = Executors.newCachedThreadPool();
Ø Executors.newFixedThreadPool():创建固定线程数目的线程池,可重用空闲线程;
Ø Executors.newCachedThreadPool():线程数目不定,可重用空闲线程;
Ø Executors.newSingleThreadExecutor():单线程执行器,顺序执行任务;
Ø Executors.newScheduledThreadPool():固定线程数目,支持延迟和周期性任务。
① 执行执行器中的任务:execute(Runnble)和 submit(Callable<T>) 方法
② 关闭执行器不再接受任务: shutdown方法【IsShutdown方法判断执行器是否已经关闭】
③ 终止执行器中的任务: shutdownNow方法
④ 等待任务执行完毕: awaitTermination(long, TimeUnit)方法 【IsTerminated方法判断执行器中任务是否已经全部执行完毕】
下面的例子定义了两个线程, 其中主线程每隔 2 秒钟打印一次信息, 从线程则从控制台接受整数输入并打印出来。 一旦用户输入 0 或输入非法数据, 那么从线程将退出, 进而通过设置结束标记 endflag 来终止主线程.
package threadprogramming.basic; import java.util.Scanner; import java.util.concurrent.TimeUnit; public class IOConcurrency { private static Scanner s = new Scanner(System.in); private static volatile boolean endflag = false; public static boolean end() { return endflag == true; } public static void setflag() { endflag = true; } public static boolean getflag() { return endflag; } public static void main(String[] args) { System.out.println("Enter the main thread."); // 从线程从标准输入中读取整数并输出到控制台 new Thread(new Runnable() { public void run() { System.out.println("Enter into thread: " + Thread.currentThread().getName()); try { while (true) { int i = s.nextInt(); System.out.println(Thread.currentThread().getName() + "\tRead: " + i); if (i == 0) { System.out.println("Exit from thread: " + Thread.currentThread().getName()); break; } } } catch (Exception e) { System.out.println("Caught: " + e.getMessage()); System.out.println("Exit from thread: " + Thread.currentThread().getName()); } finally { IOConcurrency.setflag(); } } }).start(); // 主线程每隔 2s 打印一条信息 while (!end()) { long start = 2000; try { TimeUnit.MILLISECONDS.sleep(start); } catch (InterruptedException e) { System.out.println("Interrupted in main thread."); } System.out.printf("in main thread: %d ms passed.\t", start); System.out.printf("The End flag: %b\n" , getflag()); } System.out.println("Exit the main thread."); } }
4.带返回值的任务定义、执行和结果获取
*① 实现Callable<T>接口,并指定参数类型 T ;
*② 实现方法: public T call() ,其中 T 是已指定的具体类型。
*③ 创建线程,并使用 ExecutorService.submit(Callable<T> task)来执行;
*④ 创建 Future<T>来存储任务执行对象。
*⑤ 使用Future<T>对象的get()方法获得执行结果。
下面的例子中, 每个线程接受一个整数值 givenNum , 并计算 1 - givenNum 的平方和, 最后返回结果。
package threadprogramming.basic; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class ReturnableThread implements Callable<Integer> { private int givenNum; public ReturnableThread(int givenNum) { this.givenNum = givenNum; } /** * 计算 1 - givenNum 的平方和 */ @Override public Integer call() throws Exception { int sum = 0; for (int i=1; i <= givenNum; i++) { sum += i*i; } return sum; } public static void main(String[] args) { ExecutorService es = Executors.newCachedThreadPool(); List<Future<Integer>> results = new ArrayList<Future<Integer>>(); for (int i=1; i<=10; i++) { results.add(es.submit(new ReturnableThread(i))); } es.shutdown(); for (Future<Integer> result: results) { try { System.out.println(result.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } }
5.任务暂停:
调用【Thread.sleep(n), n: 毫秒数】 或 【TimeUnit.MILLISECONDS.sleep(n) , n毫秒数】可使线程休眠一段时间【处于阻塞状态】,在该时间之后线程会自动苏醒。
如果在线程休眠期间使线程调用interrupt()方法,则会抛出 中断异常 InterruptedException, 该方法可使线程终止阻塞状态【终止任务】。
下面的例子演示了 sleep 方法的使用, 以及如何中断睡眠线程。
package threadprogramming.basic; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class SleepInterruption { public static void main(String[] args) { ExecutorService es = Executors.newCachedThreadPool(); for (int i=1; i <= 5; i++) { es.execute(new Sleeper(i*1000 + 500)); } try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { System.out.println("Interrupted !"); } es.shutdownNow(); } } class Sleeper implements Runnable { private int millis; public Sleeper(int millis) { this.millis = millis; } public void run() { long start = 0, end, passed; try { start = System.currentTimeMillis(); TimeUnit.MILLISECONDS.sleep(millis); } catch (InterruptedException e) { end = System.currentTimeMillis(); passed = end - start; System.out.printf("Waken up! requiring %6d ms, sleep %6d ms, rest %6d ms\n", millis, passed, millis-passed); } System.out.println("time: " + millis); } }
6. 任务调度提示:
调用【Thread.yield()方法】建议线程调度和分派器可以将CPU让给其它线程使用。 注意,只是提示,并不保证一定会进行。对于极其重要的控制和调度,不可依赖此方法;用Thread.yield()方法可以模拟某些看上去是原子操作的执行情况(比如 count++),从而使微小的线程错误更早地浮现。
7.线程异常: 尽量在任务内部处理异常,不要让异常逃逸到更广的范围。
8.后台线程:
Ø 为非后台线程提供通用服务,程序中的可有可无组成成分;
Ø 当所有的非后台线程均终止时(包括 main主线程也退出时),程序将无可阻止地终
止,所有的后台线程都将立即被终止;
Ø 普通线程调用方法 setDaemon(true)即可成为后台线程;
Ø 后台线程创建的线程自动地成为后台线程。
Ø 通过实现线程工厂 ThreadFactory可以用来生成定制属性的线程;将其实现类传给
ExecutorService(ThreadFactory)构造器来执行生成后台线程的工厂。下面的例子演示了后台线程的用法: 通过 DaemonThreadFactory 定制后台线程工厂, 通过传入参数, 使每个后台线程具有一个服务名称并在任务中打印它。 从输出结果可以看出, 当 main 和 计算乘法运算的非后台线程结束后, 所有的后台线程也结束了。
package threadprogramming.basic; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; public class DaemonThreadDemo { private static String[] services = new String[] { "Data Manager", "Network", "Event reporting", "Media devices", "Memory monitor" }; public static void main(String[] args) { ExecutorService es = Executors.newCachedThreadPool(new DaemonThreadFactory()); // 创建后台线程 for(int i=0; i < 5; i++) { es.execute(new DaemonServiceThread(services[i])); } es.shutdown(); // 创建非后台线程 new Thread(new Runnable() { @Override public void run() { for (long i=1; i < 20000; i++) { for (long j=1; j < 20000; j++) { long m = i * j; } } System.out.println("Exit from : " + Thread.currentThread().getName()); } }).start(); // Main 线程结束 System.out.println("quit main."); } } // 定制线程工厂 class DaemonThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } } // 后台线程: 间隔地提供服务 class DaemonServiceThread implements Runnable { private String serviceName; public DaemonServiceThread(String serviceName) { this.serviceName = serviceName; } @Override public void run() { while(true) { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { System.out.println("Interrupted."); } System.out.println(Thread.currentThread().getName() + " Providing service: " + serviceName + " ... "); } } }
9.匿名线程:
可以在类的内部、方法内部创建匿名线程(匿名内部类)。可以看到, 匿名内部类在多线程程序设计中,尤其是GUI 程序设计中无处不在。 这里顺带演示了 Thread.join 方法, 该方法在使调用线程挂起, 直到 t.join 的线程 t 执行完毕, 或者产生中断异常。
package threadprogramming.basic; public class Joiner extends Thread { private Thread jThread; public Joiner(Thread jThread) { this.jThread = jThread; } @Override public void run() { try { jThread.start(); jThread.join(); } catch (InterruptedException e) { System.out.println("Interrupted."); } System.out.println("Enter thread: " + Thread.currentThread().getName()); for (int i = 0; i < 10; i++) System.out.printf("print [%d]\n", i); System.out.println("Exit thread: " + Thread.currentThread().getName()); } public static void main(String[] args) { System.out.println("Enter thread: " + Thread.currentThread().getName()); Thread joinerThread = new Thread() { public void run() { System.out.println("Enter thread: " + Thread.currentThread().getName()); System.out.println("joiner do something..."); for (int i=0; i < 10; i++) { System.out.printf("%d * %d = %d\n", i, i, i*i); } System.out.println("Exit thread: " + Thread.currentThread().getName()); } }; new Joiner(joinerThread).start(); System.out.println("Exit thread: " + Thread.currentThread().getName()); } }