Java核心技术 第十四章 多线程
第十四章 多线程
多线程与多进程的本质区别:每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间更有效、更容易。此外,在有些操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小的多。
创建一个线程
1.将任务代码移到实现了Runnable接口的类的run方法中。这个接口只有一个方法。
public interface Runnable
{
void run() ;
}
class MyRunnable implements Runnable
{
public void run()
{
task code
}
}
2.创建一个类对象
Runnable r = new MyRunnable() ;
3.由Runnable创建一个Thread对象
Thread t = new Thread(r) ;
4.启动线程
t.start() ;
Ball b = new Ball() ;
panel.add(b) ;
Runnable r = new BallRunnable(b, panel) ;
Thread t = new Thread(r) ;
t.start() ;
public interface Runnable
{
void run() ;
}
class MyRunnable implements Runnable
{
public void run()
{
task code
}
}
中断线程
当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常时,方法终止。
当对一个线程调用interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志。每个线程都应该时不时地检查这个标志,以判断线程是否被中断。
判断线程中断状态是否被置位:
while(!Thread.currentThread().isInterrupt() && more work to do)
{
do more work
}
但是如果线程被阻塞,就无法检测中断状态。当在一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被Interrupt Exception异常中断。
被中断的线程不一定会终止,由线程本身决定如何响应中断。普遍线程会将中断作为一个终止的请求。
在中断状态被置位时调用sleep方法,它不会休眠。相反,它将清除这一状态(!)并抛出InterruptedException。
调用sleep方法,要捕获InterruptedException异常。
线程状态
线程有如下6种状态:
l New(新创建)
l Runnable(可运行)
l Blocked(被阻塞)
l Waiting(等待)
l Timed waiting(计时等待)
l Terminated(被终止)
1.新创建线程
new一个线程,该线程还没有运行
2.可运行线程
一旦调用start方法,线程处于Runnable状态。一旦一个线程开始运行,它不必始终保持运行。线程调度的细节依赖于操作系统提供的服务,现在一般使用抢占式调度。
3.被阻塞线程和等待线程
线程处于被阻塞或等待状态,暂时不活动,直到线程调度器重新激活它。
进入这种状态的原因:
l 当一个线程试图获得一个内部的对象锁,而这个锁被其他线程占用,该线程进入阻塞状态
l 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。Object.wait、Thread.join、Lock、Condition
l 有几个方法有一个超时参数。调用它们导致线程进入计时等待状态。这一状态将一直保持到超时期满或者接收适当的通知。Thread.sleep、Object.wait、Thread.join、Lock.trylock、Condition.await等的计时版。
4.被终止的线程
线程终止原因:
l 因为run方法正常退出而自然死亡
l 因为一个没有捕获的异常终止了run方法而意外死亡
线程属性
线程优先级
每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设置在MIN_PRIORITY(Thread类中定义为1)和MAX_PRIORITY(10)之间的任何值。默认为NORM_PRIORITY(5)。每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是线程优先级是高度依赖于系统的。
不要将程序构建为功能的正确性依赖于优先级。
守护线程
可以通过调用
t.setDaemon(true)将线程转换为守护线程。守护线程的唯一用途是为其他线程提供服务。当只剩下守护线程时,虚拟机就退出了。
守护线程应该永远不去访问固定资源。
未捕获异常处理器
线程的run方法不能抛出任何被检测的异常,但是,不被检测的一场会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于捕获异常的处理器。
该处理器必须属于一个实现Thread.UncaughtException接口的类。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。
同步
锁对象
有两种机制防止代码块受并发访问的干扰。Java语言提供了一个synchronized关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock类。
用ReentrantLock保护代码块的基本结构如下:
myLock.lock()
try
{
critical section
}
finally
{
myLock.unlock() ; //make sure the lock is unlocked even if an exception is thrown
}
线程试图获取正在被占用的锁,会被阻塞。当锁被释放后,线程才能运行。
锁是可重入的,因为线程可以重复地获得已持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。
条件对象
通常,对await的调用应该在如下形式的循环体中
while(!(ok to procceed))
condition.await()
至关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await时,它没办法重新激活自身。它寄希望于其他线程。
另一个方法signal,则随机解除等待集中某个线程的阻塞状态。更易导致死锁。
package synch;
import java.util.concurrent.locks.*;
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
锁和条件的关键之处
l 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
l 锁可以管理管理试图进入被保护代码段的线程
l 锁可以拥有一个或多个相关的条件对象
l 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
synchronized关键字
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然而,Java提供了内部机制。
从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
public synchronized void method()
{
method body
}
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。换句话说,调用wait或notifyAll等价于
intrinsicCondition.awit()
intrinsicCondition.awit()
wait,notifyAll以及notify方法是Object类的final方法。Condition方法必须被命名为await、signalAll和signal以便它们不会与那些方法发生冲突。
package synch2;
/**
* Transfers money from one account to another.
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public synchronized void transfer(int from, int to, double amount) throws InterruptedException
{
while (accounts[from] < amount)
wait();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll();
}
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或者任何其他的同步静态方法。
内部锁的局限:
l 不能中断一个正在试图获得锁的线程
l 试图获得锁时不能设定超时
l 每个锁仅有单一的条件,可能是不够的
建议:
l 最好既不使用Lock/Condition也不使用synchronized关键字。
l 如果synchronized关键字适合你的程序,那么请尽量使用它
l 特别需要Lock/Condition,才使用Lock/Condition
同步阻塞
除了通过同步方法获得内部锁,还有一种方法获得锁
synchronized(obj)
{
critical section
}
obj被创建仅仅用来使用每个Java对象持有的锁
private Object lock = new Object() ;
synchronized(lock)
{
...
}
监视器概念
锁和条件不是面向对象的,因此提出监视器的概念。
详情:P656
Volatile域
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一线程并发更新的。
Volatile变量不能提供原子性。
final变量
final Map<String, Double> accounts = new HashMap<>() ;
其他线程会在构造函数完成构造之后才看到这个变量。
原子性
java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是锁)来保证其他操作的原子性。
AtomicInterger、AtomicBoolean、AutomaticLong、AtomicReference以及Boolean值、整数、long值和引用的原子数组。
线程局部变量
有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。
public static ThreadLocal<SimpleDateFormat> dateFormat =
new ThreadLocal<SimpleDateFormat>(
{
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat(“yyyy-MM-dd”) ;
}
});
要访问具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new Date()) ;
在一个给定线程中首次调用get时,会调用initialValue方法。此后,get方法会返回属于当前线程的那个实例。
对于java.util.Random类,Java SE 7另外提供了一个便利类。只需做如下调用:
int random = ThreadLocalRandom.current().nextInt(upperBound) ;
ThreadLocalRandom.current()会返回当前线程的Random实例。
锁测试
线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能阻塞。应该更加谨慎的申请锁。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则立即返回false,而且线程可以立即离开去做其他事情。
if(myLock.tryLock())
try{...}
finally {myLock.unlock}
else
//do something else
可以调用tryLock时,使用超时参数,
if(myLock.tryLock(100, TimeUnit.MILLISECONDS)) ...
lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock方法就无法终止。
然而,如果调用带有超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。这是一个非常有用的特性,因为允许程序打破死锁。
也可以调用lockInterruptibly方法。相当于一个超时设为无限的tryLock方法。
在等待一个条件时,也可以提供一个超时:
myCondition.awit(100, TimeUnit.MILLISECONDS)
如果一个线程被另外一个线程通过调用signalAll或signal激活,或者超时时限已达到,或者线程被中断,那么await方法将返回。
如果等待的线程被中断,await方法将抛出一个InterruptedException异常。
读写锁
java.util.concurrent.locks包定义了两个锁类,ReentrantLock和ReentrantReadWriteLock
使用读写锁的步骤:
1)构造一个ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock() ;
2)抽取读锁和写锁
private Lock readLock = rwl.readLock() ;
private Lock writeLock = rwl.writeLock() ;
3)对所有的获取方法加读锁
public double getTotalBalance()
{
readLock.lock() ;
try {...}
finally {readLock.unlock() ;}
}
4)对所有的修改方法加写锁
public void transfer
{
writeLock.lock() ;
try {...}
fianlly {writeLock.unLock() ;}
}
阻塞队列
对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出他们。使用队列可以安全地向从一个线程向另一个线程传递数据
当试图向队列添加元素而队列已满,或想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞。
多线程时,队列随时可能为空或满,应使用不抛出异常的方法,offer,poll,peek。
java.util.concurrent提供了阻塞队列的几个变种:
LinkedBlockingQueue的容量没有上限,也可以指定最大容量。
LinkedBlockingDeque是一个双端版本。
ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。
PriorityBlockingQueue是一个带优先级的队列。元素按照优先级被取出。该队列容量没有上限,但取元素时,队列为空,取元素的操作会阻塞。
最后,DelayQueue包含实现了Delayed接口的对象:
interface Delayed extends Comparable<Delayed>
{
long getDelay(TimeUnit unit) ;
}
getDelay方法返回对象的残留延迟。负值表示延迟一结束。元素只有在延迟用完的情况下才能从DelayQueue移除。
线程安全的集合
java.util.concurrent包提供了映射表、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentlinkedQueue.
这些集合使用复杂的算法,通过允许并发的访问数据结构的不同部分来使竞争极小化。
与大多数集合不同,size方法不必再常量时间内操作。确定这样的集合当前的大小通常需要遍历。
并发的散列映射表,可高效地支持大量的读者和写者。默认情况下,假定可以有多达16个线程同时执行,多于16时,其他线程将被阻塞。
写数组的拷贝
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。
较早的线程安全集合
Vector和Hashtable提供了线程安全的动态数组和散列表,现已弃用。
任何集合可以通过同步包装器变成线程安全的。
List<E> syncArrayList = Collections.synchronizedList(new ArrayList<E>()) ;
Map<K, V> syncHashMap = Collections.synchronizedMap(new HashMap<K, V>()) ;
如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用”客户端”锁定。
synchronized(syncHashMap)
{
Iterator<K> iter = syncHashMap.keyset().iterator() ;
while(iter.hasNext()) ... ;
}
最好使用java.util.concurrent包中定义的集合,不使用同步包装器中的。特别是,假如它们访问的是不同的桶。
Callable与Future
Runnable封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call。
public interface Callable<V>
{
V call() Exception ;
}
类型参数是返回值的类型。例如,Callable<Integer>表示一个最终返回Integer对象的异步计算。
Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在结果计算好之后就可以获得它。
Future接口具有下面的方法:
public interface Future<V>
{
V get() throws ... ;
V get(long timeout, TimeUnit unit) throws ...
void cancel(boolean mayInterrupt) ;
boolean isCancelled() ;
boolean isDone() ;
}
第一个get方法的调用被阻塞,直到计算完成。第二个get调用超时,抛出TimeoutException。
如果计算还在进行,isDone方法返回false,否则返回true。
FutureTask包装器非常便利,可以将Callable转换成Future和Runnable,它同时实现二者接口。
Callable<Integer> myComputation = ... ;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation) ;
Thread t = new Thread(task) ; // it’s a Runnable
t.start() ;
...
Integer result = task.get() ;
执行器
构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至会使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数固定的线程池以限制并发线程的总数。
执行器类有许多静态工厂方法用来构建线程池。
newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor三个方法返回实现了ExecutorSevice接口的ThreadPoolExecutor类的对象。
可用下面的方法之一将一个Runnable对象或Callable对象提交给ExeCutorService:
Future<?> submit(Runnable task)
Future<T> submit(Runnable task, T result)
Future<T> submit(Callable<T> task)
第一个调用get方法,返回null
第二个Future的get方法在完成时返回指定的result对象
第三个返回的Future对象将在计算结果准备好时得到它。
当用完一个线程池的时候,调用shutdown。该方法启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有的任务都完成后,线程池中的线程死亡。另一种方法是调用shutdownNow。该池取消尚未开始的所有任务并试图中断正在运行的线程。
下面总结了在使用连接池时应该做的事:
1)调用Executors类中静态的方法newCacheedThreadPool或newFixedThreadPool
2)调用submit提交Runnable或Callable对象
3)如果想要取消一个任务时,或如果提交Callable对象,那就要保存好返回的Future对象
4)当不再提交任何子任务时,调用shutdown
package threadPool;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
ExecutorService pool = Executors.newCachedThreadPool();
MatchCounter counter = new MatchCounter(new File(directory), keyword, pool);
Future<Integer> result = pool.submit(counter);
try
{
System.out.println(result.get() + " matching files.");
}
catch (ExecutionException e)
{
e.printStackTrace();
}
catch (InterruptedException e)
{
}
pool.shutdown();
预定执行
ScheduledExecutorService接口具有为预定执行或重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛华。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象。
可以预定Runnable或Callable在初始的延迟之后只运行一次。也可以预定一个Runnable对象周期性地运行。详情见P684
控制任务组
执行器服务不但可以作为线程池使用,而且还可以控制一组相关任务。例如,可以在执行器中使用shutdownNow方法取消所有的任务。
invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。无法知道返回的究竟是哪个任务的结果,也许是最先完成的那个任务的结果。
invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。
List<Callable<T>> tasks = ... ;
List<Future<T>> result = executor.invokeAll(tasks) ;
for(Future<T> result : results)
processFurther(result.get()) ;
这个方法缺点是第一个任务过长,则可能不得不进行等待。ExecutorCompletionService可以对结果进行排列。
用常规方法获得一个执行器。然后,构建一个ExecutorCompletionService,提交任务给完成服务。该服务管理Futrue对象的阻塞队列,其中包含已经提交的任务的执行结果。
ExecutorCompletionService service = new ExecutorCompletionService(excutor) ;
for(Callable<T> task : task) service.submit(task) ;
for(int i = 0 ; i < task.size(); i++)
processFurther(result.get()) ;
Fork-Join框架
Java SE 7中引入了fork-join框架。 可能对每个处理器内核分别使用一个线程,来完成计算密集型任务,如图像或视频处理。假设有一个处理任务,它可以很自然地分解为子任务,如下:
if(problemSize < threshold)
solve problem directly
else
{
break problem into subproblems
recursively solve each subproblem
combine the results
}
要采用框架可用的一种形式完成这种递归计算,需要提供一个扩展RecursiveTask<T>(如果计算会生成一个类型为T的结果)的类或者提供了一个扩展RecursiveAction的类(如果不生成任何结果)。再覆盖compute方法来生成并调用子任务,然后合并其结果。
Class Counter extends RecursiveTask<Integer>
{
...
protected Integer compute()
{
if(to - from < THRESHOLD)
{
solve problem directly
}
else
{
int mid = (from + to) / 2 ;
Counter first = new Counter(values, from, mid, filter) ;
Counter second = new Counter(values, mid, to, filter) ;
invokeAll(first, second) ;
return first.join() + second.join() ;
}
}
}
在这里,invokeAll方法接收到很多任务并阻塞,直到所有的这些任务都已经完成。join方法将生成结果。我们对每个子任务应用了join,并返回其总和。
例子程序见P687
同步器
java.util.concurrent包包含了几个能帮助人们管理相互合作的线程集的类。
信号量
信号量管理许多的许可证。为了通过信号量,线程通过调用acquire请求许可。其实没有实际的许可对象,信号量仅维护一个计数。许可的数目是固定的,由此限制了通过的线程数量。其他线程可以通过调用release释放许可。而且许可不是必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。
倒计时门栓(CountDownLatch)
一个倒计时门栓让一个线程集等待直到计数变为0.倒计时门栓是一次性的。一旦计数为0,就不能再重用了。
障栅(CycliBarrier)
CycliBarrier类实现了一个集结点称为障栅。考虑大量线程运行在一次计算不同部分的情形。当所有的部分都准备好时,需要把结果组合在一起。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
具体实现见P690
交换器(Exchanger)
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器。典型情况,一个线程向缓冲区填入数据,另一线程消耗这些数据。当它们都完成以后,相互交换缓冲区。
同步队列
同步队列是一种将生产者与消费者配对的机制。当一个线程调用SynchronousQueue的put方法,它会阻塞直到另一线程调用take方法为止,反之亦然。
即使SynchronousQueue类实现了BlockingQueue接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的size方法总是返回0。