第一章 多线程
多线程和多进程之间的区别:
本质区别在于每个进程有他自己的变量的完备集,线程则共享相同的数据,这个听起来似乎有些危险,事实上也的确如此,你将会在本章后面的内容中看到这个问题,尽管如此,对于程序来说,共享的变量使线程之间的通信
比进程间的通信更加有效简单,而且,对于某些操作系统而言,线程比进程更加轻量级。创建和销毁单个线程比发起进程的开销要小很多。
线程优先级
在Java程序设计语言中,每一个线程都有一个优先级,默认情况下,一个线程继承他的父线程的优先级,一个线程的父线程就是启动他的那个线程,你可以用setPriority方法提高或降低任何一个线程的优先级,你可以将优先级设置为
1-10之间的任何值。默认的优先级是5.
无论何时,当线程调用器有机会去挑选一个新的线程时,他会优先考虑高优先级的线程,但是,线程优先级是高度依赖系统的。
因此最好仅将线程优先级看做线程调度器的一个参考因素,千万不要将程序构建为其功能的正确性依赖于优先级。
守护线程
你可以通过调用t.setDaemon(true),将线程转变成一个守护线程,这样的线程并没有什么神奇的地方,一个守护线程的唯一作用就是为其他的线程提供服务,计时器线程就是一个例子,他定是发送信号给其他线程,当只剩下守护线程时,
虚拟机就退出了,因为如果剩下的线程都是守护线程,就没有继续运行程序的必要了,
线程组
Java程序设计语言允许你创建线程组,这样就可以同时对一组线程进行操作。
你可以通过下面的构造器器来创建线程组:
String groupName = "";
ThreadGroup g = new THreadGroup(groupName);
ThreadGroup构造器中的字符串是用来表示该线程组的,他必须是唯一的,然后你可以
添加线程到这个组中,方法是在线程的构造器中指定线程组。
Thread t = new Thread(g,threadName);
要查明某个特定的线程组是否有线程仍然处于可运行状态,应该使用activeCount方法。
if(g.actviceCount() == 0){
}
要中断一个线程组中的所有线程,只需要在组对象上调用interrupt()方法。
g.interrupt()
未捕获异常处理器
线程的run方法不能抛出任何被检查的异常,但是不被检查的异常可以导致线程终止,如果楚翔
这种情况,线程就是死亡了
然而,并不需要任何catch子句来处理可以被传播的异常,在线程死亡钱,异常将被传播给未捕获异常的处理器来处理。
处理器必须实现Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法.
锁对象
有两种机制来保护代码块不受并行访问的干扰。旧版本的java使用synchronized关键字来达到这个目的,而jdk1.5
引进ReentrantLock类,synchronized关键字自动提供了一个锁和相关联的条件,
用ReentrantLock保护代码块的基本结构是:
myLock.lock();
try{
}finally{
myLock.unlock();
}
这个结构保证了在任何时刻只有一个线程能够进入临界区,一旦一个线程锁住了锁对象,其他任何线程都无法通过lock语句,
当其他线程调用lock时,他们会被阻塞,直到占用线程释放锁对象。
注意每一个bank对象都有个他自己的锁对象,如果两个线程试图访问同一个对象,锁就会串行的服务于访问,但是如果两个线程访问不同的bank对象,
那么每一个线程都会得到一个不同的锁,两者都不会阻塞,这正是我们期待的结果,因为当线程操作不同的bank实例,彼此之间不会互相影响,
锁是可重入的,因为线程能够重复的获取他已经拥有的锁,锁对象维护一个持有计数来追踪lock方法的嵌套调用,线程在每次调用Lock后都要调用unlock
来释放所,由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
一般而言,你希望保护那些需要多个操作才能更新检查一个数据结构的代码块,你必须确保这些操作完成之后其他线程才可以使用相同的对象
条件对象
通常,一个线程进入临界区,却发现他必须等待某个条件满足后才能执行,你要使用一个条件对象来管理那些已经获得锁却不能开始执行有用的工作的线程。
让我们来精化那个银行模拟程序,我们不希望选择余额不足以进行转账的账户作为转出账户,注意,我们不能用下面这样的代码。
if(bank.getBalance(from)>=amount)
bank.transfer(from,to,amount)
当前线程完全有可能在条件语句检查成功之后,并在transfer被调用之前被中断。
在线程再次运行前,账户余额可能已经小于要提出的金额,你必须确保在检查余额操作
和后面的转账操作之间线程不会修改余额,将检查和转账动作同一个锁保护起来就可以达到目的:
synchronized关键字
在前一节,你看到了如何使用lock和condition对象,在进一步深入之前,我们总结一下lock和condition的关键点:
1锁用来保护代码片段,任何时刻只允许一个线程执行被保护的代码。
2锁可以管理试图进入被保护代码段的线程。
3锁可以有一个或多个相关的条件对象。
4每个条件对象管理那些已经进入被保护代码段但还不能运行的线程
可以看到使用synchronized关键字来编写代码要简洁的多,当然为了理解代码,你必须知道每个对象都有一个隐式的锁,
并且每一个锁都有一个隐式条件,由锁来管理试图进入synchronized方法的线程,而由条件来管理那些调用的wait的线程。
但是,隐式的锁和条件存在一些缺点,其中包括:
你不能终端一个正在试图获得锁的线程
试图获得锁时你不能设定超时
每个锁只有一个条件有时候显得不够用,
虚拟机的加锁原句不能很好的映射到硬件可用的最有效的加锁机制上。
你在代码中应该使用哪一种呢?
最好既不要使用lock,也不要使用synchronized关键字,你可以使用java.util.concurrent包中的一种机制
他会为你处理所有的枷锁。
如果synchronized关键字在你的程序中可以工作,那么请尽量使用它,这样可以减少你的代码数量,减少出错的几率
只有在你非常需要lock结构的独有特性的时候才能使用他们
void notifyAll()
解除在该对象上调用wait的线程的阻塞状态,这个方法只能在同步方法或同步块内部调用,如果当前线程不是对象
锁的持有者,该方法抛出一个IllegaMonitorStateException异常。
void notify()
随机选择一个在该对象上调用wait的线程,解除他的阻塞状态,这个方法只能在一个同步方法或同步块内部调用,如果
当前线程不是对象锁的持有者,该方法抛出一个IllegaMonitorStateException异常。
void wait(long &&)
导致线程进入等待状态直到被通知,这个方法只能在一个同步的方法中调用,如果当前线程不是对象锁的持有者,该方法抛出一个IllegaMonitorStateException异常。
监视器
锁和条件是线程同步的强大工具,但他们不是严格意义上的面向对象。很多年来,研究院一直在寻找一种方法,可以在不需要程序员考虑具体如何加锁的情况下保证多线程的安全
其中最成功的解决方案是监视器的概念。在java中,一个监视器有一下的特征:
一个监视器是只有一个私有域的类。
每个监视器类的对象都有一个相关的锁。
这个锁负责对所有方法枷锁
这个锁可以有任意多个关联条件
同步块
如果你在处理遗留代码,你需要知道内置的同步原句,别忘了每一个对象都有一个锁,线程通过两种方法来获得
锁,调用一个同步方法或者进入同步块,如果线程调用obj.method,就需要获得obj的锁,如果线程以如下方法
进入一个块,情况也类似。然后线程获得obj的锁,锁可以重入,如果一个线程已经获得了锁,他可以再次获得他,
同时会增加锁的持有计数,特殊情况是一个同步方法能够用相同的隐式参数调用其他的同步方法,而不需要等待锁释放
你经常会看到在遗留代码中非正规的锁,例如,
class Bank{
public void transfer(int from ,int to,int amount){
synchronized(lock){
accounts[from]-=amount;
accounts[to]+=amount;
}
private double accounts[];
private Object lock = new Object();
}
这里,创建lock对象仅是为了使用每一个java对象拥有的锁,将静态方法声明为同步是合法的,如果这个方法被调用,他会获得所关联的类对象的锁
例如如果bank对象有一个同步方法,那么他被调用时,bank.class对象的锁就会被锁住
Volatile域
有时候,只是为了读写实例的一两个域就是用同步,其带来的开销似乎太大了,毕竟,这么简单的操作能出什么错呢?遗憾的是,
使用现代的处理器和编译器,可能出现错误的地方有很多。
1多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值,这么做锁造成的结果就是在不同的处理器上运行的线程可能在同一个内存地址上看到不同的值。
2编译器能够改变指令执行的顺序以便吞吐量最大化,这种顺序上的变化不会改变代码的语义,但编译器假设只有在代码中存在显式的修改指令时,内存中的值才会发生变化,但是,内存的值可能会被另一个线程所改变
如果你使用锁来保护可以被多个线程访问的代码,那么你不需要考虑这些问题,编译器被要求在必要时刷新本地缓存并且不能不正当的重排序指令,通过这种方法保证不对加锁的效果产生干扰。
Volatile关键字为对一个实例的域的同步访问提供了一个免锁机制,如果你把一个域声明为Volatile,那么编译器和虚拟机就知道该域可能会被另一个线程并发更新。
假如一个对象有个布尔标记,由一个线程设置他的值,而由另一个线程来查询,那么有下面两种方法:
1.使用锁
public synchronized boolean isDone(){ return done;}
private boolean done;
2.将域声明为Volatile
public boolean isDone(){return done;}
private Volatile boolean done;
当然访问一个Volatile变量比访问一个一般变量要慢,这是为线程安全所付出的代价。
总之,在下面三个条件下,对一个域的并行访问是安全的。
域是Volatile的。
域是final的,并且在构造器调用完成后被访问。
对域的访问有锁的保护。
死锁
公平
在你构建一个ReentrantLock时,你可以制定你需要的一个公平锁策略
Lock fairLock = new ReentrantLock(true);
锁测试和超时
线程在调用Lock方法来获得另一个线程所持有的锁时,不时地会发生阻塞,你应该对获得锁更加谨慎,tryLock方法试图获得一个锁,如果成功就返回true,否则返回false,并且线程可以立即离开去干任何其他的事。
if(lock.tryLock()){
try{}
finally{myLock.unlock();}
else
你可以在调用时给tryLock方法一个超时参数,如下所示,
if(lock.tryLock(100,TimeUnit.MILLISECONDS)
这些方法处理公平和线程终端的方式存在细微的差别。
带一个超时参数的trylock方法在处理公平性时和lock方法是一样的,但对于不带超时的trylock方法,如果它被调用时锁可以
获得,那么当前线程就会获得锁,而不管是否有线程已经等待很旧,如果你不想发生这样的事情,可以总是调用
if(lock.tryLock(0,TimeUnit.MILLISECONDS)
lock方法不能被中断,如果线程在等待获得一个锁时被中断,那么中断线程将一直被阻塞直到可以获得为止,如果发生死锁,那么Lock方法将无法终止。
但是如果你调用带超时的tryLock方法,那么如果线程在等待时被中断,将抛出异常
读写锁
java.util.concurrent.locks包定义了两个锁类,我们已经讨论过了reentrantlock和reentrantReadWriteLock类,当有很多线程都从某个数据结构中读取数据而很少有线程对其
进行修改时,后者就很有用了,在这种情况下,允许读取器线程共享访问是合适的,当然,写入线程依然是必须是互斥访问的,
下面是使用读写锁的必要步骤。
1)创建一个reentrantreadwriteLock对象:
private Reentrantreadwritelock rwl = new Reentrantreadwritelock();
2) 抽取读锁和写锁
private Lock rl = rwl.readLock();
private LOck wl = rwl.writeLock();
3) 对所有的访问者加读锁
public double getTotleBalance(){
readLock.lock();
try{}finally{readLock.unlock();}}
4)对所有修改者加锁
public void transfer(...){
writeLock.lock();
try{}finally{writeLock.unlock();}}
为什么要弃用stop和suspend方法
1.0定义了stop和suspend方法,前者是用来直接终止线程,后者会阻塞线程直到另一个线程调用了resume。
他们有一些共同点,都试图专横的控制一个给定线程的行为。
首先我们来看看stop方法,这个方法将终止所有的未结束的方法,包括run方法,当一个线程停止时,他会立即释放
所有他锁住的对象上的锁,这会导致对象处于不一致的状态,例如,假设一个线程在将钱从一个账户转至另一个账户的过程中
在取款之后存款之前停止了,那么现在银行对象就被破坏了,因为锁已经释放了,这种破损会被那些未被停止的线程所观察到。
当一个线程想终止另一个线程时,他无法知道何时调用stop方法才是安全的,何时会导致对象被破坏,因此这个方法被弃用了,
你应该中断一个线程而不是停止他,被中断的线程在安全的时候停止。
下面我们看看suspend方法会发生什么问题,和stop不同,suspend不会破坏对象,但是,如果你用它挂起一个拥有锁的线程
那么锁在恢复之前不会被释放,如果调用他的线程试图取得相同的锁,程序就会死锁,被挂起的线程在等待恢复,而挂起他的线程在
等待获得锁
阻塞队列
队列是一种数据结构,他有两个基本操作,在队列尾部加入一个元素,和从队列头部移除一个元素。
就是说,队列以一种先进先出的方式管理数据。如果你试图向一个已经满了的阻塞队列添加一个元素,或者从一个空的阻塞队列中移除一个元素,将导致
线程阻塞。在多线程进行合作时,阻塞队列是和游泳的工具。工作者线程可以定期的把中间结果存在阻塞队列轰炸那个,而其他的工作者线程吧中间结果取出
并在将来修改他们,队列会自动平衡负载,如果第一个线程集运行的比第二个慢,则第二个线程集在等待结果时会阻塞,如果第一个线程集运行的块,那么他将在
第二个线程集赶上来。
add 增加一个元素 如果队列已满,则抛出一个IllefalStateException异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,返回false
poll 移除并返回队列头部的元素 如果队列为空,则返回null
peek 返回队列头部元素 如果队列为空,则返回null
put 添加一个元素 如果队列已满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞
阻塞队列的操作可以根据他们的响应方式分为三类,add,remove和element操作在你试图为一个已满的队列中添加元素或者从一个空队列中获得元素时抛出异常
当然在多线程程序中,队列在任何时间都可能变成满的或则空的,所以你可以想使用,offer,poll,和peek方法,这些方法在无法完成任务时只是给出了一个出错提示
而不是抛出异常。
最后我们有阻塞操作put和take,put方法在队列满时阻塞,take在队列空时阻塞,在不设置超时的情况下,offer和poll是等价的,
java.util.concurrent包提供了阻塞队列的4个变种,默认情况下,LinkedBlockingQueue的容量是没有上限的,但是也可以选择指定其的最大容量,ArrayBlockingQueue
在构造时需要给定容量,并可以选择是否需要公平性。如果公平性参数被设置了,等待最长时间的线程会优先得到处理,通常,公平性会使得你在性能上付出代价,只有在
的确非常需要的时候再使用它
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列,元素按照优先级排序被移除,该队列也没有上限,但是如果队列是控,那么取元素的操作就会被阻塞,
最后,DelayQueue包含了实现了Delayed接口的对象:
线程安全的集合
如果多线程要并发的修改一个数据结构,例如一个散列表,那么就很容易破坏了这个数据结构。例如,一个线程可能要开始向表中插入一个新的元素了,假设他在改变散列表的元素之间的连接关系的过程中剥夺了控制权,那么如果此时另一个线程也开始遍历同一个列表,他
可能得到无效的节点,从而产生混乱,因此可能会抛出异常或进入死循环。
你可以通过提供锁来保护共享数据结构,但是选择一个线程安全的实现似乎更统一些,
高效队列和散列表
java.util.concurrent包提供了队列和散列表的高效实现:ConcurrentLinkedQueue和ConcurrentHashMap。并发散列表能够有效的支持多个读取器操作和固定数量的写入器操作。默认情况下,他假设可以有多达16个写入器线程同时进行,读取器可能更多。
但如果同时存在写入器线程超过了16个,其中一些就可能被暂时阻塞,你可以在构造器制定更大的容许写入器线程数目,但这似乎不会用到。
这些集合返回弱一致性的迭代器。这意味着迭代器不一定能够反映出他们被构造之后所做的所有的修改,但他们不会两次返回同一个值,也不会抛出异常
写数组的拷贝
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有对他们的修改线程都会拥有一个底层数组拷贝
当集合上的跌代线程数目大大多于修改线程时,这种安排就显得十分有用了,当你构建的是一个迭代器时,他包含对当前数组的引用,如果数组在后来被修改了,那么迭代器依然
引用旧的数组,但此时集合的数组已经被替换了,因此,较旧的迭代器用有一致的视图,访问他无需任何同步开销。
旧的线程安全的集合
Vector和HashTable类就分别提供了线程安全的动态数组和散列表的实现,这些类被弃用了,ArrayList和HashMap类取代了他们,这些类不是线程安全的。他们使用了另一种机制。任何
一种collection类都可以通过同步包装器变成线程安全的:
List synvhArrayList = Collections.synchronizedList(new ArrayList());
Map syncHashMap = Collections.synchronizedMap(new HashMap());
经过同步包装的collection中的方法由一个锁保护起来,从而提供了线程安全的访问,但是如果你想迭代这个collection,你就必须使用一个同步块:
synchronized(syncHashMap){
Iterator iter = synchHashMap.keySet.iterator();
while(iter.hashNext())
}
Callable和Future
Runnable封装一个异步运行的任务,你可以吧他想想成一个没有任何参数和返回值的异步方法,Callable和runnable相似,但是他有返回值,CallBale接口是一个参数化的类型,只有一个
方法call
public interface Callable<V>{
V call() throws 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 isCanelled();
boolean isDone();
}
第一个get方法的调用将被阻塞,直到计算完成,第二个get方法的调用如果在计算完成之前超时,那么将会抛出异常,如果运行计算的线程被中断
这两个方法都会抛出异常,如果计算已经完成,那么get方法将立即返回。
如果计算还在进行中,isDone方法将返回false,如果计算完成就返回ture。
你可以使用cancel方法来取消计算,如果计算还没哟开始,他会被取消永远不会开始。如果计算正在进行,那么,如果Mayinterrupt参数值为ture,
他就会被中断。
FutureTask包装器是一种很方便的将Callable转换成Future和Runnable的机制,他同时实现了两者的接口。
执行器
构建一个新的线程的代价还是有些高的,因为他涉及与操作系统的交互,如果你的程序创建 大量生存期很短的线程,那就应该使用线程池,一个线程池包含大量准备
运行的空闲线程,你将一个runnable对象给线程池,线程池中的一个线程就会调用run方法。当run方法退出时,线程不会死亡,而是继续在线程池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数量,创建大量的线程会降低性能甚至导致虚拟机崩溃,如果你用的算法会创建许多线程,那么就应该使用一个线程数固定的线程池来限制
并发线程的数量。
线程池
newCachedThreadPoll方法构建了一个线程池,对于每个人物,如果有空闲线程可用,立即让他执行任务,否则创建一个新的线程。newFixedThreadPool方法创建一个大小固定的线程池。
如果提交的任务数大于空闲线程数,那么得不到服务的任务将被置于队列中,当其他任务完成后他们就能运行了。newSingleThreadExecutor是一个退化了大小为1的线程池,由一个线程执行所有任务
一个接一个,这三个方法返回一个实现了executorService接口的ThreadPoolExecutor类的对象。
当用完一个连接池后,要调用shutdown,这个方法将启动池的关闭序列,被关闭的执行器不再接受新的任务,当所有任务都完成以后,池中的线程就死亡了,另外,还可以调用shutdownNow,池会取消所有还没有开始的任务
并试图中断正在运行的线程。
下面总结了在使用线程池时应该做的事:
1)调用Executors类中静态的newCachedThreadPool或newFixedThreadPool方法。
2)调用submit来提交一个runnable或callable对象。
3)如果希望能够取消任务或如果提交了一个callable对象,那就保存好返回的future对象。
4)当不想在提交任何任务时调用shutdown。
集合
集合接口
将集合接口和实现分离
与现代数据结构类库中的常见情况一样,java集合类库也将接口和实现分离了,让我们来看一看我们熟悉的一种数据结构---“队列"中的这种分离情况
队列接口规定你可以在队列尾部添加元素,删除队列头部的元素,并且可以查找队列中有多少个元素。当你需要收集对象并且按照先进先出原则来检索对象
时,你就可以使用队列
一个队列接口的最小形式可能类似下面的形式: