并发(Concurrency)
到目前为止,你学到的都是有关顺序编程的知识.即程序中的所有事物在任意时刻都只能执行一个步骤.
我们可以很公正地说,并发"具有可论证的确定性,但是实际上具有不可确定性".这就说,你可以得出结论,通过仔细设计和代码审查,编写能够正确工作的并发程序是可能的.
实际上,你可能无法编写出能够针对你的并发程序生成故障条件的测试代码.所产生的故障经常是偶尔发生的,并且是以客户端的形式出现的.这是研究并发问题的最强理由:如果视而不见,你就会遭其反噬.
Web系统是最常见的Java应用系统之一,而基本的Web库类和Servlet具有天生的多线程性–这很重要,因为Web服务器经常包含多个处理器,而并发是充分利用这些处理器的理想方式.
注意,你可能很容易就会变得过分自信,在你编写任何复杂程序之前,应该学习一下专门讨论这个主题的书籍.
21.1并发的多面性
并发编程令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系(而且通常只具有模糊的界限).因此你必须理解所有这些问题和特例,以便有效地使用并发.
并发通常是提高运行在单处理器上的程序的性能.这有点违背直觉.如果仔细考虑一下就会发现在但处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务).表面上看,将程序的所有部分当作单个的任务运行好像是开销更小一点,并且可以节省上下文切换的代价.
是这个问题变得有些不同的是阻塞.如果程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了.如果没有并发,则整个程序都将停止下来,直至外部条件发生变化.事实上从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义.
常见示例是事件驱动的编程.实际上,使用并发最吸引人的一个原因就是要产生具有可响应的用户界面.如果不适用并发,则产生可响应用户界面的唯一方式就是所有的任务都周期地检查用户输入.通过创建单独的执行线程来响应用户的输入,即使这个线程在大多数时间都是阻塞的,但是程序可以保证具有一定成都的可响应性.
实现并发最直接的方式实在操作系统级别使用进程(隔离性).但是由于进程通常有数量和开销的限制所以不行,于此相反的是,像java所使用的这种并发系统会共享诸如内存和I/O这样的资源,因此编写多线程程序的基本的困难在于在协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问.
某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常被称为函数型语言.Erlang就是这样的语言,如果你发现程序中某个部分必须大量使用并发,并且在试图构建这个部分碰到了过多的问题,那么你可以考虑使用这样的并发语言来创建这个部分.
- java采取了更加传统的方式,在顺序语言的基础上提供对线程的支持.与多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务.这种方式产生的一个好处是操作系统的透明性,这对java而言是一个重要的设计目标.
协作多线程,java线程机制是抢占式的,这表示调度机制会周期性的终端线程,将上下文切换到另一个线程.在协作式系统中,每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句.协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉的许多,并且对可以同时执行的线程数量在理论上没有任何限制.当你处理大量的仿真元素时,这是一中理想的解决方案.
21.2基本的线程机制
(手动码字的时候突然发现自己还是对线程基本了解不足所以把基本代码还是码上吧!!!)
线程的一大好处是可以是你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上.所以,使用线程机制是一种建立透明的 可扩展的程序的方法.多任务和多线程往往是使用多处理器系统的最合理方式.
- 线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供,定义任务只需要实现Runnable接口并编写run()方法即可.
- 在run()中对静态方法Thread.yield()的调用是对线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议,它在声明:“我已经执行完声明周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机”.这完全是选择性的.
//: concurrency/LiftOff.java
// Demonstration of the Runnable interface.
public class LiftOff implements Runnable {
protected int countDown = 10; // Default
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" +
(countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
} ///:~
public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run();
}
} /* Output:
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!),
*///:~
- 上面这个实例中,这个任务的run()不是由单独的线程驱动的,它是在main()中直接调用的(实际上,这里仍旧使用了线程,即总是分配给main()的那个线程)
- Thread构造器只需要一个Runnable对象,调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务.
- 当main()创建Thread对象时,它并没有捕获任何对这些对象的引用.在使用普通对象时,这对于垃圾回收来说是一场公平的游戏,但是在使用Thread时,情况就不同了.每个Thread都"注册"了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它.
- 一个线程会创建一个单独的执行线程,在对start()的调用完成之后,他仍旧会继续存在.
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
exec.execute(new LiftOff());
}
exec.shutdown();
}
}
- Java SE5的java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程.Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务.Executor允许你管理异步任务的执行,而无需显示的管理线程的声明周期.Executor在JavaSE5/6中是启动任务的优选方法.
- CachedThreadPool将为每个任务都创建一个线程.
- 注意:ExecutorService对象是使用静态的Executor方法创建的,这个方法可以确定其Executor类型;
下面来探究一下Exector及其实现源码
可以看到它是ExecutorSevice的父接口有三个实现类
可以看到AbstractExecutorService提供了ExecutorSevice方法的默认实现,而另两个实现继承此类
可以看到API的规律,实现类中Abstract开头的类总是默认实现了接口大部分的方法
带有接口+s的类作为一个辅助工具类出现
- Note that in any of the thread pools, existing threads are automatically reused when possible
- 有了FixedThreadPool你就可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量了.还可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销
- CacheThreadPool在程序执行过程中通常会创建所需数量相同的线程,然后在它回收旧线程时,停止创建新线程,因此它时合理的Executor的首选.只有当这种方式发生问题时,你才需要切换到FixedThreadPool.
- SingleThreadExecutor就像是线程数量为1的FixedThreadPool.这对于你希望在另个线程中连续运行的任何事物(长期存活的任务)来说,都是很有用的.
- 如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务都将使用相同的线程.并且SingleThreadExecutor会序列化所有提交给它的任务,并维护它自己(隐藏)的悬挂任务队列.
- 示例假设有大量的线程,那它们运行的任务将使用文件系统.你可以用SingleThreadExecutor来运行这些线程,以确保任一时刻在任何线程中都只有唯一的任务在运行.但是SingleThreadExecutor可以让你省去只是为了维持某些事物的原型而进行的各种协调努力.通过序列化任务,你可以消除对序列化对象的需求.
- 休眠javaSE5引入了更加显式的sleep()版本,作为TimeUnit类的i部分,TimeUnit.MILLISECONDS.sleep(100);
- 因为异常不能跨线程传播回main(),所以你必须在本地处理所有在任务内部产生的异常.
- 优先级 线程的优先级将该线程的重要性传递给了调度器.尽管CPU处理现有的线程集的顺序时不确定的,但是调度器将倾向于让优先权最高的线程先执行.优先级较低的线程仅仅时执行的频率较低.
- 在大多数时间里,所有线程都应该以默认的优先级运行.试图操纵线程优先级通常是一种错误.
- 尽管JDK有10个优先级,但它与多数操作系统都不能映射得很好.唯一可移植性的方法是当调整优先级的时候,只使用MAX_PRIORITY,NORM_PRIORITY和MIN_PRIORITY三种级别.
- 后台线程:是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分.因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程.反过来说,只要有任何非后台线程还在运行,程序就不会终止.
- 必须在线程启动之前(调用start()之前)调用setDaemon()方法,才能把它设置称为后台线程.
- 一旦main()完成其工作,就没有什么能阻止程序终止了,因为除了后台线程之外,已经没有线程在运行了.main()线程被设定为短暂睡眠,所以可以观察到所有后台线程启动后的结果.
public class SelManaged implements Runnable {
private int countDown = 5;
private Thread t = new Thread(this);
public SelManaged(){
t.start();
}
public String toString(){
return Thread.currentThread().getName() + "(" + countDown + "), ";
}
@Override
public void run() {
while(true){
System.out.print(this);
if (--countDown == 0)
return;
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SelManaged();
}
}
}
- **注意:**start()实在构造器中调用的.这个示例相当简单,因此可能时安全的,但是你赢意识到,在构造器中启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味这该任务能够访问处于不稳定状态的对象.这是优选Executor而不是显式地创建Thread对象的另一个原因.
class InnerThread1{
private int countDown = 5;
private Inner inner;
private class Inner extends Thread{
Inner(String name){
super(name);
start();
}
public void run(){
try {
while(true){
print(this);
if (--countDown == 0){
return ;
}
sleep(10);
}
}catch (InterruptedException e){
print("interrupted");
}
}
public String toString(){
return getName() + ": " + countDown;
}
}
public InnerThread1(String name){
inner = new Inner(name);
}
}
//Using an anonymous inner class:
class InnerThread2{
private int countDown = 5;
private Thread t;
public InnerThread2(String name){
t = new Thread(name){
public void run(){
try {
while(true){
print(this);
if (--countDown == 0)return;
sleep(10);
}
}catch (InterruptedException e){
print("sleep() interrupted");
}
}
public String toString(){
return getName() + ": " + countDown;
}
};
t.start();
}
}
//Using a named Runnable implementation
class InnerRunnable1{
private int countDown = 5;
private Inner inner;
private class Inner implements Runnable{
Thread t;
Inner(String name){
t = new Thread(this,name);
t.start();
}
public void run(){
try {
while(true){
print(this);
if (--countDown == 0)return ;
TimeUnit.MILLISECONDS.sleep(10);
}
}catch (InterruptedException e){
print("sleep() interruption");
}
}
public String toString(){
return t.getName() + ": " + countDown;
}
}
public InnerRunnable1(String name){
inner = new Inner(name);
}
}
// Using an anonymous Runnable implementation:
class InnerRunnable2 {
private int countDown = 5;
private Thread t;
public InnerRunnable2(String name) {
t = new Thread(new Runnable() {
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public String toString() {
return Thread.currentThread().getName() +
": " + countDown;
}
}, name);
t.start();
}
}
// A separate method to run some code as a task:
class ThreadMethod {
private int countDown = 5;
private Thread t;
private String name;
public ThreadMethod(String name) { this.name = name; }
public void runTask() {
if(t == null) {
t = new Thread(name) {
public void run() {
try {
while(true) {
print(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
print("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
}
- 如果该线程只执行辅助操作,而不是该类的重要操作,那么这与在该类的构造器内部启动线程相比,可能是一种更加有用而适合的方式.
- 加入一个线程:一个线程可以在其他线程之上调用join方法,其效果是等待一段时间直到第二个线程结束才继续执行.如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复.
- 对join()方法的调用可以被中断,做法是再调用线程上调用interrupt()方法,这时需要用到try-catch子句.
- 注意:JavaSE5的java.until.concurrent类库包含诸如CyclicBarrier这样的工具,它们可能比最初的线程类库中的join()更加合适.
- 课外补充线程组:线程组持有一个线程集合.线程组的价值可以引用Joshua Bloch的话来总结,他在Sun时是软件架构师,订正并极大地改善了JDK1.2中Java集合类库:"最好把线程组看成是一次不成功的尝试,你只要忽略它就好了"而Sun公司也没有关于这个主题的官方声明,诺贝尔经济学奖得主Joseph Stiglitz的生活哲学可以用来解释这个问题,他被称为承诺升级理论(The Theory of Escalating Commitment):“继续错误的代价由别人来承担,而承认错误的代价由自己承担”
- 捕获异常由于线程的本质特性(个人认为main是一个线程其他也是一个线程相互之间有的关系只有main要等其他线程执行完才能结束),使得你不能捕捉从线程中逃逸的异常.一旦异常逃出任务run()方法,它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误异常.在javaSE5,就可以用Executor来解决这个问题.
import java.util.concurrent.*;
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
} ///:~
//将main的主体放到try-catch语句块中是没有作用的:这将产生与前面实例相同的效果:为捕获的异常
public class NaiveExceptionHandling {
public static void main(String[] args) {
try {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// This statement will NOT execute!
System.out.println("Exception has been handled!");
}
}
} ///:~
- 为了解决这个问题,我们要修改Executor产生线程的方式.Thread.UncaughtExceptionHandler时JavaSE5中的新街口,他允许你在每个Thread对象上都附着上一个异常处理器.Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用.
class ExceptionThread2 implements Runnable {
@Override
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory{
public Thread newThread(Runnable r){
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
t.setUncaughtExceptionHandler(
new MyUncaughtExceptionHandler()
);
System.out.println("eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
}/*out
zhuofai.demo12_1.HandlerThreadFactory@57829d67 creating new Thread
created Thread[Thread-0,5,main]
eh = zhuofai.demo12_1.MyUncaughtExceptionHandler@2acf57e3
run() by Thread[Thread-0,5,main]
eh = zhuofai.demo12_1.MyUncaughtExceptionHandler@2acf57e3
zhuofai.demo12_1.HandlerThreadFactory@57829d67 creating new Thread
created Thread[Thread-1,5,main]
eh = zhuofai.demo12_1.MyUncaughtExceptionHandler@7e4e6121
caught java.lang.RuntimeException
*///~
这个程序请Debugger一下试试,不是几句话能讲清楚的.
但是这里还是要探究几个函数和几个内部类(嵌套类))几个工厂类,有点迷不过感觉用的一般,所以到看的时候再次理解一下就行了.
处理器的使用:系统会检查线程专有版本,如果没有发现,则检查线程组是否有其专有的uncaughtException()方法,如果也没有,再调用defaultUncaughtExceptionHandler.
21.3共享受限资源
- 对于并发工作,防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况.防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁.
- 基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案.这意味着在给定时刻只允许一个任务访问共享资源.通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码.因为锁语句产生了一中互相排斥的效果,所以这种机制常常称为互斥量(mutex).
- Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持.当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁.
- 共享资源一般是以对象形式存在的内存片段,但也可以是文件 输入/输出端口,或者是打印机.
- 要控制对共享资源的访问,得先把它包进一个对象.然后把所有要访问这个资源的方法标记为synchronized.
- 如果某个任务处于一个对标记为synchronized的方法调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchroized方法的线程都会被阻塞.
- 声明synchronized方法的方式:synchroized void f(){} synchronized void g(){}
- **注意:**在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突.
- 每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用资源.
- 针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问.
- 什么时候使用同步呢?
如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且读写线程都必须用相同的监视器锁同步.(每个访问临界共享资源的方法都必须被同步,否则它们就不会正确地工作)
- 使用显式的Lock对象
- 如果在使用synchronized关键字时,某些事物事变了,那么就会抛出一个异常.但是你没有机会去做任何清理工作,以维护系统使其处于良好状态.有了显式Lock对象,你就可以使用finally子句将系统维护在正确的状态了.
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。
- 大体上,当你使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的Lock对象.例如:用synchronized关键字不能尝试着获取锁且最终获取锁都会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,你必须使用concurrent类库.
- 原子性与易变性
有关java线程的讨论中,一个常不正确的知识是"原子操作不需要进行同步控制". 原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的"上下文切换"之前(切换到其他线程执行)执行完毕.
Goetz测试:如果你可以编写用于现代微处理器的高性能JVM,那么就有资格去考虑是否可以避免同步.
原子性可以应用于除long和double之外的所有基本类型之上的"简单操作".对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存.但是JVM可以将64位(long和double变量)的读取和写入当作两个分离的32位操作来执行,这就产生了一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性(这有时被称为撕裂,因为你可能会看到部分被修改过的数值).
但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性(注意,在JavaSE5之前volatile一直未能正确工作).不同的JVM可以任一地提供更强的保证,但是你不应该依赖于平台相关性的特性.
- volatile关键字还确保了应用中的可视性.如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改,即便使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中.
- 如果一个域完全由synchronized方法或语句块来防护,那么不必将其设置为是volatile的.
- 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域.再次提醒,你的第一选择应该是使用synchronized关键字,这是最安全的方式.
- 原子类:JavaSE5引入了诸如AtomicInteger AtomicLong AtomicReference等特殊的原子性变量类.它们提供了原子性条件更新操作:boolean compareAndSet(expectedValue,updateValue);
- 这些类被调整为可以使用在某些现代处理器上可获得的,并且是在机器级别上的原子性,因此在使用它们时,通常不需要担心.对于常规编程来说,它们很少会派上用场,但是在涉及性能调优好似,它们就大有用武之地了.
- 临界区:有时我们只是希望防止多个线程同时访问方法内部的部分嗲吗而不是防止访问整个方法.通过这种方式分离出来的代码段被称为临界区(critical section)
- 它也使用synchronized关键字建立,这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:
synchronized(syncObject){
//This code can be accessed
//by only one task at a time
}
- 这也被称为同步控制块,在进入此段代码前,必须得到syncObject对象的锁,如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区.
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
for(int i = 0; i < 5; i++) {
print("f()");
Thread.yield();
}
}
public void g() {
synchronized(syncObject) {
for(int i = 0; i < 5; i++) {
print("g()");
Thread.yield();
}
}
}
}
public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
}
} /* Output: (Sample)
g()
f()
g()
f()
g()
f()
g()
f()
g()
f()
*///:~
- 线程本地存储防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享.线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储.
class Accessor implements Runnable {
private final int id;
public Accessor(int idn) { id = idn; }
public void run() {
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ": " +
ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value =
new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() { return value.get(); }
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new Accessor(i));
TimeUnit.SECONDS.sleep(3); // Run for a while
exec.shutdownNow(); // All Accessors will quit
}
} /* Output: (Sample)
#0: 9259
#1: 556
#2: 6694
#3: 1862
#4: 962
#0: 9260
#1: 557
#2: 6695
#3: 1863
#4: 963
...
*///:~
21.4终结任务
//有点复杂,可以做练习然后写一个简写版的
class Count {
private int count = 0;
private Random rand = new Random(47);
// Remove the synchronized keyword to see counting fail:
public synchronized int increment() {
int temp = count;
if(rand.nextBoolean()) // Yield half the time
Thread.yield();
return (count = ++temp);
}
public synchronized int value() { return count; }
}
//方法太多了所以比较混乱,只要看一些重要的方法就行了
class Entrance implements Runnable {
private static Count count = new Count();
private static List<Entrance> entrances =
new ArrayList<Entrance>();
private int number = 0;
// Doesn't need synchronization to read:
private final int id;
//**为了结束线程而设的静态volatile,多个线程共享同一份canceled**
private static volatile boolean canceled = false;
// Atomic operation on a volatile field:
public static void cancel() { canceled = true; }
public Entrance(int id) {
this.id = id;
// Keep this task in a list. Also prevents
// garbage collection of dead tasks:
entrances.add(this);
}
public void run() {
while(!canceled) {
synchronized(this) {
++number;
}
print(this + " Total: " + count.increment());
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch(InterruptedException e) {
print("sleep interrupted");
}
}
print("Stopping " + this);
}
public synchronized int getValue() { return number; }
public String toString() {
return "Entrance " + id + ": " + getValue();
}
public static int getTotalCount() {
return count.value();
}
public static int sumEntrances() {
int sum = 0;
for(Entrance entrance : entrances)
sum += entrance.getValue();
return sum;
}
}
public class OrnamentalGarden {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new Entrance(i));
// Run for a while, then stop and collect the data:
TimeUnit.SECONDS.sleep(3);
Entrance.cancel();
exec.shutdown();
if(!exec.awaitTermination(250, TimeUnit.MILLISECONDS))
print("Some tasks were not terminated!");
print("Total: " + Entrance.getTotalCount());
print("Sum of Entrances: " + Entrance.sumEntrances());
}
} /* Output: (Sample)
Entrance 0: 1 Total: 1
Entrance 2: 1 Total: 3
Entrance 1: 1 Total: 2
Entrance 4: 1 Total: 5
Entrance 3: 1 Total: 4
...
Entrance 1: 30 Total: 146
Entrance 0: 30 Total: 149
Entrance 3: 30 Total: 148
Entrance 4: 30 Total: 150
Stopping Entrance 2: 30
Stopping Entrance 1: 30
Stopping Entrance 0: 30
Stopping Entrance 3: 30
Stopping Entrance 4: 30
Total: 150
Sum of Entrances: 150
*///:~
- 在阻塞时终结sleep()这种情况,它使任务从执行状态变为阻塞状态,而有时你必须终止阻塞任务
- 线程状态:
- 进入阻塞状态:
- 通过调用slee(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行.
- 你通过调用wait()使线程挂起.直到线程得到了notify()或notifyAll()消息(或者在JavaSE5的java.until.concurrent类库中等价的singal()或signalAll()消息),线程才会进入就绪状态.
- 任务在等待某个输入/输出完成.
- 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁.
- 需要查看的问题:能够终止处于阻塞状态的任务.
- 中断:与之前用cancel来打断,和在Runnable.run()方法的中间打断它要棘手的多,因为当你打断被阻塞的任务时,可能需要清理资源.正因为这一点,在任务的run方法中间打断更像是抛出的异常,因此在Java线程中的这种类型的异常中断中用到了异常(这会滑向异常的不恰当用法,因为这意味着你经常用它们控制执行流程).仔细编写catch子句以正确清除所有事物.
- Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方式.
- 那么为了调用interrupt(),必须持有Thread对象.那么在新的concurrent类库似乎避免对Thread对象的直接操作,转而尽量通过Executor来执行所有操作(shutdownNow方法将发送一个interrupt()调用给他停止所有线程).当你希望同时关闭某个特定Executor的任务.那么通过调用submit()而不是executor()来启动任务,就可以持有该任务的上下文.并返回一个Funture<?>,关键在于你可以通过其调用cancel(),来中断某个特定任务.
class SynchronizedBlocked implements Runnable{
public synchronized void f(){
while(true)//Never releases lock
Thread.yield();
}
public SynchronizedBlocked(){
new Thread(){
public void run(){
f();//Lock acquired by this thread
}
}.start();//初始化过程完毕了已经并没有一直在初始化块里面没出来
}
@Override
public void run() {
print("Trying to call f()");
f();
print("Exiting SynchronizedBlocked.run()");
}
}
public class Interrupting {
private static ExecutorService exec =
Executors.newCachedThreadPool();
static void test(Runnable r)throws InterruptedException{
Future<?> f = exec.submit(r);
TimeUnit.MILLISECONDS.sleep(100);
print("Interrupting " + r.getClass().getName());
f.cancel(true);//Interrupts if running
print("Interrupt sent to " + r.getClass().getName());
}
public static void main(String[] args) throws InterruptedException {
test(new SynchronizedBlocked());
TimeUnit.SECONDS.sleep(3);
print("Aborting with System.exit(0)");
System.exit(0);//...since last 2 interrupts failed
}
}/*
Trying to call f()
Interrupting SynchronizedBlocked
Interrupt sent to SynchronizedBlocked
Aborting with System.exit(0)
*///:~
为了演示SynchronizedBlock我们首先必须获取锁.在匿名Thread类的对象通过调用f()获取了锁(这个线程必须有别于为SynchronizedBlock驱动run()线程,因为一个线程可以多次获得某个对象锁).由于f()永远都不会返回.因此这个锁永远不会释放,而SynchronizedBlock.run()在试图调用f(),并阻塞以等待这个锁被释放.
你可以中断对sleep()的调用(或者任何要求抛出InterruptedException的调用).但是你不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程,这意味着I/O具有锁住你的多线程程序的潜在可能.特别是基于Web的程序.这更是关乎利害.(shutdownNow()被调用之后以及在输入流上调用close()之前的延迟强调的是一旦底层资源关闭,任务将解除阻塞)关于这个后面的JavaSE5并发类库中添加了一个新特性.(后面有写)
public class MultiLock {
public synchronized void f1(int count) {
if(count-- > 0) {
print("f1() calling f2() with count " + count);
f2(count);
}
}
public synchronized void f2(int count) {
if(count-- > 0) {
print("f2() calling f1() with count " + count);
f1(count);
}
}
public static void main(String[] args) throws Exception {
final MultiLock multiLock = new MultiLock();
new Thread() {
public void run() {
multiLock.f1(10);
}
}.start();
}
} /* Output:
f1() calling f2() with count 9
f2() calling f1() with count 8
f1() calling f2() with count 7
f2() calling f1() with count 6
f1() calling f2() with count 5
f2() calling f1() with count 4
f1() calling f2() with count 3
f2() calling f1() with count 2
f1() calling f2() with count 1
f2() calling f1() with count 0
*///:~
- 被互斥与阻塞:如果尝试着在一个对象上调用其synchronized方法,而这个对象的锁已经被其他任务获得,那么调用任务将被挂起(阻塞),直至这个锁可获得. 一个任务应该能够调用在同一个对象中的其他的synchronized方法,而这个任务已经持有锁了.
- 只要任务以不可中断的方式被阻塞,那么都有潜在的会锁住程序程序的可能.JavaSE5并发类库中添加了一个新特性,即在ReentrantLock上阻塞的任务具备可以被中断的能力,这与在synchronized方法或临界区上阻塞的任务完全不同.
- 与I/O调用不同,interrupt()可以打断被互斥所阻塞的调用.
- 检查中断注意:当你在线程上调用interrupt()时,中断发生的唯一时刻时在任务要进入到阻塞操作中,或者已经在阻塞操作内部时(如你所见,除了不可中断的I/O或被阻塞的synchronized方法之外,在其余情况下,你无可事事).如果你遇到了只能通过在阻塞调用上抛出异常来退出,那么你无法总是可以离开run()循环.
- 因此如果你调用interrupt()已停止某个任务,那么在run()循环碰巧没有产生任何阻塞调用的情况下,这种机会是由中断状态来表示的,其状态可以通过调用interrupt()来设置.你可以通过调用interrupted()来检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态.
//经典程序 展示了典型的惯用法,应该在run()方法中使用它来处理在终端状态被设置时,被阻塞和不被阻塞的各种可能:
class NeedsCleanup {
private final int id;
public NeedsCleanup(int ident) {
id = ident;
print("NeedsCleanup " + id);
}
public void cleanup() {
print("Cleaning up " + id);
}
}
class Blocked3 implements Runnable {
private volatile double d = 0.0;
public void run() {
try {
while(!Thread.interrupted()) {
// point1
NeedsCleanup n1 = new NeedsCleanup(1);
// Start try-finally immediately after definition
// of n1, to guarantee proper cleanup of n1:
try {
print("Sleeping");
TimeUnit.SECONDS.sleep(1);
// point2
NeedsCleanup n2 = new NeedsCleanup(2);
// Guarantee proper cleanup of n2:
try {
print("Calculating");
// A time-consuming, non-blocking operation:
for(int i = 1; i < 2500000; i++)
d = d + (Math.PI + Math.E) / d;
print("Finished time-consuming operation");
} finally {
n2.cleanup();
}
} finally {
n1.cleanup();
}
}
print("Exiting via while() test");
} catch(InterruptedException e) {
print("Exiting via InterruptedException");
}
}
}
public class InterruptingIdiom {
public static void main(String[] args) throws Exception {
if(args.length != 1) {
print("usage: java InterruptingIdiom delay-in-mS");
System.exit(1);
}
Thread t = new Thread(new Blocked3());
t.start();
TimeUnit.MILLISECONDS.sleep(new Integer(args[0]));
t.interrupt();
}
} /* Output: (Sample)
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
Finished time-consuming operation
Cleaning up 2
Cleaning up 1
NeedsCleanup 1
Sleeping
Cleaning up 1
Exiting via InterruptedException
*///:~
21.5线程之间的协作
- 当任务协作时,关键问题是这些任务之间的握手.为了实现这种握手,我们使用了相同的基础特性:互斥.
- You don’t want to idly loop while testing the condition inside your task; this is called busy waiting
- It’s important to understand that sleep( ) does not release the object lock when it is called, and neither does yield( ).
- There are two forms of wait( ). One version takes an argument in milliseconds that has the same meaning as in sleep( ): “Pause for this period of time.” But unlike with sleep( ), with wait(pause):
- The object lock is released during the wait( ).
- You can also come out of the wait( ) due to a notify( ) or notifyAll( ), in addition to letting the clock run out
- 使用notify()而不是notifyAll()是一种优化.使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务.另外,为了使用notify(),所有的任务必须等待相同的条件,因为如果你有多个任务在等待不同的条件,那么你就不知道是否唤醒了恰当的任务.如果使用notify(),当条件发生变化时,必须只有一个任务能够从中受益.最后这些限制对所有可能存在的子类都必须总是起作用的.如果这些规则中任何一条不满足,那么你就必须使用notifyAll()而不是notify().
BlockingQueue 实现主要用于生产者-使用者队列,但它另外还支持 Collection 接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不 会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
//以下是基于典型的生产者-使用者场景的一个用例。注意,BlockingQueue 可以安全地与多个生产者和多个使用者一起使用。
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
- 死锁 : which in turn waits for another task, and so on, until the chain leads back to a task waiting on the first one. You get a continuous loop of tasks waiting on each other, and no one can move. This is called deadlock.
- 哲学家问题…我真的是无语
- 死锁发生的四个条件
- 互斥条件
- 循环等待条件
- 打破其中的一个条件即可结束死锁
21.7 新类库中的构件
- CountDownLatch
//示例用法: 下面给出了两个类,其中一组 worker 线程使用了两个倒计数锁存器:
//第一个类是一个启动信号,在 driver 为继续执行 worker 做好准备之前,它会阻止所有的 worker 继续执行。
//第二个类是一个完成信号,它允许 driver 在完成所有 worker 之前一直等待。
class Driver { // ...
void main() throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
doSomethingElse(); // don't let run yet
startSignal.countDown(); // let all threads proceed
doSomethingElse();
doneSignal.await(); // wait for all to finish
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}
- CyclicBarrier赛马游戏超有趣
- int await() 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待
/*public CyclicBarrier(int parties,
Runnable barrierAction)创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
参数:
parties - 在启动 barrier 前必须调用 await() 的线程数
barrierAction - 在启动 barrier 时执行的命令;如果不执行任何操作,则该参数为 null
抛出:
IllegalArgumentException - 如果 parties 小于 1
*/
class Solver {
final int N;
final float[][] data;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) { myRow = row; }
public void run() {
while (!done()) {
processRow(myRow);
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
}
public Solver(float[][] matrix) {
data = matrix;
N = matrix.length;
barrier = new CyclicBarrier(N,
new Runnable() {
public void run() {
mergeRows(...);
}
});
for (int i = 0; i < N; ++i)
new Thread(new Worker(i)).start();
waitUntilDone();
}
}
- Semaphore
/*
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。例如,下面的类使用信号量控制对内容池的访问:
*/
class Pool {
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
public Object getItem() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
public void putItem(Object x) {
if (markAsUnused(x))
available.release();
}
// Not a particularly efficient data structure; just for demo
protected Object[] items = ... whatever kinds of items being managed
protected boolean[] used = new boolean[MAX_AVAILABLE];
protected synchronized Object getNextAvailableItem() {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (!used[i]) {
used[i] = true;
return items[i];
}
}
return null; // not reached
}
protected synchronized boolean markAsUnused(Object item) {
for (int i = 0; i < MAX_AVAILABLE; ++i) {
if (item == items[i]) {
if (used[i]) {
used[i] = false;
return true;
} else
return false;
}
}
return false;
}
}
- Exchanger
/*
可以在对中对元素进行配对和交换的线程的同步点。每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象。Exchanger 可能被视为 SynchronousQueue 的双向形式。Exchanger 可能在应用程序(比如遗传算法和管道设计)中很有用。
用法示例:以下是重点介绍的一个类,该类使用 Exchanger 在线程间交换缓冲区,因此,在需要时,填充缓冲区的线程获取一个新腾空的缓冲区,并将填满的缓冲区传递给腾空缓冲区的线程。 */
class FillAndEmpty {
Exchanger<DataBuffer> exchanger = new Exchanger<DataBuffer>();
DataBuffer initialEmptyBuffer = ... a made-up type
DataBuffer initialFullBuffer = ...
class FillingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialEmptyBuffer;
try {
while (currentBuffer != null) {
addToBuffer(currentBuffer);
if (currentBuffer.isFull())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ... }
}
}
class EmptyingLoop implements Runnable {
public void run() {
DataBuffer currentBuffer = initialFullBuffer;
try {
while (currentBuffer != null) {
takeFromBuffer(currentBuffer);
if (currentBuffer.isEmpty())
currentBuffer = exchanger.exchange(currentBuffer);
}
} catch (InterruptedException ex) { ... handle ...}
}
}
void start() {
new Thread(new FillingLoop()).start();
new Thread(new EmptyingLoop()).start();
}
}