到目前为止,你学到的都是有关 顺序编程 的知识。即程序中的所有事物在任意时刻都只能执行一个步骤。 下面我们应该学一学并发编程吧。
并发“具有可论证的确定性,但是实际上具有不可确定性”。实际上,你可能无法编写出能够针对你的并发程序生成故障条件的测试代码。
Web系统是最常见的Java应用系统之一,而基本的Web库类,Servlet具有天生的多线程性–这很重要,因为Web服务器经常包含多个处理器,而并发是充分利用这些处理器的理想方式。
并发编程令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,而且在这两者之间没有明显的映射关系(而且通常只具有模糊的界限)。因此,必须理解所有这些问题和特例,以便有效地使用并发。
而并发解决的问题大体上可以分为两种:
1.速度
2.设计可管理性
速度问题初听起来很简单:如果你想要一个程序运行得更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。
如果你有一台多处理器的机器,那么就可以在这些处理器之间分布多个任务,从而可以极大地提高吞吐量。这是使用强有力的多处理器web服务器的常见情况,在为每个请求分配一个线程的程序中,它可以将大量的用户请求分布到多个CPU上。
但是,并发通常是提高运行在单处理器上的程序的性能。听起来有些违背直觉。在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务)。
但是,有些情况下,情况就不同了,比如阻塞。如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。
事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。
这里需要注意这句话的前提哦,我想应该有这么几个:
1.从性能的角度看
2.在单处理器机器上
另一个例子是事件驱动的编程。考虑比如手机页面上的一个“退出”按钮。如果不使用并发,则产生可响应用户界面的唯一方式就是所有的任务都周期性地检查用户输入。通过创建单独的执行线程来响应用户的输入,即使这个线程在大多数时间里都是阻塞的,但是程序可以保证一定程度的可响应性。
实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。
java采取了更加传统的方式,在顺序型语言的基础上提供了对线程的支持。与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建人物,这种方式产生的一个好处是操作系统的透明性。
线程的一大好处是可以使你从处理器层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上。
线程可以驱动任务,因此你需要一种可以描述任务的方式。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。例如:
package test4.copy;
class LiftOff implements Runnable{
L
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public String status(){
return "#"+id+"("+(countDown>0?countDown:"liftoff")+"),";
}
@Override
public void run() {
// TODO Auto-generated method stub
while(countDown-->0){
System.out.print(status());
Thread.yield();
}
}
}
标识符id可以用来区分任务的多个实例,它是final的,因为它一旦初始化就不希望被修改。
run()方法中通常总会有某种形式的循环。在run()中对静态方法Thread.yield()的调用时对线程调度器(java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议(注意,仅仅是建议,不能保证一定切换),他在声明:我已经执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机。
在下面的实例中,这个任务的run()不是由单独的线程驱动的,它是在main()中直接调用的(实际上,这里仍旧使用了线程,即总是分配给main()的那个线程):
public class MultiThread{
public static void main(String[] args){
LiftOff lf = new LiftOff();
LiftOff lf1 = new LiftOff();
lf.run();
lf1.run();
}
}
运行结果:
#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(liftoff),#1(9),#1(8),#1(7),#1(6),#1(5),#1(4),#1(3),#1(2),#1(1),#1(liftoff),
当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处–它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。例如:
public class MultiThread{
public static void main(String[] args){
Thread t = new Thread(new LiftOff());
t.start();
LiftOff lf = new LiftOff();
lf.run();
}
}
运行结果:
#1(9),#1(8),#1(7),#1(6),#1(5),#1(4),#1(3),#0(9),#1(2),#1(1),#0(8),#1(liftoff),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(liftoff),
因为main()所在的线程和t是两个不同的线程,都绑定了Runnable。Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。输出说明不同任务的执行在线程被换进换出时混在了一起。
当main()创建Thread对象时,它并没有捕获任何对这些对象的引用。在使用普通对象时,这对于垃圾回收俩说是一场公平的游戏,但是在使用Thread对象时,情况就不同了。每个Thread都“注册”了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。你可以从输出中看到,这些任务确实运行到了结束,因此,一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍旧会继续存在。
Java SE5中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无须显式地管理线程的生命周期。
Executor在Java SE5/6中是启动任务的优选方法。
我们可以使用Executor来代替显式地创建Thread对象。ExecutorService(具有服务生命周期的Executor,例如关闭)知道如何构建恰当的上下文来执行Runnable对象。在下面的例子中,CachedThreadPool将为每个任务都创建一个线程。注意,ExecutorService对象是使用静态的Executor方法创建的,这个方法可以确定其Executor类型:
public class CachedThreadPool{
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<3;i++){
exec.execute(new LiftOff());
}
exec.shutdown();
}
}
运行结果:
#0(9),#2(9),#1(9),#2(8),#0(8),#2(7),#2(6),#2(5),#1(8),#2(4),#0(7),#2(3),#2(2),#2(1),#1(7),#2(liftoff),#1(6),#0(6),#0(5),#0(4),#0(3),#0(2),#1(5),#0(1),#0(liftoff),#1(4),#1(3),#1(2),#1(1),#1(liftoff),
非常常见的情况是,单个的Executor被用来创建和管理系统中所有的任务。
shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程(在本例中,即驱动main()的线程)将继续运行在shutdown()被调用之前提交的所有任务。这个程序将在Executor中的所有任务完成之后尽快退出。
可以将CachedThreadPool替换成不同类型的Executor。例如:
Executor exec = Executors.newFixedThreadPool(Number)
有了FixedThreadPool,你就可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。
注意,在任务线程池中,现有线程在可能的情况下,都会被自动复用。
Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()中返回的值的类型,并且必须使用ExecutorService.submit()方法调用它。下面是示例:
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
public String call(){
return "return of taskWithResult"+id;
}
}
public class CallableDemo{
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList> results = new ArrayList>();
for(int i=0;i<3;i++){
results.add(exec.submit(new TaskWithResult(i)));
}
for(Future fs:results){
try {
//get()方法会阻塞直到完成
System.out.println(fs.get());
} catch (InterruptedException | ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
exec.shutdown();
}
}
}
}
结果:
return of taskWithResult0
return of taskWithResult1
return of taskWithResult2
submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。你可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪。
影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。
线程的优先级将该线程的重要性传递给了调度器。并不是优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
在绝大多数时间内,所有线程都应该以默认的优先级运行。试图操作线程优先级通常是一种错误。优先级代码:
class SimplePriorities implements Runnable{
private int countDown = 5;
private volatile double d;//no optimization
private int priority;
public String toString(){
return Thread.currentThread()+":"+countDown;
}
public SimplePriorities(int priority) {
// TODO Auto-generated constructor stub
this.priority = priority;
}
@Override
public void run() {
// TODO Auto-generated method stub
Thread.currentThread().setPriority(priority);
while(true){
//a expensive,interruptable operation
for(int i=0;i<1000;i++){
d+=(Math.PI+Math.E)/(double)i;
if(i%1000==0)
Thread.yield();
}
System.out.println(this);
if(--countDown==0) return;
}
}
}
public class PriorityDemo{
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
exec.execute(new SimplePriorities(Thread.MIN_PRIORITY));
}
exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
exec.shutdown();
}
}
结果:
Thread[pool-1-thread-3,1,main]:5
Thread[pool-1-thread-6,10,main]:5
Thread[pool-1-thread-5,1,main]:5
Thread[pool-1-thread-6,10,main]:4
Thread[pool-1-thread-4,1,main]:5
Thread[pool-1-thread-6,10,main]:3
Thread[pool-1-thread-2,1,main]:5
Thread[pool-1-thread-6,10,main]:2
Thread[pool-1-thread-2,1,main]:4
Thread[pool-1-thread-1,1,main]:5
Thread[pool-1-thread-2,1,main]:3
Thread[pool-1-thread-6,10,main]:1
Thread[pool-1-thread-5,1,main]:4
Thread[pool-1-thread-5,1,main]:3
Thread[pool-1-thread-3,1,main]:4
Thread[pool-1-thread-5,1,main]:2
Thread[pool-1-thread-3,1,main]:3
Thread[pool-1-thread-2,1,main]:2
Thread[pool-1-thread-3,1,main]:2
Thread[pool-1-thread-1,1,main]:4
Thread[pool-1-thread-4,1,main]:4
Thread[pool-1-thread-5,1,main]:1
Thread[pool-1-thread-3,1,main]:1
Thread[pool-1-thread-1,1,main]:3
Thread[pool-1-thread-2,1,main]:1
Thread[pool-1-thread-4,1,main]:3
Thread[pool-1-thread-1,1,main]:2
Thread[pool-1-thread-4,1,main]:2
Thread[pool-1-thread-1,1,main]:1
Thread[pool-1-thread-4,1,main]:1
如果将6的优先级也设为最低的话,结果:
Thread[pool-1-thread-1,1,main]:5
Thread[pool-1-thread-2,1,main]:5
Thread[pool-1-thread-1,1,main]:4
Thread[pool-1-thread-2,1,main]:4
Thread[pool-1-thread-3,1,main]:5
Thread[pool-1-thread-2,1,main]:3
Thread[pool-1-thread-2,1,main]:2
Thread[pool-1-thread-1,1,main]:3
Thread[pool-1-thread-3,1,main]:4
Thread[pool-1-thread-4,1,main]:5
Thread[pool-1-thread-2,1,main]:1
Thread[pool-1-thread-1,1,main]:2
Thread[pool-1-thread-1,1,main]:1
Thread[pool-1-thread-5,1,main]:5
Thread[pool-1-thread-3,1,main]:3
Thread[pool-1-thread-6,1,main]:5
Thread[pool-1-thread-4,1,main]:4
Thread[pool-1-thread-5,1,main]:4
Thread[pool-1-thread-6,1,main]:4
Thread[pool-1-thread-4,1,main]:3
Thread[pool-1-thread-5,1,main]:3
Thread[pool-1-thread-3,1,main]:2
Thread[pool-1-thread-6,1,main]:3
Thread[pool-1-thread-5,1,main]:2
Thread[pool-1-thread-4,1,main]:2
Thread[pool-1-thread-5,1,main]:1
Thread[pool-1-thread-3,1,main]:1
Thread[pool-1-thread-6,1,main]:2
Thread[pool-1-thread-4,1,main]:1
Thread[pool-1-thread-6,1,main]:1
可以看出,高的优先级并不是保证最先执行,而是一种频率问题。注意,优先级是在run()的开头部分设定的,在构造器中设置他们不会有任何好处,因为Executor在此刻还没有开始执行任务。
这个暗示将通过调用yield()方法来做出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。
当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。即:
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);//必须在start之前调用
daemon.start();
可以通过调用isDaemon()方法来确定一个线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程都将被自动设置为后台线程。
class DaemonThreadFactory implements ThreadFactory{
//重写newThread方法,使得每个创建的线程都为后台线程
@Override
public Thread newThread(Runnable r) {
// TODO Auto-generated method stub
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
public class DaemonFromFactory implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try{
while(true){
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread()+" "+this);
}
}catch(InterruptedException e){
System.out.println("Interrupted");
}
}
public static void main(String[] args) throws Exception{
//传入创建线程的工厂方法
ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
for(int i=0;i<10;i++){
exec.execute(new DaemonFromFactory());
}
System.out.println("all daemons started");
TimeUnit.MILLISECONDS.sleep(500);
}
}
程序运行结果:
all daemons started
Thread[Thread-2,5,main] test4.copy.DaemonFromFactory@42d792c2
Thread[Thread-4,5,main] test4.copy.DaemonFromFactory@189e7143
Thread[Thread-6,5,main] test4.copy.DaemonFromFactory@4a710158
Thread[Thread-3,5,main] test4.copy.DaemonFromFactory@c1e719b
Thread[Thread-5,5,main] test4.copy.DaemonFromFactory@202ad687
Thread[Thread-9,5,main] test4.copy.DaemonFromFactory@7b4ef7f0
Thread[Thread-0,5,main] test4.copy.DaemonFromFactory@48df2951
Thread[Thread-1,5,main] test4.copy.DaemonFromFactory@692e1ca8
Thread[Thread-7,5,main] test4.copy.DaemonFromFactory@6cb478f5
Thread[Thread-8,5,main] test4.copy.DaemonFromFactory@2fd38f41
Thread[Thread-7,5,main] test4.copy.DaemonFromFactory@6cb478f5
Thread[Thread-6,5,main] test4.copy.DaemonFromFactory@4a710158
Thread[Thread-2,5,main] test4.copy.DaemonFromFactory@42d792c2
Thread[Thread-5,5,main] test4.copy.DaemonFromFactory@202ad687
Thread[Thread-0,5,main] test4.copy.DaemonFromFactory@48df2951
Thread[Thread-4,5,main] test4.copy.DaemonFromFactory@189e7143
Thread[Thread-8,5,main] test4.copy.DaemonFromFactory@2fd38f41
Thread[Thread-9,5,main] test4.copy.DaemonFromFactory@7b4ef7f0
Thread[Thread-1,5,main] test4.copy.DaemonFromFactory@692e1ca8
Thread[Thread-3,5,main] test4.copy.DaemonFromFactory@c1e719b
Thread[Thread-7,5,main] test4.copy.DaemonFromFactory@6cb478f5
Thread[Thread-2,5,main] test4.copy.DaemonFromFactory@42d792c2
Thread[Thread-3,5,main] test4.copy.DaemonFromFactory@c1e719b
Thread[Thread-6,5,main] test4.copy.DaemonFromFactory@4a710158
Thread[Thread-5,5,main] test4.copy.DaemonFromFactory@202ad687
Thread[Thread-9,5,main] test4.copy.DaemonFromFactory@7b4ef7f0
Thread[Thread-1,5,main] test4.copy.DaemonFromFactory@692e1ca8
Thread[Thread-0,5,main] test4.copy.DaemonFromFactory@48df2951
Thread[Thread-8,5,main] test4.copy.DaemonFromFactory@2fd38f41
Thread[Thread-4,5,main] test4.copy.DaemonFromFactory@189e7143
Thread[Thread-7,5,main] test4.copy.DaemonFromFactory@6cb478f5
Thread[Thread-6,5,main] test4.copy.DaemonFromFactory@4a710158
Thread[Thread-3,5,main] test4.copy.DaemonFromFactory@c1e719b
Thread[Thread-4,5,main] test4.copy.DaemonFromFactory@189e7143
Thread[Thread-0,5,main] test4.copy.DaemonFromFactory@48df2951
Thread[Thread-2,5,main] test4.copy.DaemonFromFactory@42d792c2
Thread[Thread-1,5,main] test4.copy.DaemonFromFactory@692e1ca8
Thread[Thread-8,5,main] test4.copy.DaemonFromFactory@2fd38f41
Thread[Thread-5,5,main] test4.copy.DaemonFromFactory@202ad687
Thread[Thread-9,5,main] test4.copy.DaemonFromFactory@7b4ef7f0
可以试试将t.setDaemon(true)
注释掉,那么程序将会一直执行死循环。
值得注意的是,后台线程在不执行finally子句的情况下就会终止其run()方法:
class ADaemon implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
try{
System.out.println("starting a daemon");
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedException e){
System.out.println("catch exception");
}finally{
System.out.println("finally");
}
}
}
public class DaemonDontRunFinally{
public static void main(String[] args){
Thread thread = new Thread(new ADaemon());
thread.setDaemon(true);
thread.start();
System.out.println("start");
}
}
运行结果:
start
starting a daemon
如果注释掉thread.setDaemon(true)
,就会执行finally语句。
当最后一个非后台线程终止时,后台线程会“突然”终止。如果你不想这样,可以使用非后台的Executor,因为Executor控制的所有任务可以同时被关闭。在这种情况下,关闭将以有序的方式执行。
到目前为止,所有的任务类都实现了Runnable。下面有两种简化的方式,分别是直接使用Thread和Runnable。
1.在非常简单的情况下,直接使用Thread继承这种方式。
public class SimpleThread extends Thread{
private int countDown = 5;
private static int threadCount=0;
public SimpleThread(){
//store the thread name
super(Integer.toString(++threadCount));
start();
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "#"+getName()+"("+countDown+")";
}
public void run(){
while(true){
System.out.println(this);
if(--countDown==0){
return;
}
}
}
public static void main(String[] args){
for(int i=0;i<3;i++){
new SimpleThread();
}
}
}
运行结果为:
#1(5)
#3(5)
#3(4)
#2(5)
#2(4)
#2(3)
#2(2)
#2(1)
#3(3)
#3(2)
#3(1)
#1(4)
#1(3)
#1(2)
#1(1)
2.直接使用Runnable,在Runnable内部维护一个Thread。
public class SimpleRunnable implements Runnable{
private int countDown = 5;
private Thread t = new Thread(this);
public SimpleRunnable() {
// TODO Auto-generated constructor stub
t.start();
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "#"+t.getName()+"("+countDown+")";
}
public void run(){
while(true){
System.out.println(this);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(--countDown==0){
return;
}
}
}
public static void main(String[] args){
for(int i=0;i<3;i++){
new SimpleRunnable();
}
}
}
第二种方式,特殊点在于,实现接口使得你可以继承另一个不同的类,而从Thread继承将不行。
你应该注意到了,要执行的任务与驱动它的线程之间有一个差异。这个差异在java类库中尤为明显,因此你对Thread类实际没有任何控制权(并且这种隔离在使用执行器时更加明显,因为执行器将替你处理线程的创建和管理)。你创建任务,并通过某种方式将一个线程附着到任务上,以使得这个线程可以驱动任务。
在java中,Thread类自身不执行任何操作,它只是驱动附着于它身上的任务。
一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程才继续执行。举例说明:
如果线程t1在另一个线程t2上调用t2.join(),那么该t1线程将被挂起,直到目标线程t2结束才恢复。(即t2.isAlive()返回为假)。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时需要用到try-catch子句。
如前所述,使用线程的动机之一就是创建有响应的用户界面。
由于线程的本质属性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台。在java SE5以后,可以使用Executor来解决这个问题。
class ExceptionThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
throw new RuntimeException();
}
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
将会产生异常:
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
at test4.copy.ExceptionThread.run(ExceptionThread.java:114)
at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
如果在main()方法中加入try-catch还是不能够捕捉它。
try{
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}catch(Exception e){
System.out.println(e.getMessage());
}
原因是,我们不能在一个线程中捕捉另一个线程的异常。
为了解决这个问题,可以用Executor。Thread.UncaughtExceptionHandler是java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。其基本架构是:
class ExceptionThread2 implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
}
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
// TODO Auto-generated method stub
}
}
class HandlerThreadFactory implements ThreadFactory{
@Override
public Thread newThread(Runnable r) {
// TODO Auto-generated method stub
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException{
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
}
如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器。如下:
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
先看一个例子:
//IntGenerator.java
public abstract class IntGenerator {
private volatile boolean canceled =false;
public abstract int next();
public void cancel(){
canceled = true;
}
public boolean isCanceled(){
return canceled;
}
}
//EvenChecker.java
public class EvenChecker implements Runnable{
private IntGenerator generator;
private final int id;
public EvenChecker(IntGenerator g,int ident){
generator = g;
id = ident;
}
public void run(){
while (!generator.isCanceled()){
int val = generator.next();
if(val%2!=0){
System.out.println(val+ "not even");
generator.cancel();
}
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void test(IntGenerator gp,int count) {
// TODO Auto-generated method stub
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0;inew EvenChecker(gp,i));
}
exec.shutdown();
}
}
//EvenGenerator.java
public class EvenGenerator extends IntGenerator{
private int currentValue = 0;
public int next(){
++currentValue;//这里很危险
Thread.yield();//增加切换线程的可能性
++currentValue;
return currentValue;
}
public static void main(String[] args){
System.out.println("start");
EvenChecker.test(new EvenGenerator(), 50);
}
}
一个任务有可能在另一个任务执行第一个对currentValue的递增操作之后,但是没有执行第二个操作之前,调用next()方法(即,代码中被注释为“这里很危险”的地方)。事实上,上述代码将会很快出现错误。
有一点很重要,那就是要注意到递增程序自身也需要多个步骤,并且在递增过程中任务可能会被线程挂起–也就是说,在java中,递增不是原子性的操作。
对于并发工作,你需要某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况。
共享资源一般是以对象形式存在的内存片段,但也可以是文件,输入/输出端口,或者是打印机。
所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。所以,对于某个特定对象来说,其所有synchronized方法共享同一个锁,这可以被用来防止多个任务同时访问该对象。
注意,在使用并发访问时,将域(field)设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会发生冲突。
一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数变为0。在任务第一次给对象加锁的时候,计数变为1。
针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问。
应该什么时候同步呢?可以运用Brian的同步规则:
如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。
要解决上面代码的错误只需要在next方法前加上synchronized关键字。
使用显式的Lock对象:
上述代码可以改变为:
//MutexEvenGenerator.java
public class MutexEvenGenerator extends IntGenerator{
private int currentValue = 0;
private Lock lock = new ReentrantLock();
public int next(){
lock.lock();
try{
++currentValue;//这里很危险
Thread.yield();
++currentValue;
return currentValue;
}finally {
lock.unlock();
}
}
public static void main(String[] args){
System.out.println("start");
EvenChecker.test(new MutexEvenGenerator(), 50);
}
}
Lock对象必须被显式地创建,锁和释放。因此,它与内建的锁形式相比,代码缺乏优雅性。但是对于某些类型的问题来说,它更加灵活。
如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常。但是你没有任何机会去做清理工作,以维护系统使其处于良好状态。有了显式的Lock对象,你就可以使用finally将系统维护在正确的状态了。通常只有在解决特殊问题的时候,才使用显式地Lock对象。
一个常常不正确的知识是“原子操作不需要进行同步控制”。原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完毕。
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值操作与返回操作的)原子性。
不可过分依赖原子性而放弃同步!
在多处理器系统(现在以多核处理器的形式出现,即在单个芯片上有多个CPU)上,相对于单处理器系统而言,可视性问题远比原子性问题多得多。
一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其他任务也可能是不可视的(例如,修改只是暂时地存储在本地处理器的缓存中),因此不同的任务对应用的状态有不同的视图。另一方面,同步机制强制在处理器系统中,一个任务做出的修改必须在应用中是可视的。
如果没有同步机制。那么修改时可视将无法确定。
volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。即使使用了本地缓存,情况也是如此。volatile域会立即被写入到主存中,而读取操作就发生在主存中。
理解原子性和易变性不同的概念是很重要的。在非volatile域上的原子性不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或者语句块来防护,那就不必将其设置为是volatile的。
一个任务所做的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile。
什么才属于原子操作呢?对域中的值做赋值和返回操作通常都是原子性的。
盲目地使用原子性概念:
//AtomicityTest.java
public class AtomicityTest implements Runnable{
private int i = 0;
public int getValue(){
return i;
}
private synchronized void evenIncrement(){
i++;
i++;
}
@Override
public void run() {
while(true){
evenIncrement();
}
}
public static void main(String[] args){
ExecutorService exexc = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exexc.execute(at);
while(true){
int val = at.getValue();
if(val%2!=0){
System.out.println(val);
System.exit(0);
}
}
}
}
上述代码非常地迷惑了我一阵。发现它并发错误主要有:
1.尽管return i
确实是原子性操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取。
2.由于i不是valatile的,因此还存在可视性问题。
所以,getValue()和evenIncrement()必须是synchronized的。
如果你将一个域定义为volatile,那么它就会告诉编译器不要执行任何移除读取和写入操作的优化,这些操作的目的是用线程中的局部变量维护对这个域的精确同步。
Java SE5引入了诸如AtomicInteger,AtomLong,AtomicReference等特殊的原子性变量类。举例:
public class AtomicIntegerTest implements Runnable{
private AtomicInteger i = new AtomicInteger(0);
public int getValue(){
return i.get();
}
private void evenIncrement(){
i.addAndGet(2);
}
@Override
public void run() {
while(true){
evenIncrement();
}
}
}
有时希望多个线程同时访问方法中部分代码而不是整个方法,通过这种方式分离出来的代码段称为临界区(critical section),也使用关键字synchronized来建立。
synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。
synchronized(syncObject){
//this code can be accessed by only one task at a time
}
在进入到这段代码前,必须得到syncObject对象的锁。
演示如何把一个非线程安全的类,包装称应用于多线程环境下的类(很有意思):
//不是线程安全的类
class Pair{
private int x,y;
public Pair(int x,int y){
this.x = x;
this.y = y;
}
public Pair(){
this(0,0);
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void incrementX(){
x++;
}
public void incrementY(){
y++;
}
@Override
public String toString() {
return "x: "+x+" y: "+y;
}
public class PairValueNotEqual extends RuntimeException{
public PairValueNotEqual(){
super("value not equal: "+Pair.this);
}
}
public void checkState(){
if(x!=y){
throw new PairValueNotEqual();
}
}
}
//线程安全的manager,管理线程不安全的Pair类
abstract class PairManager{
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
public synchronized Pair getPair(){
//make a copy to make the original safe
return new Pair(p.getX(),p.getY());
}
//assume this a consuming operation
protected void store(){
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public abstract void increment();
}
class PairManager1 extends PairManager{
//用同步方法
@Override
public synchronized void increment() {
p.incrementX();
p.incrementY();
store();
}
}
class PairManager2 extends PairManager{
//用同步代码块
@Override
public void increment() {
synchronized (this){
p.incrementX();
p.incrementY();
store();
}
}
}
class PairManipulator implements Runnable{
private PairManager pm;
public PairManipulator(PairManager pm){
this.pm = pm;
}
@Override
public void run() {
while(true){
pm.increment();
}
}
@Override
public String toString() {
return "pair: "+pm.getPair()+" checkCpunter= "+pm.checkCounter.get();
}
}
class PairChecker implements Runnable{
private PairManager pm;
public PairChecker(PairManager pm){
this.pm = pm;
}
@Override
public void run() {
while(true){
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
}
public class CriticalSection{
//测试两种实现方法
static void testApproaches(PairManager pman1,PairManager pman2){
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator pm1 = new PairManipulator(pman1);
PairManipulator pm2 = new PairManipulator(pman2);
PairChecker pcheck1 = new PairChecker(pman1);
PairChecker pcheck2 = new PairChecker(pman2);
exec.execute(pm1);
exec.execute(pm2);
exec.execute(pcheck1);
exec.execute(pcheck2);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("pm1: "+pm1+" pm2: "+pm2);
System.exit(0);
}
public static void main(String[] args){
PairManager pman1 = new PairManager1();
PairManager pman2 = new PairManager2();
testApproaches(pman1,pman2);
}
}
可以想象一下这种情况:某人交给你一个非线程安全的Pair类,而你需要在一个线程环境下使用它。通过创建PairManager类就可以实现这一点,PairManager类持有一个Pair对象并控制它的一切访问。注意唯一的public方法是getPair(),它是synchronized的。对于抽象方法increment(),对increment()的同步控制将在实现的时候进行处理。
注意:在PairManager1实现中,因为synchronized关键字不属于方法特征签名的组成部分,所以在覆盖方法的时候加上去。
有时需要在另一个对象上同步,但是如果你要这么做,就必须确保所有相关的任务都是在同一个对象上同步的。
下面演示了两个任务可以同时进入同一个对象,只要这个对象上的方法是在不同的锁上同步即可。
public class DualSync {
private Object syncObject = new Object();
public synchronized void f(){
for (int i = 0; i < 10; i++) {
System.out.println("f");
Thread.yield();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void g(){
synchronized (syncObject){
for (int i = 0; i < 10; i++) {
System.out.println("g");
Thread.yield();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
DualSync sync = new DualSync();
new Thread(new Runnable() {
@Override
public void run() {
sync.f();
}
}).start();
sync.g();
}
}
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。
创建和管理线程本地存储可以由ThreadLocal类来实现。例如:
private static ThreadLocal value = new ThreadLocal(){
private Random rand = new Random(47);
@Override
protected Integer initialValue() {
return rand.nextInt(1000);
}
};
ThreadLocal对象通常当做静态域存储。在创建ThreadLocal时,只能通过get()和set()方法来访问该对象的内容。
在前面示例中,cancel()和isCanceled()(不突然终止)方法被放到一个所有任务都可以看到的类中。这些任务通过检查isCanceled()来确定何时终止他们自己,对于这个问题来说,这时一种合理的方式。但,在某些情况下,任务必须立刻终止。
线程状态:(一个线程可以处于以下四种状态之一)
1.新建(new):
当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器把这个线程转变为可运行状态或阻塞状态。
2.就绪(runnable):
在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片非线程,它就可以运行。不同于阻塞和死亡状态。
3.阻塞(blocked):
线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配时间片给线程。直到线程重新进入了就绪状态,它才有可能执行操作。
4.死亡(dead):
处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已经结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。
进入阻塞状态:
一个任务进入阻塞状态,可能有如下原因:
1.通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
2.通过调用wait()使线程挂起。直到线程得到了notify()或者notifyAll()消息(或者在Java SE5中的concurrent类库中等价的signal()或者signalAll()消息),线程才会进入就绪状态。
3.任务在等待某个输入/输出完成。(I/O操作)
4.任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。(等待另一个任务释放锁)
有时,你希望能够终止处于阻塞状态的任务。你可能希望让它主动地终止,那么你就必须强制这个任务跳出阻塞状态。
Thread类包含interrupt()方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位。Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方法。
为了调用interrupt(),你必须持有Thread对象。但是,新的类库concurrent似乎在避免对Thread对象的直接操作,转而尽量通过Executor来执行所有的操作。、
如果你在Executor上调用shutdown(),那么它将发送一个interrupt()调用给它启动的所有线程。
你能够中断对sleep()的调用(或者任何要求抛出InterruptedException的调用)。但是,你不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程。
这优点令人烦恼,特别是在创建执行I/O任务时,因为这意味着I/O具有锁住你的多线程程序的潜在可能。特别是对于基于Web的程序,这更关乎利害。
对于这类I/O任务不能被有效中断的问题,有一个笨拙但是行之有效的方法,即关闭任务在其上发生阻塞的底层资源。
public class CloseResource {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
Socket socket = new Socket("localhost",8080);
InputStream is = socket.getInputStream();
exec.execute(new IOBlocked(is));
TimeUnit.SECONDS.sleep(5);
System.out.println("shuting down all the threads");
List a = exec.shutdownNow();
TimeUnit.SECONDS.sleep(5);
is.close();
System.out.println("close end");
System.out.println("end");
}
}
class IOBlocked implements Runnable{
private InputStream in;
public IOBlocked(InputStream in){
this.in = in;
}
@Override
public void run() {
System.out.println("waiting for read");
try {
in.read();
} catch (IOException e) {
if(Thread.currentThread().isInterrupted()){
System.out.println("interrupted from blocked i/o");
}else{
throw new RuntimeException(e);
}
}
System.out.println(" exiting run method");
}
}
输出结果:
waiting for read
shuting down all the threads
close end
end
interrupted from blocked i/o
exiting run method
Process finished with exit code 0
幸运的是,在18章介绍的各种nio类提供了更加人性化的I/O中断。被阻塞的nio通道会自动地响应中断。更多关于NIO
被互斥所阻塞:
下面演示同一个互斥量如何被同一个任务多次获得:
public class MultiLock {
public synchronized void f1(int count){
if(count-->0){
System.out.println("f1 calling f2 with count "+count);
f2(count);
}
}
public synchronized void f2(int count) {
if(count-->0){
System.out.println("f2 calling f1 with count "+count);
f1(count);
}
}
public static void main(String[] args){
final MultiLock multiLock = new MultiLock();
new Thread(new Runnable() {
@Override
public void run() {
multiLock.f1(10);
}
}).start();
}
}
当你在线程上调用interrupt()方法时,中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时(如你所见,除了不可中断的I/O或被阻塞的synchronized方法之外,在其余的例外情况下,你无可事事)。
你可以通过调用interrupted()来检查中断状态,这不仅1.可以告诉你interrupt()是否被调用过,而且2.还可以清除中断状态。
之前我们讨论的问题是线程彼此之间的干涉,现在我们将讨论彼此之间的协调。当任务协作时,关键问题是这些任务之间的握手。
调用sleep()的时候锁并没有释放,调用yield()也属于这种情况,理解这一点很重要。另一方面,当一个任务在方法里遇到了对wait()的调用的时候,线程的执行将被挂起,对象上的锁被释放。
有两种形式的wait()。第一种版本1.接受毫秒数作为参数,含义与sleep()方法里参数的意思相同,都是指“在此期间暂停”。但是与sleep()不同的是,对于wait()而言:
1.在wait()期间对象锁是释放的
2.可以通过notify(),notifyAll(),或者令时间到期,从wait()中恢复执行
第二种,也是2.更常用形式的wait()不接受任何参数。这种wait()将无限等待下去,直到线程接收到notify()或者notifyAll()消息。
注意:wait(),notify()以及notifyAll()有一个比较特殊的方面,那就是这些方法是基类Object的一部分,而不是属于Thread的一部分。
只能在同步控制方法或者同步块里调用wait(),notify(),notifyAll()(因为他们都是要操作对象的锁啊!)。而sleep()方法因为不需要操作锁,所以sleep()方法可以在非同步控制方法里调用。
这里在补充一句:虽然在非同步方法或块中调用wait()等同类方法,能够通过编译。但运行的时候,会弹出java.lang.IllegalMonitorStateException
异常。
WaxOMatic.java有两个过程:一个是将蜡涂到Car上,一个是抛光它。
public class WaxOMatic {
public static void main(String[] args) throws InterruptedException {
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));
exec.execute(new WaxOn(car));
TimeUnit.SECONDS.sleep(20);
exec.shutdownNow();
}
}
class Car{
private boolean waxOn = false;
public synchronized void waxed(){
waxOn = true;
notifyAll();
}
public synchronized void buffed(){
waxOn = false;
notifyAll();
}
public synchronized void waitForWaxing() throws InterruptedException {
while(waxOn == false){
wait();
}
}
public synchronized void waitForBuffing() throws InterruptedException {
while(waxOn==true){
wait();
}
}
}
class WaxOn implements Runnable{
private Car car;
public WaxOn(Car car){
this.car = car;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
System.out.println("wax on");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed();
car.waitForBuffing();
}
}catch (InterruptedException e){
System.out.println("exiting via interrupt");
}
System.out.println("ending wax on task");
}
}
class WaxOff implements Runnable{
private Car car;
public WaxOff(Car car){
this.car = car;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
car.waitForWaxing();
System.out.println("wax off");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();
}
}catch (InterruptedException e){
System.out.println("exiting via interrupt");
}
System.out.println("ending wax off task");
}
}
它的过程还是比较简单的。
这里强调一点:必须用一个检查感兴趣的条件的while循环包围wait()。原因是:
1.可能有多个任务出于相同的原因在等待同一个锁,而第一个唤醒任务可能会改变这种状况。如果属于这种情况,那么这个任务应该被再次挂起,直至其感兴趣的条件发生变化。
2.在这个任务从其wait()中被唤醒的时刻,可能会有其他某个其他的任务已经做出了改变,从而使得这个任务在此时不能执行,或者执行其操作已显得无关紧要。此时,应该通过再次调用wait()来将其重新挂起。
3.也有可能某些任务处于不同的原因在等待你的对象上的说(在这种情况下必须使用notifyAll())。在这种情况下,你需要检查是否已经由正确的原因唤醒,如果不是,就再次调用wait()。
因此,其本质就是要检查所感兴趣的特定条件,并在条件不满足的情况下返回到wait()中。惯用的方法就是使用while来编写这种代码。
错失的信号:
如下编程方式可能会错失信号:
//T1:
synchronized (sharedMonitor){
//setup condition for T2
sharedMonitor.notify();
}
//T2:
while(someCondition){
//point 1
synchronized (sharedMonitor){
sharedMonitor.wait();
}
}
该问题的解决方案是防止在someCondition变量上产生竞争条件。下面是正确的编码方式:
//T2:
synchronized (sharedMonitor){
while(someCondition){
sharedMonitor.wait();
}
}
现在T1首先执行,当控制返回T2时,它将发现条件发生了变化,从而不会进入wait()。反过来,如果T2首先执行,那么他将进入wait(),并且稍后会由T1唤醒。因此,信号不会错失。
其实上面的对比代码想表达的意思很简单,在synchronized块中运用while加wait(),正如前面在synchronized方法中运用while加wait()一样。
因为在技术上,可能会有多个任务在单个Car对象上处于wait()状态,因此调用notifyAll()比只调用notify()更安全。
使用notify()而不是notifyAll()是一种优化。使用notify()时,在众多等待同一个锁的任务中只有一个被唤醒。因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务。
为了使用notify(),所有任务必须等待相同的条件,因为如果你有多个任务在等待不同的条件,那么你就不会直到是否唤醒了恰当的任务。如果使用notify(),当条件发生变化时,必须只有一个任务从中受益。
如果这些规则中有任何一条不满足,就必须使用notifyAll()而不是notify()。所以,总结一句话,使用notify()有很多限制。
一个令人困惑的描述:notifyAll()将唤醒“所有正在等待的任务”。这是否意味着,任何处于wait()状态的任务都将被任何对notifyAll()的调用而唤醒呢?不是的!事实上,当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。
初级的生产者与消费者模型,一个饭店里有一个厨师和一个服务员,这个服务员必须等待厨师准备好膳食。当厨师准备好时,会通知服务员,之后服务员上菜,然后返回继续等待。
class Meal{
private final int orderNum;
public Meal(int orderNum){
this.orderNum = orderNum;
}
@Override
public String toString() {
return "meal : "+orderNum;
}
}
class WaitPerson implements Runnable{
private Restaurant restaurant;
public WaitPerson(Restaurant restaurant){
this.restaurant = restaurant;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
synchronized (this){//取得自身的锁
while(restaurant.meal==null){//while将会保证在这里一直等待下去啊,直到条件变化
wait();//等待厨师做饭
}
Print.print("waitperson got "+restaurant.meal);
}
synchronized (restaurant.chef){//之所以要取得chef的锁,是为了调用其notifyAll()
restaurant.meal=null;
restaurant.chef.notifyAll();
}
}
}catch (InterruptedException e){
Print.print("waitperson interrupted");
}
}
}
class Chef implements Runnable{
private Restaurant restaurant;
private int count=0;
public Chef(Restaurant restaurant){
this.restaurant = restaurant;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
synchronized (this){//取得自身的锁
while(restaurant.meal!=null){//while将会保证在这里一直等待下去啊,直到条件变化
wait();//等待服务员拿走饭
}
if(++count==3){
Print.print("out of food,closing");
restaurant.exec.shutdownNow();
}
}
Print.print("order up");
synchronized (restaurant.waitPerson){//之所以要取得waitPerson的锁,是为了调用其notifyAll()
restaurant.meal=new Meal(count);
restaurant.waitPerson.notifyAll();
}
TimeUnit.MILLISECONDS.sleep(100);
}
}catch (InterruptedException e){
Print.print("chef interrupted");
}
}
}
public class Restaurant {
Meal meal;
Chef chef = new Chef(this);
ExecutorService exec = Executors.newCachedThreadPool();
WaitPerson waitPerson = new WaitPerson(this);
public Restaurant(){
exec.execute(chef);
exec.execute(waitPerson);
}
public static void main(String[] args){
new Restaurant();
}
}
代码输出:
order up
waitperson got meal : 1
order up
waitperson got meal : 2
out of food,closing
order up
chef interrupted
waitperson got meal : 3
Process finished with exit code 0
注意,wait()被包装在一个while()语句中,这个语句在不断地测试正在等待的事物。在并发应用中,某个其他的任务可能会在WaitPerson被唤醒时,会突然插足并拿走订单,唯一安全的方式是使用下面这种wait()的惯用法(当然要在恰当的同步内部,并采用防止错失信号可能性的程序设计)。
对notifyAll()的调用必须首先捕获waitPerson上的锁,而在WaitPerson.run()中的对wait()的调用会自动地释放这个锁,因此这是有可能实现的。
在前面的示例中,对于一个任务而言,只有一个单一的地点用于存放对象,从而使得另一个任务稍后可以使用这个对象,也就是在WaitPerson和Chef中的Restaurant对象。但是,在典型的生产者和消费者实现中,应使用先进先出队列来存储被生产和消费的对象。
wait()和notifyAll()方法以一种非常低级的方式解决了任务互操作的问题,即每次交互时都握手。
在很多情况下,你可以瞄向更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时候都只允许一个任务插入或移除元素。
在java.util.concurrent.BlockQueue接口中提供了这个队列,这个接口有大量的标准实现。你通常可以使用LinkedBlockingQueue,它是一个无界队列,还可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在它被阻塞之前,向其中放置有限数量的元素。
下面是一个例子:
吐司BlockingQueue,有一台机器有三个任务:一个制作吐司,一个给吐司抹黄油,另一个在抹过黄油的吐司上凃酱。我们可以通过各个处理过程之间的BlockingQueue来运行这个吐司制作程序。
class Toast{
//使用enum
public enum Status{DRY,BUTTERED,JAMMED}
private Status status = Status.DRY;
private final int id;
public Toast(int id){
this.id = id;
}
public void butter(){
status = Status.BUTTERED;
}
public void jam(){
status = Status.JAMMED;
}
public Status getStatus() {
return status;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "toast "+id+" : "+status;
}
}
//自定义自己的Queue
class ToastQueue extends LinkedBlockingQueue<Toast>{}
//step1
class Toaster implements Runnable{
private ToastQueue toastQueue;
private int count = 0;
private Random rand = new Random(47);
public Toaster(ToastQueue toastQueue){
this.toastQueue = toastQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
TimeUnit.MILLISECONDS.sleep(100+rand.nextInt(500));
//make toast
Toast t = new Toast(count++);
Print.print(t.toString());
//insert into queue
toastQueue.put(t);
}
}catch(InterruptedException e){
Print.print("toaster interrupted");
}
Print.print("toaster off");
}
}
//step2:apply butter to toast
class Butterer implements Runnable{
private ToastQueue dryQueue,butteredQueue;
public Butterer(ToastQueue dryQueue,ToastQueue butteredQueue){
this.dryQueue = dryQueue;
this.butteredQueue = butteredQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
//block until next piece of toast if available
Toast t = dryQueue.take();
t.butter();
Print.print(t.toString());
//insert into queue
butteredQueue.put(t);
}
}catch(InterruptedException e){
Print.print("butterer interrupted");
}
Print.print("butterer off");
}
}
//step3:apply jam to buttered toast
class Jammer implements Runnable{
private ToastQueue butteredQueue,finishQueue;
public Jammer(ToastQueue butteredQueue,ToastQueue finishQueue){
this.finishQueue = finishQueue;
this.butteredQueue = butteredQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
//block until next piece of toast if available
Toast t = butteredQueue.take();
t.jam();
Print.print(t.toString());
//insert into queue
finishQueue.put(t);
}
}catch(InterruptedException e){
Print.print("jammer interrupted");
}
Print.print("jammer off");
}
}
//consume the toast
class Eater implements Runnable{
private ToastQueue finishQueue;
private int count = 0;
public Eater(ToastQueue finishQueue){
this.finishQueue = finishQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
//block until next piece of toast is available
Toast t = finishQueue.take();
//verify that toast is coming in order and all pieces are getting jammed
if(t.getId()!=count++||t.getStatus()!= Toast.Status.JAMMED){
Print.print("error : "+t);
System.exit(1);
}else{
Print.print("chomp : "+t);
}
}
}catch(InterruptedException e){
Print.print("eater interrupted");
}
Print.print("eater off");
}
}
public class ToastOMatic {
public static void main(String[] args) throws InterruptedException {
ToastQueue dryQueue = new ToastQueue();
ToastQueue butterQueue = new ToastQueue();
ToastQueue finishQueue = new ToastQueue();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Toaster(dryQueue));
exec.execute(new Butterer(dryQueue,butterQueue));
exec.execute(new Jammer(butterQueue,finishQueue));
exec.execute(new Eater(finishQueue));
TimeUnit.SECONDS.sleep(2);
exec.shutdownNow();
}
}
Toast是一个使用enum值的优秀示例。注意,这个示例中没有任何显式的同步(即使用Lock对象或者synchronized关键字的同步),因为同步由队列(其内部是同步的)和系统的设计隐式地管理了–每片Toast在任何时刻都只由一个任务在操作。因为队列的阻塞,使得处理过程将被自动地挂起和恢复。在使用显式的wait()和notifyAll()时存在的类和类之间的耦合被消除了,因为每个类都只和它的BlockingQueue通信。
哲学家就餐问题就是一个经典的死锁例证。问题描述:作为哲学家,他们很穷,只能买5根筷子(更一般的讲,筷子和哲学家的数量相同)。他们围坐在桌子周围,每人之间放一根筷子。当一个哲学家要就餐时,这个哲学家必须同时拿到左边和右边的筷子。如果筷子被占用,那么哲学家就必须等待,直至可得到必需的筷子。
当下列四个条件同时满足时,就会发生死锁:
1.互斥条件。任务使用的资源中至少有一个是不能共享的。一根筷子一次就只能被一个哲学家使用。
2.至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。哲学家必须拿着一根筷子然后等待另一根筷子。
3.资源不能被任务抢占,任务必须把资源释放当做普通事件。哲学家很有礼貌,他们不会从其他哲学家手上抢筷子。
4.必须要有循环等待。A等待B,B等待C,C等待A,这样就循环等待了。所有哲学家都试图先得到左边的筷子,然后得到右边的筷子,所以发生了循环等待。
因为要发生死锁,所有这些条件都必须全部满足,所以要防止死锁的话,只需破坏其中一个即可。在程序中,防止死锁最容易的方法是破坏第4个条件。
具体到哲学家问题中,如果最后一个哲学家被初始化称先拿右边的,再拿左边的,那么永远不会有循环等待,所以不会形成死锁。
新构件:
1.CountDownLatch
它被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。
2.CyclicBarrier
它适用于这样的情况:你希望创建一个任务,它们并行地执行工作,然后在进行下一个步骤之前等待,直至所有任务都完成(这看起来有些像join())。它使得所有的并行任务都将在栅栏处列队,因此可以一致地向前移动。这非常像CountDownLatch,只是CountDownLatch是只触发一次的事件,而CyclicBarrier可以多次重用。
3.DelayQueue
这时一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期的时间最长。如果没有任何延迟到期,那么就不会有任何头元素,并且poll()将返回null(正因为这样,你不能将null放置在这种队列中)。
4.PriorityBlockingQueue
这是一个很基础的优先级队列,它具有可阻塞的读取操作。
5.Semaphore
正常的锁(来自concurrent.locks或者synchronized锁)在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。你可以将信号量看做是在向外分发使用资源的“许可证”,尽管实际上没有使用任何许可证对象。
6.Exchanger
Exchanger是在两个任务之间交换对象的栅栏。当这些任务进入栅栏时,它们各自拥有一个对象,当它们离开时,它们都拥有之前由对象持有的对象。Exchanger的典型应用场景是:一个任务在创建对象,这些对象的生产代价很高,而另一个任务在消费这些对象。通过这种方式,可以有更多的对象在被创建的同时被消费。
1.银行出纳员仿真
这个经典的仿真可以表示任何属于下面这种类型的情况:1.对象随机地出现,并且2.要求由数量有限的服务器提供随机数量的服务时间。通过构建仿真可以确定理想的服务器数量。
本例中,每个银行顾客要求一定数量的服务时间,这是出纳员必须花费在顾客身上,以服务顾客要求的时间单位的数量。服务时间的数量对每个顾客来说都是不同的,并且是随机确定的。另外,你不知道在每个时间间隔内有多少顾客会到达,因此这也是随机确定的:
//read-only objects dont require synchronization
class Customer{
private final int serviceTime;
public Customer(int serviceTime){
this.serviceTime = serviceTime;
}
public int getServiceTime() {
return serviceTime;
}
@Override
public String toString() {
return "[ "+serviceTime+" ]";
}
}
//teach the customer line to display itself
class CustomerLine extends ArrayBlockingQueue{
public CustomerLine(int maxLineSize){
super(maxLineSize);
}
@Override
public String toString() {
if(this.size()==0){
return "[empty]";
}
StringBuilder result = new StringBuilder();
for (Customer customer:this){
result.append(customer);
}
return result.toString();
}
}
//randomly add customers to a queue
class CustomerGenerator implements Runnable{
private CustomerLine customers;
private static Random rand = new Random(47);
public CustomerGenerator(CustomerLine customerLine){
customers = customerLine;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
TimeUnit.MILLISECONDS.sleep(rand.nextInt(300));
customers.put(new Customer(rand.nextInt(1000)));
}
}catch (InterruptedException e){
Print.print("customer generator interrupted");
}
Print.print("customer generator end");
}
}
class Teller implements Runnable,Comparable{
private static int counter = 0;
private final int id = counter++;
//customers served during this shift
private int customersServed = 0;
private CustomerLine customers;
private boolean servingCustomerLine = true;
public Teller(CustomerLine cq){
customers = cq;
}
//used by priority queue
//在priority queue中排序比较的值为customersServed
@Override
public synchronized int compareTo(Teller other) {
return customersServed1:(customersServed==other.customersServed?0:1);
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
//serve customer from the line
Print.print("teller take a customer");
Customer customer = customers.take();
TimeUnit.MILLISECONDS.sleep(customer.getServiceTime());
synchronized (this){
customersServed++;
while(!servingCustomerLine){
wait();
}
}
}
}catch (InterruptedException e){
Print.print(this+" interrupted");
}
Print.print(this+" end");
}
public synchronized void doSthElse(){
customersServed = 0;
servingCustomerLine = false;
}
public synchronized void serveCustomerLine(){
assert !servingCustomerLine:"already serving "+this;
servingCustomerLine = true;
notifyAll();
}
@Override
public String toString() {
return "teller "+id+" ";
}
public String shortString() {
return "T "+id;
}
}
class TellerManager implements Runnable{
private ExecutorService exec;
private CustomerLine customers;
private PriorityBlockingQueue workingTellers = new PriorityBlockingQueue<>();
private Queue tellersDoingOtherThings = new LinkedList();
private int adjustmentPeriod;
private static Random rand = new Random(47);
public TellerManager(ExecutorService e,CustomerLine customers,int adjustmentPeriod){
exec = e;
this.customers = customers;
this.adjustmentPeriod = adjustmentPeriod;
//start with a single teller
Teller teller = new Teller(customers);
Print.print("construct method");
exec.execute(teller);
workingTellers.add(teller);
}
public void adjustTellerNumber(){
//this is actually a control system by adjusting
//the numbers. you can reveal stability issues in
//the conttol mechanism
//if line is too long. add another teller:
if(customers.size()/workingTellers.size()>2){
//if tellers are on break or doing
//another job,bring one back
Print.print("size is too large"+customers.size()+" "+workingTellers.size());
if(tellersDoingOtherThings.size()>0){
Print.print("bring sthElse teller back");
Teller teller = tellersDoingOtherThings.remove();
teller.serveCustomerLine();
//Inserts the specified element into this priority queue.
workingTellers.offer(teller);
return;
}
//else create(hire) a new teller
Print.print("hire a teller");
Teller teller = new Teller(customers);
exec.execute(teller);
workingTellers.add(teller);
return;
}
//if line ts short enough,remove a teller
if(workingTellers.size()>1&&customers.size()/workingTellers.size()<2){
reassingOneTeller();
}
//if there is no line, we only need one teller
if(customers.size()==0){
while(workingTellers.size()>1){
reassingOneTeller();
}
}
}
//give a teller a different job or a break
private void reassingOneTeller() {
Print.print("reassing one teller");
Teller teller = workingTellers.poll();
teller.doSthElse();
tellersDoingOtherThings.offer(teller);
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
TimeUnit.MILLISECONDS.sleep(adjustmentPeriod);
adjustTellerNumber();
System.out.println("\n");
System.out.print("customers: "+customers +" --- " );
System.out.print("working tellers: { ");
for(Teller teller:workingTellers){
System.out.print(teller.shortString()+" ");
}
System.out.print(" } --- ");
System.out.print("doSthElse tellers: { ");
for(Teller teller:tellersDoingOtherThings){
System.out.print(teller.shortString()+" ");
}
System.out.print(" }");
System.out.println("\n");
}
}catch (InterruptedException e){
Print.print(this+ "interrupted");
}
Print.print(this+" end");
}
@Override
public String toString() {
return "teller manager ";
}
}
public class BankTellerSimulation{
static final int MAX_LINE=50;
static final int ADJUSTMENT_PERIOD = 1000;
public static void main(String[] args)throws Exception{
ExecutorService exec = Executors.newCachedThreadPool();
//if line is too long,customers will leave
CustomerLine customers = new CustomerLine(MAX_LINE);
exec.execute(new CustomerGenerator(customers));
//manager will add and remove tellers as necessary:
exec.execute(new TellerManager(exec,customers,ADJUSTMENT_PERIOD));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
输出结果:
construct method
teller take a customer
teller take a customer
size is too large3 1
hire a teller
teller take a customer
customers: [ 200 ][ 207 ] --- working tellers: { T 1 T 0 } --- doSthElse tellers: { }
teller take a customer
teller take a customer
teller take a customer
teller take a customer
teller take a customer
customers: [ 861 ][ 258 ][ 140 ][ 322 ] --- working tellers: { T 1 T 0 } --- doSthElse tellers: { }
teller take a customer
teller take a customer
teller take a customer
teller take a customer
teller take a customer
size is too large6 2
hire a teller
customers: [ 575 ][ 342 ][ 804 ][ 826 ][ 896 ][ 984 ] --- working tellers: { teller take a customer
T 2 T 0 T 1 } --- doSthElse tellers: { }
teller take a customer
teller take a customer
teller take a customer
teller take a customer
size is too large10 3
hire a teller
customers: [ 984 ][ 810 ][ 141 ][ 12 ][ 689 ][ 992 ][ 976 ][ 368 ][ 395 ][ 354 ] --- working tellers: { teller take a customer
T 3 T 2 T 1 T 0 } --- doSthElse tellers: { }
teller take a customer
teller take a customer
teller take a customer
teller take a customer
teller take a customer
teller take a customer
teller take a customer
teller 1 interrupted
customer generator interrupted
customer generator end
teller 3 interrupted
teller 3 end
teller 2 interrupted
teller manager interrupted
teller manager end
teller 0 interrupted
teller 0 end
teller 1 end
teller 2 end
Process finished with exit code 0
根据场景确定几个类:
1.客户类:Customer
2.客户队列:CustomerLine
3.向队列里面添加客户的Runnable:CustomerGenerator
4.收银服务员Runnable:Teller
5.管理调度服务员和客户队列的Runnable:TellerManager
主main方法中exec不直接调用Teller这个Runnable,而是通过TellerManager的构造方法将exec向下传递,在其他方法中调用Teller的run()方法。
为了选择下一个出纳员,让其回到服务顾客的业务上,compareTo()方法将查看出纳员服务过的顾客数量,使得PriorityQueue可以自动地将工作量最小的出纳员推向前台。
TellerManager是各种活动的中心,它跟踪所有的出纳员以及等待服务的顾客。
既然Java包含老式的synchronized关键字和Java SE5中新的Lock和Atomic类,那么比较这些不同的方式,更多地了解它们各自的价值和使用范围,就会显得特别有意义。
比较天真的方式是针对每种方式都执行一个简单的测试。
但是,这容易出现“微基准测试”危险。这个术语通常指在隔离的,隔离上下文环境的情况下对某个特性进行性能测试。
很明显,使用Lock通常会使用synchronized要高效很多,而且synchronized的开销看起来变化范围太大,而Lock相对比较一致。那是否意味着永远不应该使用synchronized关键字呢?错!主要有两个原因:
1.在测试环境中,互斥方法的方法体是非常小的,通常这是一个很好的习惯—只互斥那些你绝对必须互斥的部分。但是,在实际中,可能会有很大的互斥体。那么在这些方法体中花费的时间会明显大于进入和退出互斥的开销,这样也就淹没了提高互斥速度带来的所有好处。也就是说,synchronized进入和退出的开销比较大,而运行速度比较快。
2.synchronized产生的代码比Lock产生的代码比,可读性提高了很多。
Atomic对象只有在非常简单的情况下才有用,这些情况通常包括你只有一个要被修改的Atomic对象,并且这个对象独立于其他所有的对象。
所以安全的推荐的方法是:以更加传统的方式(synchronized或Lock)入手,只有在性能方面的需求能够有明确指示时再替换为Atomic。
容器是所有编程中的基础工具,这其中自然也包括了并发编程,但早期的容器类,内部使用了大量的synchronized方法,用于多线程时,开销很大。Java SE5添加了新的容器,通过使用更灵巧的技术来消除加锁,从而提高了线程安全的性能。从上面可以推断出简单地通过synchronized来进行同步并不怎么高效。
这些免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的数据结构才会主动地与主数据结构进行交换,之后读取者就可以看到这个修改了。这有点像数据库主从复制的策略,这种思想比较常见。
比如,CopyOnWriteArrayList的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException,因此你不必编写特殊的代码去防范这种异常,就像你以前必须做的那样。
此外,新增加的跟CopyOnWriteArrayList相似的还有,CopyOnWriteArraySet,ConcurrentHashMap和ConcurrentLinkedQueue也使用了类似的技术。
乐观锁:
只要你主要是从免锁容器中读取,那么它就会比其synchronized对应物快很多,因为获取和释放锁的开销被省掉了。如果需要先免锁容器中执行少量写入,情况仍旧如此,但是什么是“少量”呢?
尽管Atomic对象将执行像decrementAndGet()这样的原子操作,但是某些Atomic类还允许你执行所谓的“乐观加锁”。这意味着当你执行某项计算时,实际上没有使用互斥,但是1.在这项计算完成,并且2.你准备更新这个Atomic对象时,你需要使用一个称为compareAndSet()的方法。
你将旧值和新值一起提交给这个compareAndGet()方法,如果旧值与它在Atomic对象中发现的值不一致,那么这个操作就失败—这意味着某个其他的任务已经于此操作执行期间修改了这个对象。
记住,我们在正常情况下将使用互斥(synchronized或者Lock)来防止多个任务同时修改一个对象,但是这里我们是“乐观的”,因为我们保持树为未锁状态,并希望没有任何其他任务插入修改它。所有这些又都是以性能的名义执行的—通过使用Atomic来替代synchronized或Lock,可以获得性能上的好处。
这里补充下“乐观锁”的知识:
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
一个重要的问题是,如果compareAndSet()操作失败会发生什么?这正是棘手的地方,也是你在应用这项技术时的受限之处,即只能针对能够吻合这些需求的问题。
一个例子:
考虑一个假想的仿真,它由长度为30的10000个基因构成,这可能是某种遗传算法的起源。假设伴随着遗传算法的每次“进化”,都会发生某些代价高昂的计算,因此你决定使用一台多处理器机器来分布这些任务以提高性能。另外,你将使用Atomic对象而不是传统的互斥技术来实现(它们太慢了)。因为你的模型的特性,使得如果在计算过程中产生冲突,那么发现冲突的任务将直接忽略它,并不会更新它的值。下面是代码:
public class FastSimulation {
static final int N_ELEMENTS = 10000;
static final int N_GENES = 30;
static final int N_EVOLVERS = 50;
static final AtomicInteger[][] GRID = new AtomicInteger[N_ELEMENTS][N_GENES];
static Random rand = new Random(47);
static class Evolver implements Runnable{
@Override
public void run() {
synchronized (GRID){
while(!Thread.interrupted()){
//randomly select an element to work on
int element = rand.nextInt(N_ELEMENTS);
for (int i = 0; i < N_GENES; i++) {
int previous = element-1;
if (previous<0){
previous = N_ELEMENTS-1;
}
int next = element+1;
if(next>=N_ELEMENTS){
next = 0;
}
int oldValue = GRID[element][i].get();
//perform some kind of modeling calculation
int newValue = oldValue+GRID[previous][i].get()+GRID[next][i].get();
newValue /=3;//average the three values
if (!GRID[element][i].compareAndSet(oldValue,newValue)){
//deal with failure
Print.print("old value changed from "+oldValue);
}
}
}
}
}
}
public static void main(String[] args)throws InterruptedException{
ExecutorService exec = Executors.newCachedThreadPool();
//init
for (int i = 0; i < N_ELEMENTS; i++) {
for (int j = 0; j < N_GENES; j++) {
GRID[i][j] = new AtomicInteger(rand.nextInt(1000));
}
}
for (int i = 0; i < N_EVOLVERS; i++) {
exec.execute(new Evolver());
}
TimeUnit.MILLISECONDS.sleep(5000);
exec.shutdownNow();
}
}
上述代码如果不加synchronized语句块,就会产生值冲突。
ReadWriteLock对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock使得你可以同时有多个读取者,只要它们不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。
读完本章,可能会发现,多线程非常复杂又难以正确使用。尽管多个任务可以并行工作,但是必须花大力气其防止这些任务之间彼此互相干涉的技术。
是多线程模型自身有问题吗?因为它来自于过程型编程世界,可能存在另一种不同的并发模型,更适合面向对象编程。有一种可替换的方式称为活动对象或行动者。之所以称这些对象是“活动的”,是因为每个对象都维护者他们自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中的一个。
读完本章并发的基础知识,应该理解:
1.可以运行多个独立的任务
2.必须考虑当这些任务关闭时,可能出现的所有问题
3.任务可能会在共享资源上彼此干涉。互斥(锁)是用来防止这种冲突的基本工具
4.如果任务设计得不够仔细,就会发生死锁
线程的一个额外好处是他们提供了轻量级的执行上下文切换(大概100条指令),而是重量级的进程上下文切换(上千条指令)。因为一个给定的进程内的所有线程共享相同的内存空间,轻量级的上下文切换只是改变了程序的执行序列和局部变量。而进程切换必须改变所有的内存空间。