实际应用中,多线程非常有用。如,一个浏览器可以同时下载继父图片;一个web服务器可以同时处理几个并发的请求;GUI程序用一个独立的线程从宿主操作环境中收集用户界面的事件。
多进程与多线程本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间的通信更有效、更容易。
14.1 线程
Thread的静态方法sleep将暂停当前线程给定的毫秒数,不会创建一个新线程。
在一个独立的线程中执行一个任务的简单过程:
1. 将任务代码移到实现了Runnable接口的类的run方法中。
2. 创建一个Runnable对象:Runnable r = new MyRunnable();
3. 由Runnable创建一个Thread对象:Thread t = new Thread(r);
4. 启动线程:t.start();。不要调用Thread或Runnable的run方法,直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。start方法将创建一个执行run方法的新线程。
14.2 中断线程
当对一个线程调用interrupt方法,线程的中断状态将被置位,这是每个线程都具有的boolean标志。
调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted方法确认中断状态是否被置位。
如果线程被阻塞,就无法检测中断状态。当在一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。
没有任何语言方面的需求要求一个被中断的线程应该终止,中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后继续执行,而不理会中断。更普遍的情况是,线程将简单地将中断作为一个终止的请求。
14.3 线程状态
如下6中状态:New(新生),Runnable(可运行),Blocked(被阻塞),Waiting(等待),TimeWaiting(计时等待),Terminated(被终止)。通过getState确定线程的当前状态。
14.3.1 新生线程
当用new操作符创建一个新线程时,该线程还没有开始运行线程中的代码,它的状态是new。
14.3.2 可运行线程
一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,取决于操作系统给线程提供运行的时间。
一旦一个线程开始运行,它不必始终保持运行,取决于操作系统的调度。
14.3.3 被阻塞线程和等待线程
当线程处于被阻塞或等待状态时,暂时不活动,不运行任何代码且消耗最少的资源,直到线程调度器重新激活它。
1. 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,且线程调度器允许本线程持有它的时候,该线程变为非阻塞状态。
2. 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait方法或Thread.join方法、或是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。
3. 有一个方法有一个超时参数,调用它们导致线程进入计时等待状态。这一状态一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep,Object.wait,Thread.join,Lock.tryLock,Condition.await。
线程状态切换:当一个线程被阻塞或等待时(或终止时),另一个线程被调度为运行状态。当一个线程被重新激活,调度器检查它是否具有比当前运行线程更高的优先级。如果是,调度器从当前运行线程中挑选一个,剥夺其运行权,选择一个新的线程运行。
14.3.4 被终止的线程
因两个原因被终止:1. run方法正常退出而自然死亡,或2. 因为一个没有捕获的异常终止了run方法而意外死亡。
不要使用stop和suspend方法。
14.4 线程属性
线程优先级、守护线程、线程组、处理未捕获异常的处理器。
14.4.1 线程优先级
默认一个线程继承它的父线程的优先级,通过setPriority设置线程优先级,从MIN_PRIORITY(1)到MAX_PRIORITY(10)。
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。
避免滥用线程优先级,以防低优先级的线程完全饿死。
Thread.yield()方法会导致当前执行线程处于让步状态,如果有其他可运行线程具有相同或更高优先级,那么那些线程会被调度。
14.4.2 守护线程
调用thread.setDaemon(true);,将线程转换为守护线程,需要在线程启动之前调用。唯一用途是为其他线程提供服务。如计时线程。当只剩下守护线程时,就没必要继续运行程序了,虚拟机就退出了。
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
14.4.3 未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但是不被检测的异常会导致线程终止,此时线程就死亡了。
但是不需要任何catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。该处理器必须实现Thread.UncaughtExceptionHandler接口。
如果不安装默认的处理器,默认的处理器为空。如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。
线程组ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法做如下操作:
1. 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。
2. 否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。
3. 否则,如果Throwable是ThreadDeath的一个实例,什么都不做。
4. 否则,线程的名字以及Throwable的栈踪迹被输出到System.err上。
14.5 同步
14.5.1 竞争条件
根据各线程访问数据的次序,可能会产生讹误的对象,这样一个 情况通常称为竞争条件。
14.5.2 详解竞争条件
通过非原子操作读写同一变量,极易发生。
14.5.3 锁对象
synchronized关键字,自动提供一个锁以及相关的“条件”。
使用ReentrantLock保护代码块的基本结构:
lock.lock(); try{ critical section;//临界区 } finally { lock.unlock();}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁。
把解锁操作括在finally子句内十分重要。即锁必须释放,否则其他线程永远阻塞。
锁是可重入的,线程可以重复获得已经持有的锁。锁保持一个持有技术来跟踪对lock方法的嵌套调用,线程在每一次调用lock都要调用unlock来释放锁。被一个锁保护的代码可以调用另一个使用相同锁的方法。
14.5.4 条件对象Condition(后续补充)
锁和条件的关键之处:
1. 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
2. 锁可以管理试图进入被保护代码段的线程。
3. 锁可以拥有一个或多个相关的条件对象。
4. 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
14.5.5 synchronized关键字
Java中每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。即由内部锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
使用synchronized关键字声明的方法,等价于14.5.3中的lock的结构。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。wait和notifyAll等价于condition.await();和condition.signalAll();。
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。
内部锁和条件存在的局限:1. 不能中断一个正在试图获得锁的线程;2. 试图获得锁时不能设定超时。3. 每个锁仅有单一的条件,可能是不够的。
14.5.6 同步阻塞(后续补充)
14.5.7 监视器概念
14.5.8 Volatile域
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
14.5.9 死锁
14.5.10 锁测试与超时
lock.tryLock();
lock.tryLock(timeout, timeUnit);
14.5.11 读/写锁
14.5.12 为什么弃用stop和suspend方法
14.6 阻塞队列BlockingQueue
生产者线程向队列插入元素,消费者线程则取出它们,通过队列,安全地从一个线程向另一个线程传递数据。对于银行转账程序,转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问该银行对象的内部。
当试图入队而队列已满,或是想出队而队列为空的时候,阻塞队列导致线程阻塞。工作者线程周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步修改。
LinkedBlockingQueue:容量没有上边界,可以指定最大容量。
ArrayBlockingQueue:构造时需要指定容量。
PriorityBlockingQueue:带优先级的队列,而不是先进先出队列,元素按照优先级顺序被移出,没有容量上线。
DelayQueue:实现了Delayed接口。
14.7 线程安全的集合
14.7.1 高效的映像、集合和队列
java.util.concurrent包提供了映像、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
并发的散列映像表ConcurrentHashMap,高效地支持大量的读者和一定数量的写者,其他多的写者线程被暂时阻塞。
14.7.2 写数组的拷贝
14.7.3 旧的线程安全的集合
14.8 Callable与Future
Callable与Runnable类似,但是有返回值。public interface Callable<V> { V call() throws Exception; }
Callable的类型参数是返回值的类型,如Callable<Integer> 表示一个返回Integer对象的异步计算。
Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘掉它,Future对象的所有者在结果计算好后可以获得它。(待补充)
14.9 执行器Executor
如果程序中创建了大量的生命期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在线程池中准备为下一个请求提供服务。使用线程池还能减少并发线程的数目,避免因为大量线程降低性能甚至虚拟机崩溃。
Executor有许多静态工厂方法用来构建线程池:
newCachedThreadPool:必要时创建新线程,空闲线程会被保留60秒。
newFixedThreadPool:包含固定数量的线程,空闲线程会一直被保留。
newSingleThreadExecutor:只有一个线程的池,顺序执行每一个提交的任务。
newScheduledThreadPool:用于预定执行而构建的固定线程池。
newSingleThreadScheduledExecutor:用于预定执行而构建的单线程池。
14.9.1 线程池
newCachedThreadPool构建了一个线程池,对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程。
newFixedThreadPool构建一个具有固定大小的线程池。如果提交的任务多于空闲的线程,那么把得不到服务的任务放置到队列中。当其他任务完成后再运行它们。
newSingleThreadExecutor是一个退化了的大小为1的线程池,由一个线程顺序执行提交的任务。
使用submit方法:Future<T> submit();将一个Runnable或Callable对象提交给ExecutorService,返回的Future对象可用来查询该任务的状态。
当用完一个线程池的时候,调用shutdown,该方法启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。shutdownNow,则取消尚未开始的所有任务并试图中断正在运行的线程。
14.9.2 预定执行
ScheduledExecutorService接口具有为预定执行或重复执行任务而设计的方法。
14.9.3 控制任务组
14.10 同步器