《Java 编程思想》读书笔记之并发(二)
基本的线程机制
并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立的任务(也被称为子任务)中的每一个都将由「执行线程」来驱动。一个线程就是在进程中的一个单一的顺序控制流。
在使用线程时,CPU 将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用 CPU,但事实上 CPU 时间是划分成片段分配给了所有任务(此为单 CPU 情况,多 CPU 确实是同时执行)。
使用线程机制是一种建立透明的、可扩展的程序的方法,如果程序运行得太慢,为机器增添一个 CPU 就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理的方式。大数据的分布式扩展思想与之类似,当程序性能不行时,可以通过扩展集群提高程序并发度提高性能,但是不许修改代码。
定义任务
线程可以驱动任务,而描述任务需要一定的方式,java 中建议的方式是实现 Runnable 接口,其次是继承 Thread 类。以下是两种方式的代码实现:
Runnable 接口实现
public class MyTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread() + ": running...");
}
public static void main(String[] args) {
Thread t = new Thread(new MyTask());
t.start();
System.out.println(Thread.currentThread() + ": running...");
}
}
Thread 继承实现
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread() + ": running...");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread() + ": running...");
}
}
使用 Executor
Java SE5 的 java.util.concurrent 包中的执行器(Executor)将为你管理 Thread 对象,从而简化了并发编程。其实就是 Java 的线程池,它大大的减少的对于线程的管理,包括线程的创建、销毁等,并且它还能复用已经创建的线程对象,减少由于反复创建线程引起的开销,即节省了资源,同时也提高了程序的运行效率。这部分内容比较重要,之后会单独开一篇介绍,此处就介绍到这。
从任务中产生返回值
实现 Runnable 接口只能执行任务,无法获得任务的返回值。如果希望获得返回值,则应该实现 Callable 接口,并且应该使用 ExecutorService.submit() 方法调用它。下面是一个示例:
public class MyCallableTask implements Callable<String> {
private int id;
public MyCallableTask(int id) {
this.id = id;
}
@Override
public String call() throws Exception {
return "Result : " + Thread.currentThread() + ": " + id;
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futures.add(exec.submit(new MyCallableTask(i)));
}
for (Future<String> future : futures) {
try {
// 调用 future 方法会导致线程阻塞,直到 future 对应的线程执行完毕
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
exec.shutdown();
}
}
}
}
submit() 方法会产生 Future 对象,并且使用泛型的方式对返回值类型进行了定义。调用 Future.get() 方法,会导致线程阻塞,直到被调用的线程执行完毕返回结果,当然 java 也提供了设置超时时间的 get() 方法,防止线程一直阻塞,或者你也可以调用 Future 的 isDone() 方法预先判断线程是否执行完毕,再调用 get() 获取返回值。
休眠
「休眠」就是使任务中止执行指定的时间。在 Java 中可以通过Thread.sleep()
方法来实现,JDK 1.6 之后推荐使用TimeUnit
来实现任务的休眠。
对sleep()
方法的调用会抛出InterruptedException
异常,由于异常不能跨线程传播,因此必须在本地处理任务内部产生的异常。
线程间虽然可以切换,但是并没有固定的顺序可言,因此,若要控制任务执行的顺序,绝对不要寄希望于线程的调度机制。
优先级
线程的优先级是用来控制线程的执行频率的,优先级高的线程执行频率高,但这并不会导致优先级低的线程得不到执行,仅仅是降低执行的频率。
在绝大多数时间里,所有线程都应该以默认的优先级运行。试图操纵线程优先级通常是一种错误。——《Java 编程思想》
你可以在一个任务的内部,通过调用Thread.currentThread()
来获得对驱动该任务的 Thread 对象的引用。
尽管 JDK 有 10 个优先级,但它与多数操作系统都不能映射得很好。因此在手动指定线程优先级的时候尽量只使用MAX_PRIORITY
,NORM_PRIORITY
,MIN_PRIORITY
三种级别。
让步
通过调用Thread.yield()
方法可以使当前线程主动让出 CPU,同时向系统建议具有「相同优先级」的其他线程可以运行(只是一个建议,没有任何机制保证它一定会被采纳)。因此,对于任何重要的控制或在调整应用时,都不能依赖于yield()
。
后台线程
所谓后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。——《Java 编程思想》
因此,当所有的非后台线程结束时,程序也就中止了,同时会杀死进程中的所有后台线程。
必须在线程启动之前调用setDeamon()
方法,才能把它设置为后台线程。
可以通过调用isDaemon()
方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置成后台线程。
如果在后台线程中有finally{}
语句块,当所有非后台程序执行结束时,后台线程会突然终止,并不会执行finally{}
语句块中的内容,后台线程不会有任何开发者希望出现的结束确认形式。因为不能以优雅的方式来关闭后台线程,所以它们几乎不是一种好的思想。
加入一个线程
如果某个线程在另一个线程 t 上调用t.join()
,此线程将被挂起,直到目标线程 t 结束才恢复(即t.isAlive()
返回为 false)。也可以在调用join()
时带上一个超时参数,在目标线程处理超时时join()
方法总能返回。
对join()
方法的调用可以被中断,只要在调用线程上调用interrupt()
方法。
捕获异常
由于线程的本质特性,一旦在run()
方法中未捕捉异常,那么异常就会向外传播到控制台,除非采取特殊的步骤捕获这种错误的异常。
在 Java SE5 之前,可以使用线程组来捕获这些异常(不推荐),在 Java SE5 之后可以用 Executor 来解决这个问题。Executor 允许修改产生线程的方式,允许你在每个 Thread 对象上都附着一个异常处理器Thread.UncaughtExceptionHandler
,此异常处理器会在线程因未捕获的异常而临近死亡时被调用uncaughtException()
方法处理未捕获的异常。这通常是在一组线程以同样方式处理未捕获异常时使用,若不同的线程需要有不同的异常处理方式,则最好在线程内部单独处理。