java编程思想 --21并发

1)基本的线程机制
进程有3种状态,运行,就绪,阻塞。
线程有4种状态,运行,就绪,阻塞和终止。网上还有其他分法。
所谓进程和线程的联系区别,是经常问的问题,回答倒是难答。
简单说的话,一个程序至少有一个进程,而一个进程里面至少有一个线程,线程间共享进程内存空间,而进程与进程间独立。

一、线程驱动任务:
class MyThread implements Runnable{
    //Runnable 源码
    /*public
    interface Runnable {
        public abstract void run();
    }*/
    int number = 10;
    private static int count = 0;
    //之前用过,就是希望id初始化之后不再改变
    private final int id = count++;
    public MyThread(){}
    public MyThread(int number){
        number = number;
    }
    public String status(){
        return id+" "+ (number > 0? number :"zero");
    }
    public void run() {
        while(number-->0){
            System.out.println(status());
            Thread.yield();
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread m = new MyThread();
        m.run();
    }
}

Thread.yield()是线程调度器,其实就是说我跑过了,我把cpu让给你。
一run,马上就跑了起来。

二、用Thread驱动:
Thread(Runnable target)
Allocates a new Thread object.
public class TestThread {
    public static void main(String[] args) {
        Thread t = new Thread(new MyThread());
        t.start();
        System.out.println("late");
    }
}
start会为线程先做初始化操作,然后调用run方法。此时的run方法是其他线程调用,main线程继续执行,所以先打印出late。当然,如果改成t.run(),那么late就会最后打印了。

Tread的run和start有什么区别呢——java中thread的start()和run()的区别 。
按照里面的英文说法,假如我们再:
public static void main(String[] args) {
        Thread t = new Thread(new MyThread());

        Thread t1 = new Thread(new MyThread());

        t.start();
        t1.start();
        // t.run();
        // t1.run();
        System.out.println("late");
    }
    result:
    start方法:
    1 9
    0 9
    1 8
    0 8
    1 7
    0 7
    1 6
    0 6
    1 5
    0 5
    1 4
    0 4
    1 3
    0 3
    1 2
    0 2
    1 1
    0 1
    1 zero
    0 zero

    run方法:
    0 9
    0 8
    0 7
    0 6
    0 5
    0 4
    0 3
    0 2
    0 1
    0 zero
    1 9
    1 8
    1 7
    1 6
    1 5
    1 4
    1 3
    1 2
    1 1
    1 zero
run和start是不同的,两个线程使用start方法的时候,两个是同时运行的,结果是交错的。
两个线程使用run方法的时候,运行是顺序的,哪个在前,先运行哪个。所以看到的是t的输出先。
start方法会让线程运行是异步的(asynchronously),在启动的时候,其实start调用的是MyThread的run方法,但是此时执行的是其他线程,而执行main方法的线程继续,所以先打印了df,其余两个线程在交错运行中。
run方法是同步的(synchronously),只有等它运行完才进行下一步的操作,所以df在最后打印。
2)用Executor管理Thread
java.util.concurrentInterface Executor
An object that executes submitted Runnable tasks.  An Executor is normally used instead of explicitly creating threads. For example, rather than invokingnew Thread(new(RunnableTask())).start() for each of a set of tasks, you might use:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
一个执行提交的Runnable任务的类。代替显式地创建线程。
    ExecutorService es = Executors.newCachedThreadPool();
    es.execute(new MyThread());
    es.execute(new MyThread());
    es.shutdown();
  //es.execute(new MyThread()); shutdown之后不能提交新任务
newCachedThreadPool为每个任务都创建一个线程,
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(new MyThread());
newFixedThreadPool(int n)不会为每个任务都创建一个线程,而是限制了线程,需要线程的事件会到这个池里拿到线程。
newSingleThreadExecutor()的作用就和FixedThreadPool中n的值为1一样。
ExecutorService ess = Executors.newFixedThreadPool(1);
ess.execute(new MyThread());
ess.execute(new MyThread());
ess.execute(new MyThread());
ess.execute(new MyThread());
ess.execute(new MyThread());

0 9
0 8
0 7
0 6
0 5
0 4
0 3
0 2
0 1
0 zero
1 9
1 8
1 7
1 6
1 5
1 4
1 3
1 2
1 1
1 zero
2 9
2 8
2 7
2 6
2 5
2 4
2 3
2 2
2 1
2 zero
3 9
3 8
3 7
3 6
3 5
3 4
3 3
3 2
3 1
3 zero
4 9
4 8
4 7
4 6
4 5
4 4
4 3
4 2
4 1
4 zero

3)从任务中返回值
Runnable虽然执行自己的工作,但是不返回值,实现Callable接口可以返回值。
java.util.concurrent
Interface Callable<V>
Type Parameters:
V - the result type of method call
Modifier and Type Method and Description
V call()
Computes a result, or throws an exception if unable to do so.

class MyCall implements Callable<String>{
    private int id;
    public MyCall(int id){
        this.id = id;
    }
    public String call() throws Exception {
        return "call"+id;
    }
}

public class ExecuteCall {
    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        ArrayList<Future<String>> a = new ArrayList<Future<String>>();
        for (int i = 0; i < 5; i++) {
            a.add( es.submit(new MyCall(i)));
        }
        for(Future f : a){
            try {
                System.out.println(f.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}

这个例子很多新东西,因为我们是要返回值,所以用了MyCall实现了Call接口,并且必须使用ExecutorService.submit方法。
<T> Future<T> submit(Callable<T> task)
Submits a value-returning task for execution and returns a Future representing the pending results of the task.
返回的是Future对象。
public interface Future<V>
A  Future represents the result of an asynchronous computation.
Future代表的是异步计算的结果。
然后我们用了ArrayList来存储Future,通过foreach语法遍历。
V get()
      throws InterruptedException,
             ExecutionException
Waits if necessary for the computation to complete, and then retrieves its result.
get方法会找回结果。

4)休眠
记得以前看过一个排序算法是用休眠写的,很有趣,至今还记得,就是让大的数睡久一点才输出。
Mythread的修改:
public void run() {
    while(number-->0){
        System.out.println(status());
        try {
            //Thread.sleep(100);
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Thread.sleep是老版本的写法,TimeUnit.MILLISECONDS.sleep是SE5版本后的写法。其实后者也是用了Thread.sleep。
public void sleep(long timeout) throws InterruptedException {
      if (timeout > 0) {
         long ms = toMillis(timeout);
         int ns = excessNanos(timeout, ms);
         Thread.sleep(ms, ns);
      }
}

翻到火龙果的论坛帖子,其实后者和前者的区别就是可以变换单位,
TimeUnit.MICROSECONDS.sleep的话就是微秒,如果1000微秒的话那就是一毫秒,直接可以用TimeUnit.MILLISECONDS.sleep(1)。
这样就提供的更好的阅读性,不是有些题目会问你Thread.sleep方法里面的单位是多少吗?——毫秒。

5)优先级调度
public class TestPriority {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread());
        Thread t2 = new Thread(new MyThread());
        Thread t3 = new Thread(new MyThread());
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}

其实调度的映射做得不是很好,setPriority有10级优先级,1到10,数字越大优先级别越大。映射不好是指4不一定比5慢。

6)让步
让步的话就是前面写到的Thread.yield,给线程调度机制的信息就是我自己已经运行的差不多了,你们谁需要就谁去用吧。

这些是线程的基本概念,优先级,睡眠,之前没用过的就是Executor和TimeUnit.MILLISECONDS.sleep。

对象技术使你得以把程序划分成若干独立的部分。通常,你还需要把程序转换成彼此分离的,能独立运行的子任务。
每一个这些独立的子任务都被称为一个“线程”(thread)。你要这样去编写程序:每个线程都好象是在独自运行并且占有自己的处理器。处理器时间确实是通过某些底层机制进行分配的,不过一般来说,你不必考虑这些,这使得编写多线程程序的任务变得容易得多了。
所谓“进程”(process),是一个独立运行着的程序,它有自己的地址空间。“多任务”(multitasking)操作系统通过周期性地将处理器切换到不同的任务,使其能够同时运行不止一个进程(程序),而每个进程都象是连续运行、一气呵成的。线程是进程内部的单一控制序列流。因此一个进程内可以具有多个并发执行的线程。
多线程有多种用途,不过通常用法是,你的程序中的某个部分与一个特定的事件或资源联系了在一起,而你又不想让这种联系阻碍程序其余部分的运行。这时,可以创建一个与这个事件或资源相关联的线程,并且让此线程独立于主程序运行。
学习并发编程就像进入了一个全新的领域,有点类似于学习一门新的编程语言,或者至少要学习一整套新的语言概念。随着对线程的支持在大多数微机操作系统中的出现,在编程语言和程序库中也出现了对线程的扩展。总的来说,线程编程:
1.不仅看起来神秘,而且需要你改变编程时的思维方式。
2.各种语言中对线程的支持都很相似,所以只要理解了线程概念,那么在别的语言中要用到线程的话就有了共同语言。
尽管对线程的支持使Java看起来更复杂,不过这并不全是Java的错,线程本身就很讲究技巧。
要理解并发编程,其难度与理解多态机制差不多。如果你花了工夫,就能明白其基本机制,但要想真正地掌握它的实质,就需要深入的学习和理解。本章的目标就是要让你对并发的基本知识打下坚实的基础,使你能够理解其概念并编写出合理的多线程程序。注意,你可能会很容易就变得过分自信,所以在你编写任何复杂程序之前,应该学习一下专门讨论这个主题的书籍。
动机
使用并发最强制性的原因之一就是要产生能够作出响应的用户界面。考虑一个程序,它要执行某项CPU深度占用的计算,这样就会导致用户的输入被忽略,也就无法作出响应。问题的实质是:程序需要一边连续进行计算,同时还要把控制权交给用户界面,这样程序才能响应用户的操作。如果你有一个“退出”按钮,你一定不希望在程序的每段代码里都检测按钮状态,但你还是希望对这个按钮能够作出响应,就好像你定期对其进行检测一样。
传统的方法不可能一边连续执行其操作,同时又把控制权交给程序的其余部分。事实上,这听起来就像是不可能完成的任务,就好象让一个处理器同时出现在两个地方,但这恰恰是并发编程所能够提供的错觉效果。
并发还可以用来优化程序的吞吐量。比如,在你等待数据到达输入/输出端口的时候,你可以进行其他重要的工作。要是不用线程的话,唯一可行的办法就是不断查询输入/输出端口,这种方法不仅笨拙,而且很困难。
如果你有一台有多处理器的机器,多个线程就可以分布在多个处理器上,这可以极大地提高吞吐量。这种情况通常出现在有多个强劲处理器的web服务器上,在这种环境下,程序对于每个用户请求都将分配一个线程,这样就可以把大量的请求分配给多个处理器来处理。
需要牢记的是,具有多个线程的程序,必须也能够在只有单处理器的机器上运行。因此,不使用任何线程而写出具有同样功能的程序也是可能的。然而,使用多线程的重要好处是可以使程序的组织更有条理,因而能大大简化程序设计。对于某些类型的问题,比如模拟视频游戏,如果没有对并发的支持将会非常难以解决。
线程模型为编程带来了便利,它简化了在单一程序中交织在一起同时运行的多个操作。在使用线程时,处理器将轮流给每个线程分配其占用时间。每个线程都觉得自己在一直占用处理器,但事实上处理器时间是划分成片段分配给了所有的线程。例外情况是程序运行在具有多个处理器的机器上,但线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个处理器的机器上。所以,线程是一种建立透明的、可扩展的程序的方法,如果程序运行得太慢,为机器增添一个处理器就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理机系统的最合理方式。
在单处理器机器上,线程会降低一些运行效率,但是,从程序设计、资源平衡、用户使用方便等方面来看,还是非常值得的。一般来说,线程使你能得到更加松散耦合的设计;否则的话,你将不得不在部分代码中直接关注那些通常由线程处理的工作。
基本线程
写一个线程最简单的做法是从java.lang.Thread继承,这个类已经具有了创建和运行线程所必要的架构。Thread最重要的方法是run( ),你得重载这个方法,以实现你要的功能。这样,run()里的代码就能够与程序里的其它线程“同时”执行。
下面的例子创建了五个线程,每个线程由一个唯一的数字来标识,这个数字由一个静态成员变量产生。Thread类的run( )方法被重载,它的动作是在每次循环里计数值减一,当计数值为零的时候返回(在run( )方法返回的地点,将由线程机制终止此线程)。
//: c13:SimpleThread.java
// Very simple Threading example.
import com.bruceeckel.simpletest.*;
public class SimpleThread extends Thread {
private static Test monitor = new Test();
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
super("" + ++threadCount); // Store the thread name
start();
}
public String toString() {
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 < 5; i++)
new SimpleThread();
monitor.expect(new String[] {
"#1: 5",
"#2: 5",
"#3: 5",
"#5: 5",
"#1: 4",
"#4: 5",
"#2: 4",
"#3: 4",
"#5: 4",
"#1: 3",
"#4: 4",
"#2: 3",
"#3: 3",
"#5: 3",
"#1: 2",
"#4: 3",
"#2: 2",
"#3: 2",
"#5: 2",
"#1: 1",
"#4: 2",
"#2: 1",
"#3: 1",
"#5: 1",
"#4: 1"
}, Test.IGNORE_ORDER + Test.WAIT);
}
} ///:~
通过调用Thread类相应的构造器,可以给线程对象指定一个名字。这个名字可以在toString( )里用getName( )得到。
Thread对象的run( )方法一般总会有某种形式的循环,使得线程一直运行下去直到不再需要,所以你要设定跳出循环的条件(或者,就像前面的程序那样,直接从run( )返回)。通常,run( )被写成无限循环的形式,这就意味着,除非有某个条件使得run( )终止,否则它将永远运行下去(在本章后面你将看到如何安全地通知线程终止)。
你可以看到在main( )里创建并运行了一些线程。Thread类的start( )方法将为线程执行特殊的初始化动作,然后调用run( )方法。所以整个步骤是:首先调用构造器来构造对象,在构造器中调用了start( )方法来配置线程,然后由线程执行机制调用run( )。如果你不调用start( )(在后面的例子你将看到,你不必在构造器里调用start( )),线程永远不会启动。
因为线程调度机制的行为不是确定性的,所以每次运行该程序都会产生不同的输出结果。实际上,你要是在不同的JDK版本下运行这个简单的程序,就会发现程序输出的差异非常大。比如,以前版本的JDK经常都是不切片时间的,所以线程1可能首先循环执行完毕,然后是线程2完成其所有循环,如此下去。这样的做法除了启动这些线程开销更加昂贵以外,在实质上,与调用一个子程序然后马上完成该子程序所有循环的做法类似。用JDK 1.4你能得到与SimpleThread.java类似的输出,这表明了调度器执行了更合适的时间切片行为,每个线程看起来都得到了有秩序的服务。总的说来,JDK这种行为上的变化并没有被Sun所提到,所以你不能对线程的行为作任何假设。应付这类问题最好的办法就是在编写线程代码时尽可能保守些。
当在main( )中创建若干个Thread对象的时候,并没有获得它们中任何一个的引用。对于普通的对象,这会使它成为垃圾回收器要回收的目标,但对于Thread对象就不会了。每个Thread对象需要“注册”自己,所以实际上在某个地方存在着对它的引用,垃圾收集器只有在线程离开了run( )并且死亡之后才能把它清理掉。
让步
如果你知道run( )方法中已经完成了所需的工作,你可以给线程调度机制一个暗示:你的工作已经做得差不多了,可以让别的线程使用处理器了。这个暗示将通过调用yield( )方法的形式来作出。(不过这只是一个暗示,没有任何机制保证它将会被采纳。)
我们可以修改前面的例子,在每次循环之后调用yield( )。
//: c13:YieldingThread.java
// Suggesting when to switch threads with yield().
import com.bruceeckel.simpletest.*;
public class YieldingThread extends Thread {
private static Test monitor = new Test();
private int countDown = 5;
private static int threadCount = 0;
public YieldingThread() {
super("" + ++threadCount);
start();
}
public String toString() {
return "#" + getName() + ": " + countDown;
}
public void run() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
yield();
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new YieldingThread();
monitor.expect(new String[] {
"#1: 5",
"#2: 5",
"#4: 5",
"#5: 5",
"#3: 5",
"#1: 4",
"#2: 4",
"#4: 4",
"#5: 4",
"#3: 4",
"#1: 3",
"#2: 3",
"#4: 3",
"#5: 3",
"#3: 3",
"#1: 2",
"#2: 2",
"#4: 2",
"#5: 2",
"#3: 2",
"#1: 1",
"#2: 1",
"#4: 1",
"#5: 1",
"#3: 1"
}, Test.IGNORE_ORDER + Test.WAIT);
}
} ///:~
使用yield( )以后,程序的输出显得比较均衡。但要注意的是,如果输出的字符串再长一点的话,你就会得到与SimpleThread.java大致相同的输出。你可以试一试,逐步改变toString( )方法,每次输出更长的字符串,以观察效果。因为调度机制是抢占式的,它能决定在需要的时候中断一个线程并切换到别的线程,所以如果输入/输出(在main( )所在的线程内执行)占用了太多的时间,它将在run( )有机会调用yield( )之前被中断。一般来说,yield( )使用的机会并不多,你要是想对程序做认真的调整的话,就不能依赖于它。
休眠
另一种能控制线程行为的方法是调用sleep( ),这将使线程停止执行一段时间,该时间由你给定的毫秒数决定。在前面的例子里,要是把对yield( )的调用换成sleep( ),将得到如下结果:
//: c13:SleepingThread.java
// Calling sleep() to wait for awhile.
import com.bruceeckel.simpletest.*;
public class SleepingThread extends Thread {
private static Test monitor = new Test();
private int countDown = 5;
private static int threadCount = 0;
public SleepingThread() {
super("" + ++threadCount);
start();
}
public String toString() {
return "#" + getName() + ": " + countDown;
}
public void run() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void
main(String[] args) throws InterruptedException {
for(int i = 0; i < 5; i++)
new SleepingThread().join();
monitor.expect(new String[] {
"#1: 5",
"#1: 4",
"#1: 3",
"#1: 2",
"#1: 1",
"#2: 5",
"#2: 4",
"#2: 3",
"#2: 2",
"#2: 1",
"#3: 5",
"#3: 4",
"#3: 3",
"#3: 2",
"#3: 1",
"#4: 5",
"#4: 4",
"#4: 3",
"#4: 2",
"#4: 1",
"#5: 5",
"#5: 4",
"#5: 3",
"#5: 2",
"#5: 1"
});
}
} ///:~
在你调用sleep( )方法的时候,必须把它放在try块中,这是因为sleep( )方法在休眠时间到期之前有可能被中断。如果某人持有对此线程的引用,并且在此线程上调用了interrupt( )方法,就会发生这种情况。(如果对线程调用了wait( )或join( )方法,interrupt( )也会对线程有影响,所以对这些方法的调用也必须放在类似的try块中,我们将在后面学习这些方法)。通常,如果你想使用interrupt( )来中断一个挂起的线程,那么挂起的时候最好使用wait( )而不是sleep( ),这样就不可能在catch子句里的结束了。这里,我们把异常作为一个RuntimeException重新抛出,这遵循了“除非知道如何处理,否则不要捕获异常”的原则。
你将会发现程序的输出是确定的,每个线程都在下一个线程开始之前递减计数。这是因为在每个线程之上都使用了join( )(很快你就会学到),所以main( )在继续执行之前会等待线程结束。要是你不使用join( ),你可能会发现线程可以以任意顺序执行,这说明sleep( )也不是控制线程执行顺序的方法,它仅仅使线程停止执行一段时间。你得到的唯
一保证是线程将休眠至少100毫秒,这个时间段也可能更长,因为在休眠时间到期之后,线程调度机制也需要时间来恢复线程。
如果你必须要控制线程的执行顺序,你最好是根本不用线程,而是自己编写以特定顺序彼此控制的协作子程序。
优先权
线程的“优先权”(priority)能告诉调度程序其重要性如何。尽管处理器处理现有线程集的顺序是不确定的,但是如果有许多线程被阻塞并在等待运行,那么调度程序将倾向于让优先权最高的线程先执行。然而,这并不是意味着优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
下面的程序修改了SimpleThread.java,用以演示优先权。线程的优先权是通过使用setPriority( )方法进行调整的。
//: c13:SimplePriorities.java
// Shows the use of thread priorities.
import com.bruceeckel.simpletest.*;
public class SimplePriorities extends Thread {
private static Test monitor = new Test();
private int countDown = 5;
private volatile double d = 0; // No optimization
public SimplePriorities(int priority) {
setPriority(priority);
start();
}
public String toString() {
return super.toString() + ": " + countDown;
}
public void run() {
while(true) {
// An expensive, interruptable operation:
for(int i = 1; i < 100000; i++)
d = d + (Math.PI + Math.E) / (double)i;
System.out.println(this);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
new SimplePriorities(Thread.MAX_PRIORITY);
for(int i = 0; i < 5; i++)
new SimplePriorities(Thread.MIN_PRIORITY);
monitor.expect(new String[] {
"Thread[Thread-1,10,main]: 5",
"Thread[Thread-1,10,main]: 4",
"Thread[Thread-1,10,main]: 3",
"Thread[Thread-1,10,main]: 2",
"Thread[Thread-1,10,main]: 1",
"Thread[Thread-2,1,main]: 5",
"Thread[Thread-2,1,main]: 4",
"Thread[Thread-2,1,main]: 3",
"Thread[Thread-2,1,main]: 2",
"Thread[Thread-2,1,main]: 1",
"Thread[Thread-3,1,main]: 5",
"Thread[Thread-4,1,main]: 5",
"Thread[Thread-5,1,main]: 5",
"Thread[Thread-6,1,main]: 5",
"Thread[Thread-3,1,main]: 4",
"Thread[Thread-4,1,main]: 4",
"Thread[Thread-5,1,main]: 4",
"Thread[Thread-6,1,main]: 4",
"Thread[Thread-3,1,main]: 3",
"Thread[Thread-4,1,main]: 3",
"Thread[Thread-5,1,main]: 3",
"Thread[Thread-6,1,main]: 3",
"Thread[Thread-3,1,main]: 2",
"Thread[Thread-4,1,main]: 2",
"Thread[Thread-5,1,main]: 2",
"Thread[Thread-6,1,main]: 2",
"Thread[Thread-4,1,main]: 1",
"Thread[Thread-3,1,main]: 1",
"Thread[Thread-6,1,main]: 1",
"Thread[Thread-5,1,main]: 1"
}, Test.IGNORE_ORDER + Test.WAIT);
}
} ///:~
在这个版本中,toString( )方法被重载,并在里面使用了Thread.toString( )方法来打印线程的名称(你可以通过构造器来自己设置这个名称;这里是自动生成的名称,如Thread-1,Thread-2等),线程的优先权,以及线程所属的“线程组”。因为线程是自标识的,所以本例中没有使用threadNumber。重载的toString( )方法还打印了线程的倒计数值。
你可以看到thread 1的优先权最高,其余线程的优先权被设为最低。
在run( )里,加入了100,000次开销相当大的浮点运算,包括double类型的加法与除法。变量d用volatile修饰,以确保不进行优化。如果没有加入这些运算的话,你就看不到设置优先级的效果(试一试:把包含double运算的for循环注释掉)。有了这些运算,你就能观察到thread 1被线程调度机制优先选择(至少在我的Windows 2000机器上是这样)。
尽管向控制台打印也是开销大的操作,但在那种情况下看不出优先权的效果,因为向控制台打印不能被中断(否则的话,在多线程情况下控制台显示就乱套了),而数学运算是可以中断的。运算时间要足够长,这样线程调度机制才来得及进行改变,并注意到thread 1的优先权,使其被优先选择。
对于已存在的线程,你可以用getPriority( )方法得到其优先权,也可以在任何时候使用setPriority( )方法更改其优先权(这并不局限于像SimplePriorities.java里那样在构造器中修改)。
尽管JDK有10个优先级别,但它与多数操作系统都不能映射得很好。比如,Windows 2000有7个优先级且不是固定的,所以这种映射关系也是不确定的(尽管Sun的Solaris有231个优先级)。唯一可移植的策略是当你调整优先级的时候,只使用MAX_PRIORITY,NORM_PRIORITY,和MIN_PRIORITY三种级别。
后台线程
所谓“后台”(daemon)线程,是指程序运行的时候,在后台提供一种通用服务的线程,并且这种服务并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束,程序也就终止了。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main( )的就是一个非后台线程。
//: c13:SimpleDaemons.java
// Daemon threads don't prevent the program from ending.
public class SimpleDaemons extends Thread {
public SimpleDaemons() {
setDaemon(true); // Must be called before start()
start();
}
public void run() {
while(true) {
try {
sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(this);
}
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new SimpleDaemons();
}
} ///:~
你必须在线程启动之前调用setDaemon( )方法,才能把它设置为后台线程。在run( )里面,线程被设定为休眠一段时间。一旦所有的线程都启动了,程序马上会在所有的线程能打印信息之前立刻终止,这是因为没有非后台线程(除了main( ))使得程序保持运行。因此,程序未打印任何信息就终止了。
你可以通过调用isDaemon( )方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置成后台线程,如下例所示:
//: c13:Daemons.java
// Daemon threads spawn other daemon threads.
import java.io.*;
import com.bruceeckel.simpletest.*;
class Daemon extends Thread {
private Thread[] t = new Thread[10];
public Daemon() {
setDaemon(true);
start();
}
public void run() {
for(int i = 0; i < t.length; i++)
t[i] = new DaemonSpawn(i);
for(int i = 0; i < t.length; i++)
System.out.println("t[" + i + "].isDaemon() = "
+ t[i].isDaemon());
while(true)
yield();
}
}
class DaemonSpawn extends Thread {
public DaemonSpawn(int i) {
start();
System.out.println("DaemonSpawn " + i + " started");
}
public void run() {
while(true)
yield();
}
}
public class Daemons {
private static Test monitor = new Test();
public static void main(String[] args) throws Exception {
Thread d = new Daemon();
System.out.println("d.isDaemon() = " + d.isDaemon());
// Allow the daemon threads to
// finish their startup processes:
Thread.sleep(1000);
monitor.expect(new String[] {
"d.isDaemon() = true",
"DaemonSpawn 0 started",
"DaemonSpawn 1 started",
"DaemonSpawn 2 started",
"DaemonSpawn 3 started",
"DaemonSpawn 4 started",
"DaemonSpawn 5 started",
"DaemonSpawn 6 started",
"DaemonSpawn 7 started",
"DaemonSpawn 8 started",
"DaemonSpawn 9 started",
"t[0].isDaemon() = true",
"t[1].isDaemon() = true",
"t[2].isDaemon() = true",
"t[3].isDaemon() = true",
"t[4].isDaemon() = true",
"t[5].isDaemon() = true",
"t[6].isDaemon() = true",
"t[7].isDaemon() = true",
"t[8].isDaemon() = true",
"t[9].isDaemon() = true"
}, Test.IGNORE_ORDER + Test.WAIT);
}
} ///:~
在Daemon线程中把后台标志设置为“真”,然后派生出许多子线程,这些线程并没有被明确设置是否为后台线程,不过它们的确是后台线程。接着,线程进入了无限循环,并在循环里调用yield( )方法来把控制权交给其它进程。
一旦main( )完成其工作,就没什么能阻止程序终止了,因为除了后台线程之外,已经没有线程在运行了。main( )线程被设定为睡眠一段时间,所以你可以观察到所有后台线程启动后的结果。不这样的话,你就只能看见一些后台线程创建时得到的结果。(试试调整sleep( )休眠的时间,以观察这个行为。)
加入到某个线程
一个线程可以在其它线程之上调用join( )方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join( ),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive( )返回为假)。
你也可以在调用join( )时带上一个超时参数(单位可以是毫秒或者毫秒加纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join( )方法总能返回。
对join( )方法的调用可以被中断,做法是在调用线程上调用interrupt( )方法,这时需要用到try-catch子句。
下面这个例子演示了所有这些操作:
//: c13:Joining.java
// Understanding join().
import com.bruceeckel.simpletest.*;
class Sleeper extends Thread {
private int duration;
public Sleeper(String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}
public void run() {
try {
sleep(duration);
} catch (InterruptedException e) {
System.out.println(getName() + " was interrupted. " +
"isInterrupted(): " + isInterrupted());
return;
}
System.out.println(getName() + " has awakened");
}
}
class Joiner extends Thread {
private Sleeper sleeper;
public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
public void run() {
try {
sleeper.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName() + " join completed");
}
}
public class Joining {
private static Test monitor = new Test();
public static void main(String[] args) {
Sleeper
sleepy = new Sleeper("Sleepy", 1500),
grumpy = new Sleeper("Grumpy", 1500);
Joiner
dopey = new Joiner("Dopey", sleepy),
doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
monitor.expect(new String[] {
"Grumpy was interrupted. isInterrupted(): false",
"Doc join completed",
"Sleepy has awakened",
"Dopey join completed"
}, Test.AT_LEAST + Test.WAIT);
}
} ///:~
Sleeper是一个Thread类型,它要休眠一段时间,这段时间是通过构造器传进来的参数所指定的。在run( )中,sleep( )方法有可能在指定的时间期满时返回,但也可能被中断。在catch子句中,将根据isInterrupted( )的返回值报告这个中断。当另一个线程在该线程上调用interrupt( )时,将给该线程设定一个标志,表明该线程已经被中断。然而,异常被捕获时将清除这个标志,所以在异常被捕获的时候这个标志总是为假。除异常之外,这个标志还可用于其它情况,比如线程可能会检查其中断状态。
Joiner线程将通过在Sleeper对象上调用join( )方法来等待Sleeper醒来。在main( )里面,每个Sleeper都有一个Joiner,你可以在输出中发现,如果Sleeper被中断或者是正常结束,Joiner将和Sleeper一同结束。
编码的变体
到目前为止你所看到的所有简单例子中,线程对象都继承自Thread。这么做很合理,因为很显然,这些对象仅仅是作为线程而创建的,并不具有其它任何行为。然而,你的类也许已经继承了其它的类,在这种情况下,就不可能同时继承Thread(Java并不支持多重继承)。这时,你可以使用“实现Runnable接口” 的方法作为替代。要实现Runnable接口,只需实现run( )方法,Thread也是从Runnable接口实现而来的。
以下例子演示了这种方法的要点:
//: c13:RunnableThread.java
// SimpleThread using the Runnable interface.
public class RunnableThread implements Runnable {
private int countDown = 5;
public String toString() {
return "#" + Thread.currentThread().getName() +
": " + countDown;
}
public void run() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
for(int i = 1; i <= 5; i++)
new Thread(new RunnableThread(), "" + i).start();
// Output is like SimpleThread.java
}
} ///:~
Runnable类型的类只需一个run( )方法,但是如果你想要对这个Thread对象做点别的事情(比如在toString( )里调用getName( )),那么你就必须通过调用Thread.currentThread( )方法明确得到对此线程的引用。这里采用的Thread构造器接受一个Runnable对象和一个线程名称作为其参数。
当某个对象具有Runnable接口,即表明它有run( )方法,但也就仅此而已,不像从Thread继承的那些类,它本身并不带有任何和线程有关的特性。所以要从Runnable对象产生一个线程,你必须像例子中那样建立一个单独的Thread对象,并把Runnable对象传给专门的Thread构造器。然后你可以对这个线程对象调用start( ),去执行一些通常的初始化动作,然后调用run( )。
Runnable接口的方便之处在于所有事物都属于同一个类;也就是说,Runnable允许把基类和其它接口混在一起。如果你要访问某些资源,只需直接去做,而不用通过别的对象。不过,内部类也能同样方便地访问外部类的所有部分,所以,成员访问并不是使用Runnalbe接口形成混和类,而不是“一个Thread子类类型的内部类”的强制因素。
当你使用了Runnable,你通常的意思就是,要用run( )方法中所实现的这段代码创建一个进程(process),而不是创建一个对象表示该进程。这一点是有争议的,取决于你认为把线程作为一个对象来表示,或是作为完全不同的一个实体,即进程来表示,这两种方式哪一种更具实际意义1。如果你觉得应该是一个进程,你就不必拘泥于面向对象的原则,即“所有事物都是对象”。这也意味着,如果仅仅是想开启一个进程以驱动程序的某个部分,就没有理由把整个类写成是Runnable类型的。因此,使用内部类把和线程有关的代码隐藏在类的内部,似乎更合理,如下所示:
//: c13:ThreadVariations.java
// Creating threads with inner classes.
import com.bruceeckel.simpletest.*;
// Using a named inner class:
class InnerThread1 {
private int countDown = 5;
private Inner inner;
private class Inner extends Thread {
Inner(String name) {
super(name);
start();
}
public void run() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public String toString() {
return getName() + ": " + countDown;
}
}
public InnerThread1(String name) {
1 Java 1.0已经支持Runnable,而Java 1.1才引入内部类,这也部分说明了Runnable存在的原因。此外,传统的多线程模式着眼于要运行的功能,而不是表示成对象。我的习惯是尽可能从Thread继承;这样看起来更清楚和灵活。
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() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
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() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
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() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public String toString() {
return Thread.currentThread().getName() +
": " + countDown;
}
}, name);
t.start();
}
}
// A separate method to run some code as a thread:
class ThreadMethod {
private int countDown = 5;
private Thread t;
private String name;
public ThreadMethod(String name) { this.name = name; }
public void runThread() {
if(t == null) {
t = new Thread(name) {
public void run() {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
try {
sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
}
public class ThreadVariations {
private static Test monitor = new Test();
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerThread2("InnerThread2");
new InnerRunnable1("InnerRunnable1");
new InnerRunnable2("InnerRunnable2");
new ThreadMethod("ThreadMethod").runThread();
monitor.expect(new String[] {
"InnerThread1: 5",
"InnerThread2: 5",
"InnerThread2: 4",
"InnerRunnable1: 5",
"InnerThread1: 4",
"InnerRunnable2: 5",
"ThreadMethod: 5",
"InnerRunnable1: 4",
"InnerThread2: 3",
"InnerRunnable2: 4",
"ThreadMethod: 4",
"InnerThread1: 3",
"InnerRunnable1: 3",
"ThreadMethod: 3",
"InnerThread1: 2",
"InnerThread2: 2",
"InnerRunnable2: 3",
"InnerThread2: 1",
"InnerRunnable2: 2",
"InnerRunnable1: 2",
"ThreadMethod: 2",
"InnerThread1: 1",
"InnerRunnable1: 1",
"InnerRunnable2: 1",
"ThreadMethod: 1"
}, Test.IGNORE_ORDER + Test.WAIT);
}
} ///:~
InnerThread1创建了一个命名的内部类,它继承自Thread,并且在构造器中创建了一个此内部类的实例。如果你需要在别的方法中访问此内部类(比如新的方法),这么做就显得很合理。不过,绝大多数时候创建一个线程的原因仅仅是为了使用Thread的功能,所以建立一个命名的内部类往往没有必要。InnerThread2演示了另一种选择:在构造器内部建立了一个匿名内部类,它也继承自Thread,同时它被向上转型为对Thread的引用t。如果该类的别的方法需要访问t,它们可以通过Thread的接口访问,并且不需要知道这个对象的确切类型。
例子中的第三、四个类和前两个大致相同,但是它们实现了Runnable接口而不是从Thread继承。这只不过表明了在实现Runnable接口的情况下,并没有带来更多好处,而且实际上代码要稍微复杂一些(读起来也是)。所以,除非被迫使用Runnable,否则我更倾向于使用Thread。
ThreadMethod类演示了如何在方法内部创建一个线程。当你调用该方法准备运行这个线程时,在线程启动后该方法返回。当线程只是做一些辅助工作,而不是作为类的基本功能的时候,这种方案就比在类的构造器中启动一个线程显得更合理。
建立有响应的用户界面
如前所述,使用线程的动机之一就是建立有响应的用户界面。尽管我们要到第14章才接触到图形用户界面,你在本章还是可以看到一个基于控制台用户界面的简单例子。下面的例子有两个版本:一个在全神贯注于运算,所以不能读取控制台输入;另一个把运算放在线程里单独运行,此时就可以在进行运算的同时监听控制台输入。
//: c13:ResponsiveUI.java
// User interface responsiveness.
import com.bruceeckel.simpletest.*;
class UnresponsiveUI {
private volatile double d = 1;
public UnresponsiveUI() throws Exception {
while(d > 0)
d = d + (Math.PI + Math.E) / d;
System.in.read(); // Never gets here
}
}
public class ResponsiveUI extends Thread {
private static Test monitor = new Test();
private static volatile double d = 1;
public ResponsiveUI() {
setDaemon(true);
start();
}
public void run() {
while(true) {
d = d + (Math.PI + Math.E) / d;
}
}
public static void main(String[] args) throws Exception {
//! new UnresponsiveUI(); // Must kill this process
new ResponsiveUI();
Thread.sleep(300);
System.in.read(); // 'monitor' provides input
System.out.println(d); // Shows progress
}
} ///:~
UnresponsiveUI在一个无限的while循环里执行运算,显然程序不可能到达读取控制台输入的那一行(编译器被欺骗了,相信while的条件使得程序能到达读取控制台输入的那一行)。如果你把建立UnresponsiveUI的那一行解除注释再运行程序,那么要终止它的话,就只能杀死(kill)这个进程。
要想让程序有响应,就得把计算程序放在run( )方法中,这样它就能让出处理器给别的程序。当你按下回车键的时候,你可以看到计算确实在作为后台程序运行,同时还在等待用户输入(基于测试的原因,控制台输入这一行使用com.bruceeckel.simpletest.Test对象自动提交给System.in.read( ),这将在第15章中解释)。

共享受限资源
你可以把单线程程序当作在问题域求解的单一实体,每次只能做一件事情。因为只有一个实体,所以你永远不用担心诸如“两个实体试图同时使用同一个资源”这样的问题,比如:两个人在同一个地方停车,两个人同时走过一扇门,甚至是两个人同时说话。
在多线程的环境中,可以同时做多件事情。但是,“两个或多个线程同时使用同一个受限资源”的问题也出现了。必须防止这种资源访问的冲突,否则,就可能发生两个线程同时试图访问同一个银行帐户,向同一个打印机打印,改变同一个值等诸如此类的问题。
不正确地访问资源
考虑下面的例子,这里的类“保证”当你调用getValue( )方法时,总能返回一个偶数。同时,另外一个名为“Watcher”的线程不断调用getValue( )方法并检查返回值是否真的为偶数。这看起来好像没什么意义,因为从代码上看,返回值显然是偶数。但这恰恰是令人惊奇之处。下面是该程序的第一个版本:
//: c13:AlwaysEven.java
// Demonstrating thread collision over resources by
// reading an object in an unstable intermediate state.
public class AlwaysEven {
private int i;
public void next() { i++; i++; }
public int getValue() { return i; }
public static void main(String[] args) {
final AlwaysEven ae = new AlwaysEven();
new Thread("Watcher") {
public void run() {
while(true) {
int val = ae.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}.start();
while(true)
ae.next();
}
} ///:~
在main( )中,建立了一个AlwaysEven对象,它必须是final的,因为它要被一个继承自Thread的匿名内部类所访问。如果线程读出的值不是偶数,它将把这个值打印出来(以证明它捕获了对象的不稳定状态)并退出程序。
这个例子表明了使用线程会遇到的基本问题。你永远不会知道线程是何时运行的。想象一下,你坐在座子旁边,手里有一把叉子,准备叉起盘子里最后一块食物,当叉子碰到食物的时候,它忽然消失了(因为你的线程被挂起,另一个线程跑进来偷走了食物)。这就是你在写并发程序时要面临的问题。
你试图使用一个资源的同时,有时并不关心它是否正在被访问(比如别的盘子里的食物)。但为了让多线程能工作,你就需要某种方法来防止两个线程访问同一个资源,至少是在某个关键时间段内避免此问题。
要防止这类冲突,只要在线程使用资源的时候给它加一把锁就行了。访问资源的第一个线程给资源加锁,接着其它线程就只能等到锁被解除以后才能访问资源,这时某个线程就可以对资源加锁以进行访问。如果把汽车的前排座椅看成是受限资源的话,那么你的小孩大喊一声“我要坐”,就相当于在声明上锁。
一个资源测试框架
在我们继续之前,先来建立一个小型框架,试试能否简化对这种类型的线程例子的测试工作。我们可以把在多个例子中出现的常用代码分离出来做到这一点。首先,注意到“观察者”线程实际上在观察特定对象内部是否违例了约束条件。也就是说,假设对象具有保持其内部状态的条件,但如果你能从外部观察到对象的非法中间状态,那么从客户的观点来看,约束条件确实遭到了破坏(这并不是说对象在非法的中间状态下就不能存在,而是这种状态不该被客户观察到)。所以,我们不仅要检查约束条件是否被违反,还要知道这个违例的值是多少。要想用一次方法调用就得到这两个结果,我们得把它们捆绑在一个标记接口里,这个接口仅仅用来在代码中提供有意义的名字:
//: c13:InvariantState.java
// Messenger carrying invariant data
public interface InvariantState {} ///:~
在这种模式下,有关成功或是失败的信息被编码到类的名称和类型中,以使结果更可读。表示成功的类是:
//: c13:InvariantOK.java
// Indicates that the invariant test succeeded
public class InvariantOK implements InvariantState {} ///:~
要表示失败,InvariantFailure对象将包括一个对象,此对象表示了有关失败原因的信息,这样就可以显示出来:
//: c13:InvariantFailure.java
// Indicates that the invariant test failed
public class InvariantFailure implements InvariantState {
public Object value;
public InvariantFailure(Object value) {
this.value = value;
}
} ///:~
现在我们可以定义一个接口,任何需要对约束条件进行测试的类必须实现这个接口:
//: c13:Invariant.java
public interface Invariant {
InvariantState invariant();
} ///:~
在创建通用的“观察者”线程之前,要注意到本章中的某些例子也许不能在所有的平台上都按预期效果运行。这里的许多例子是故意演示在多线程环境下对单线程行为的影响,而这种情况并非总是发生2。相反,试图演示这种违例的例子也许不会(或者没有成功)造成任何影响。这时,我们需要某种方法在一段时间后能终止程序。下面的类通过继承标准类库中的Timer类做到了这一点:
//: c13:Timeout.java
// Set a time limit on the execution of a program
import java.util.*;
public class Timeout extends Timer {
public Timeout(int delay, final String msg) {
super(true); // Daemon thread
schedule(new TimerTask() {
public void run() {
System.out.println(msg);
System.exit(0);
}
}, delay);
}
2一些例子是在一台有双处理器的Win2K机器上编写的,这样冲突更容易发生。然而,在单处理器机器上运行同样的程序时可能会在相当长时间内没有冲突,正是这种不确定性使得多线程编程如此困难。你可以想象一下,你在单处理器机器上编写程序,并且认为代码是线程安全的,一旦把程序拿到一台多处理器机器上运行的时候,马上出了问题。
} ///:~
延迟的单位是毫秒,时间到期的话,将打印消息。注意通过调用super(true),此线程将作为一个后台线程而创建,所以要是你的程序采用别的方式结束,此线程将不会阻止程序终止。Timer.schedule( )方法传入了一个TimerTask的子类(此处作为匿名内部类创建)对象,这个对象的run( )方法将在schedule( )的第二个参数delay指定的时间到期之后执行。使用Timer一般来说比直接写代码调用sleep( )要简单和清晰。此外,设计Timer的目的就是承担大量并发调度任务,所以它能成为很有用的工具。
现在我们可以在InvariantWatcher线程里使用Invariant接口和Timeout类了:
//: c13:InvariantWatcher.java
// Repeatedly checks to ensure invariant is not violated
public class InvariantWatcher extends Thread {
private Invariant invariant;
public InvariantWatcher(Invariant invariant) {
this.invariant = invariant;
setDaemon(true);
start();
}
// Stop everything after awhile:
public
InvariantWatcher(Invariant invariant, final int timeOut){
this(invariant);
new Timeout(timeOut,
"Timed out without violating invariant");
}
public void run() {
while(true) {
InvariantState state = invariant.invariant();
if(state instanceof InvariantFailure) {
System.out.println("Invariant violated: "
+ ((InvariantFailure)state).value);
System.exit(0);
}
}
}
} ///:~
构造器接受一个Invariant对象的引用作为参数,它是要测试的对象,然后启动线程。第二个构造器调用第一个构造器,然后创建了一个Timeout,用来在一定的时间延迟之后终止所有的线程,如果程序中没有违反约束条件,那么线程就不可能因违反约束条件而终止,
此时就要用到它Timeout了。在run( )中,当前的InvariantState被获取和测试,如果有违例的话,违例的值将被打印出来。注意,我们不能在此线程里抛出异常,因为这只会终止线程,而不是终止程序。
现在AlwaysEven.java可以用这个框架重写:
//: c13:EvenGenerator.java
// AlwaysEven.java using the invariance tester
public class EvenGenerator implements Invariant {
private int i;
public void next() { i++; i++; }
public int getValue() { return i; }
public InvariantState invariant() {
int val = i; // Capture it in case it changes
if(val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}
public static void main(String[] args) {
EvenGenerator gen = new EvenGenerator();
new InvariantWatcher(gen);
while(true)
gen.next();
}
} ///:~
在定义invariant( )方法的时候,你必须把所有相关的值都存放到局部变量中。这样,你才能返回你真正测试的那个值,否则在返回的时候这个值可能已经(被别的线程)改变。
在这种情况下,问题已经不在于对象是否处于违反约束条件的状态,而是当对象处于这种中间的不稳定状态时,别的线程可能会调用它的方法。
资源冲突
对于EvenGenerator,最糟糕的莫过于客户线程可能会发现它处于不稳定的中间状态。尽管对象最终被观察到处于合法的状态,而且其内部一致性能够得到维护。但如果两个线程确实是在修改同一个对象,共享资源的冲突将变得更糟糕,因为这有可能会把对象设置成不正确的状态。
考虑简单的“信号量”(semaphore)概念,它可以看成是在两个线程之间进行通讯的标志对象。如果信号量的值是零,则它监控的资源是可用的,但如果这个值是非零的,则被监
控的资源不可用,所以线程必须等待。当资源可用的时候,线程增加信号量的值,然后继续执行并使用这个被监控的资源。因为把增加和减少当作是原子操作(也就是不能被中断),信号量能够保证两个线程同时访问同一资源的时候不至于冲突。
如果信号量能正确守护它所监控的资源,那么它一定不会处于不稳定的状态。下面是一个信号量概念的简化版本:
//: c13:Semaphore.java
// A simple threading flag
public class Semaphore implements Invariant {
private volatile int semaphore = 0;
public boolean available() { return semaphore == 0; }
public void acquire() { ++semaphore; }
public void release() { --semaphore; }
public InvariantState invariant() {
int val = semaphore;
if(val == 0 || val == 1)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}
} ///:~
类的核心部分很直接,包括了available( ), acquire( ), 和release( )方法。既然线程在获取资源的时候要检查其可用性,所以信号量的值一定不能是0或1以外的值,这将由invariant( )来测试。
但是请看,当测试Semaphore的线程一致性时发生的情况:
//: c13:SemaphoreTester.java
// Colliding over shared resources
public class SemaphoreTester extends Thread {
private volatile Semaphore semaphore;
public SemaphoreTester(Semaphore semaphore) {
this.semaphore = semaphore;
setDaemon(true);
start();
}
public void run() {
while(true)
if(semaphore.available()) {
yield(); // Makes it fail faster
semaphore.acquire();
yield();
semaphore.release();
yield();
}
}
public static void main(String[] args) throws Exception {
Semaphore sem = new Semaphore();
new SemaphoreTester(sem);
new SemaphoreTester(sem);
new InvariantWatcher(sem).join();
}
} ///:~
SemaphoreTester创建了一个线程,此线程不断进行测试,以检查Semaphore对象是否可用,可用的话就获取它,然后释放。注意,semaphore的字段被标记为volatile,以确保编译器不会对任何读取此值的操作进行优化。
在main( )里,建立了两个SemaphoreTester线程,你会发现很快就发生了违反约束条件的事件。其原因是,一个线程可能从对available( )的调用返回为真,但在此线程调用acquire( )的时候,另一个线程可能已经调用了acquire( )并增加了semaphore字段的值。此时InvariantWatcher可能会发现字段的值太大;如果两个线程都调用了release( )以减少字段值后就会出现负数,这时就会出现值太小的情况。注意,主线程在InvariantWatcher上调用了join( ),这将使程序一直运行,直到发生失败。
在我的机器上,我发现如果加上对yield( )的调用,会导致约束条件的违例情况出现得更快,不过这会因操作系统和JVM实现的不同而有所不同。你应该去掉对yield( )的调用,自己试验一下;失败也许要很久才会发生,这也说明了当你编写多线程代码的时候,发现程序的瑕疵是多么困难。
这个类强调了并发编程的风险:如果写这么简单的类也会出问题的话,你永远不能信任对并发编程作出的任何假设。
解决共享资源竞争
基本上所有的多线程模式,在解决线程冲突问题的时候,都是采用“序列化”(serialize)访问共享资源的方案。这意味着在给定时刻只允许一个线程访问共享资源。通常这是通过在代码前面加上一条锁语句来实现的,这就保证了在一段时间内只有一个线程运行这段代码。因为锁语句产生了一种互相排斥的效果,所以常常称为“互斥量”(mutex)。
考虑一下屋子里的浴室:多个人(即多个线程)都希望能单独使用浴室(即共享资源)。为了使用浴室,一个人先敲门,看看是否能使用。如果没人的话,他就进入浴室并且锁上
门。这时其它人要使用浴室的话,就会被“阻挡”,所以他们要在浴室门口等待,直到浴室可以使用。
当浴室使用完毕,就该把浴室给其他人使用了,这个比喻就有点不太准确了。事实上,人们并没有排队,我们也不能确定谁将是下一个使用浴室的人,因为线程调度机制并不是确定性的。实际情况是:等待使用浴室的人们簇拥在浴室门口,当锁住浴室门的那个人打开锁准备离开的时候,离门最近的那个人可能进入浴室。如前所述,可以通过yield( )和setPriority( )来给线程调度机制一些建议,但这些建议未必会有多大效果,这取决于你的具体平台和JVM实现。
Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。它的行为很像Semaphore类:当线程要执行被synchronized关键字守护的代码片断的时候,它将检查信号量是否存在,然后获取信号量,执行代码,释放信号量。不同的是,synchronized内置于语言,所以这种防护始终存在,不像Semaphore那样要明确使用才能工作。
典型的共享资源是以对象形式存在的内存片断,但也可以是文件,输入/输出端口,或者是打印机。要控制对共享资源的访问,你得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized。也就是说,一旦某个线程处于一个标记为synchronized的方法中,那么在这个线程从该方法返回之前,其它要调用类中任何标记为synchronized方法的线程都会被阻塞。
一般来说类的数据成员都被声明为私有的,只能通过方法来访问这些数据。所以你可以把方法标记为synchronized来防止资源冲突。下面是如何声明synchronized方法:
synchronized void f() { /* ... */ }
synchronized void g(){ /* ... */ }
每个对象都含有一个单一的锁(也称为监视器),这个锁本身就是对象的一部分(你不用写任何特殊代码)。当你在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时对象上的其它synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。在上个例子里,如果对对象调用了f( ),对于这个对象就只能等到f( )调用结束并释放了锁之后,才能调用g( )。所以,对于某个对象,其所有synchronized方法共享同一个锁,这能防止多个线程同时访问对象所在的内存。
一个线程可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数为0。在线程第一次给对象加锁的时候,计数变为1。每次线程在这个对象上获得了锁,计数都会增加。显然,只有首先获得了锁的线程才能允许继续获取多个锁。每当线程离开一个synchronized方法,计数减少,当计数为零的时候,锁被完全释放,此时别的线程就可以使用此资源。
针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对静态数据的并发访问。
同步控制EvenGenerator
通过在EvenGenerator.java中加入synchronized关键字,我们就可以防止不希望的线程访问:
//: c13:SynchronizedEvenGenerator.java
// Using "synchronized" to prevent thread collisions
public
class SynchronizedEvenGenerator implements Invariant {
private int i;
public synchronized void next() { i++; i++; }
public synchronized int getValue() { return i; }
// Not synchronized so it can run at
// any time and thus be a genuine test:
public InvariantState invariant() {
int val = getValue();
if(val % 2 == 0)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}
public static void main(String[] args) {
SynchronizedEvenGenerator gen =
new SynchronizedEvenGenerator();
new InvariantWatcher(gen, 4000); // 4-second timeout
while(true)
gen.next();
}
} ///:~
你可以注意到next( )和getValue( )都使用了synchronized进行修饰。要是你只同步控制其中一个方法的话,那么另一个就可以随意地忽略对象锁,从而出现不负责任的调用。关键是:每个访问关键共享资源的方法必须全部是synchronized的,否则就会出错。另一方面,InvariantState没有同步控制,因为它只是进行测试,我们希望它在任意时刻都能被调用,这样它才能真正检查对象的状态。
原子操作
在有关Java线程的讨论中,一个常被提到的认识是“原子操作不需要进行同步控制”。“原子操作”(atomic operation)即不能被线程调度机制中断的操作;一旦操作开始,那
么它一定可以在可能发生的“上下文切换”(context switch)之前(切换到其它线程执行)执行完毕。
还有一个常被提到的知识是,如果问题中的变量类型是除long或double以外的基本类型,对这种变量进行简单的赋值或者返回值操作的时候,才算是原子操作。不包括long和double的原因是因为它们比其它基本类型要大,所以JVM不能把对它的读取或赋值当成是单一原子操作(也许JVM能够这么做,但这并不能保证)。然而,你只要给long或double加上volatile,操作就是原子的了。
如果你把原子操作的概念尝试着应用到SynchronizedEvenGenerator.java,你将注意到
public synchronized int getValue() { return i; }
是符合这个定义的。但如果试着去掉synchronized,测试将会失败。因为尽管return i确实是原子操作,去掉synchronized的话,将会出现当对象还处于不稳定的中间状态的时候就被别的线程读取了。所以在试图做这样的优化之前,你必须真正知道自己在做什么。而这并没有简单可行的规则。
作为第二个例子,考虑一下更简单的情况:一个产生序列号的类3。每次调用nextSerialNumber( ) ,它必须向调用者返回一个唯一值:
//: c13:SerialNumberGenerator.java
public class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static int nextSerialNumber() {
return serialNumber++;
}
} ///:~
SerialNumberGenerator基本上与你能想到的一样简单,如果你有C++或其它低级语言的背景,你可能认为自增加操作是一个原子操作,因为它通常可以用一条微处理器指令实现。然而,在JVM中的自增加操作并不是原子的,它牵涉到一次读和一次写,所以即使在这样简单的操作中,也为线程出问题提供了空间。
serialNumber字段标记成volatile,其原因是每个线程都可能拥有一个本地栈以维护一些变量的复本。如果把一个变量定义成volatile,就等于告诉编译器不要做任何优化,这些优化可能会移除那些使字段与线程里的本地数据复本保持同步的读写操作。
要证明这一点,我们需要一个不会用完内存的集合,这时,检测到问题要花很长时间。这里的CircularSet重用了用来存储整型数组的内存,并假设在你访问数组时,覆写值时发生冲突的可能性最小。add( )和contains( )方法标记为synchronized以防止线程冲突。
3受到Joshua Bloch的 《Effective Java》的启发, Addison-Wesley 2001,第190页。
//: c13:SerialNumberChecker.java
// Operations that may seem safe are not,
// when threads are present.
// Reuses storage so we don't run out of memory:
class CircularSet {
private int[] array;
private int len;
private int index = 0;
public CircularSet(int size) {
array = new int[size];
len = size;
// Initialize to a value not produced
// by the SerialNumberGenerator:
for(int i = 0; i < size; i++)
array[i] = -1;
}
public synchronized void add(int i) {
array[index] = i;
// Wrap index and write over old elements:
index = ++index % len;
}
public synchronized boolean contains(int val) {
for(int i = 0; i < len; i++)
if(array[i] == val) return true;
return false;
}
}
public class SerialNumberChecker {
private static CircularSet serials =
new CircularSet(1000);
static class SerialChecker extends Thread {
SerialChecker() { start(); }
public void run() {
while(true) {
int serial =
SerialNumberGenerator.nextSerialNumber();
if(serials.contains(serial)) {
System.out.println("Duplicate: " + serial);
System.exit(0);
}
serials.add(serial);
}
}
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new SerialChecker();
// Stop after 4 seconds:
new Timeout(4000, "No duplicates detected");
}
} ///:~
SerialNumberChecker含有一个静态的CircularSet,后者包含了所有已经生成的序列号,以及一个获取序列号并能确保其唯一的嵌套线程。通过建立多个线程来争夺序列号,你会发现线程很快就会得到重复的序列号,(注意这个程序在你的机器上可能并不冲突,但在一台多处理器的机器上确实检查到了冲突)。要解决这个问题,就得给nextSerialNumber( )方法加上synchronized关键字。
原子操作只有在对基本类型进行读取或赋值的时候才被认为是安全的。不过,正如在EvenGenerator.java中所见,原子操作也很容易访问到对象尚出于不稳定状态时的值,所以你不能做任何假设。不仅如此,原子操作也不保证对long和double类型能工作(尽管有些JVM实现确实能保证对long和double类型操作的原子性,但如果你依赖于这一点,你的代码就失去了可移植性)。
最安全的就是使用以下方针:
1。如果你要对类中的某个方法进行同步控制,最好同步所有方法。如果你忽略了其中一个,通常很难确定这么做是否会有负面影响。
2。当去除方法的同步控制时,要非常小心。通常这么做是基于性能方面的考虑,但在JDK1.3和JDK1.4中,同步控制所需的负担已经大大减少。此外,你只应该在使用了性能评价工具证实了同步控制确实是性能瓶颈的时候,才能这么做。
修正信号量
现在考虑Semaphore.java。看起来我们能通过给三个方法加上synchronized标记,来修正这个问题,就像这样:
//: c13:SynchronizedSemaphore.java
// Colliding over shared resources
public class SynchronizedSemaphore extends Semaphore {
private volatile int semaphore = 0;
public synchronized boolean available() {
return semaphore == 0;
}
public synchronized void acquire() { ++semaphore; }
public synchronized void release() { --semaphore; }
public InvariantState invariant() {
int val = semaphore;
if(val == 0 || val == 1)
return new InvariantOK();
else
return new InvariantFailure(new Integer(val));
}
public static void main(String[] args) throws Exception {
SynchronizedSemaphore sem =new SynchronizedSemaphore();
new SemaphoreTester(sem);
new SemaphoreTester(sem);
new InvariantWatcher(sem).join();
}
} ///:~
首先,SynchronizedSemaphore类就显得很奇怪:它是从Semaphore类继承而来,且所有被重载的方法都标记为synchronized,但这些方法的基类版本却不是。Java并不允许你在重载的时候改变方法的签名,但这却没有产生出错信息。这是因为synchronized关键字不属于方法签名的一部分,所以你才能把它加进来,而它也并不局限于重载。
从Semaphore类继承的原因是为了重用SemaphoreTester类。当你运行程序的时候你会发现程序还是会产生InvariantFailure错误。
为什么会失败呢?当线程检测到Semaphore可用时,即调用available( )并且返回为真的时候,对象上的锁已经被释放。这时,另一个线程可能会冲进来,并在前一个线程增加semaphore值的时候抢先增加。同时,前一个线程还在假设Semaphore对象是可用的,所以会继续向前并盲目的进入acquire( )方法,这就使对象处于不稳定的状态。这只不过是并发编程首要规则的又一次教训而已:永远不要做任何假设。
这个问题的唯一解决方案是,把测试可用性操作和获取操作作为一个单一的原子操作,这也就是synchronized关键字与对象的锁协作所共同提供的功能。也就是说,Java的锁和synchronized关键字属于内置的信号量机制,所以你不必自己再去发明一个。
临界区
有时,你只是希望防止多个线程同时访问方法内部的部分代码而不是整个方法。通过这种方式分离出来的代码段被称为“临界区”(critical section),它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:
synchronized(syncObject) {
// This code can be accessed
// by only one thread at a time
}
这也被称为“同步控制块”(synchronized block),在进入此段代码前,必须得到syncObject对象的锁。如果其它线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
通过使用同步控制块而不是对整个方法进行同步控制,可以使多个线程访问对象的时间性能得到显著提高,下面的例子比较了这两种同步控制方法。此外,它也演示了如何把一个非保护类型的类,在其它类的保护和控制之下,应用于多线程的环境:
//: c13:CriticalSection.java
// Synchronizing blocks instead of entire methods. Also
// demonstrates protection of a non-thread-safe class
// with a thread-safe one.
import java.util.*;
class Pair { // Not thread-safe
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++; }
public String toString() {
return "x: " + x + ", y: " + y;
}
public class PairValuesNotEqualException
extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal: " + Pair.this);
}
}
// Arbitrary invariant -- both variables must be equal:
public void checkState() {
if(x != y)
throw new PairValuesNotEqualException();
}
}
// Protect a Pair inside a thread-safe class:
abstract class PairManager {
protected Pair p = new Pair();
private List storage = new ArrayList();
public synchronized Pair getPair() {
// Make a copy to keep the original safe:
return new Pair(p.getX(), p.getY());
}
protected void store() { storage.add(getPair()); }
// A "template method":
public abstract void doTask();
}
// Synchronize the entire method:
class PairManager1 extends PairManager {
public synchronized void doTask() {
p.incrementX();
p.incrementY();
store();
}
}
// Use a critical section:
class PairManager2 extends PairManager {
public void doTask() {
synchronized(this) {
p.incrementX();
p.incrementY();
}
store();
}
}
class PairManipulator extends Thread {
private PairManager pm;
private int checkCounter = 0;
private class PairChecker extends Thread {
PairChecker() { start(); }
public void run() {
while(true) {
checkCounter++;
pm.getPair().checkState();
}
}
}
public PairManipulator(PairManager pm) {
this.pm = pm;
start();
new PairChecker();
}
public void run() {
while(true) {
pm.doTask();
}
}
public String toString() {
return "Pair: " + pm.getPair() +
" checkCounter = " + checkCounter;
}
}
public class CriticalSection {
public static void main(String[] args) {
// Test the two different approaches:
final PairManipulator
pm1 = new PairManipulator(new PairManager1()),
pm2 = new PairManipulator(new PairManager2());
new Timer(true).schedule(new TimerTask() {
public void run() {
System.out.println("pm1: " + pm1);
System.out.println("pm2: " + pm2);
System.exit(0);
}
}, 500); // run() after 500 milliseconds
}
} ///:~
正如注释中注明的,Pair不是线程安全的,因为它的约束条件(虽然是任意的)需要两个变量维护成相同的值。此外,如本章前面所述,自增操作不是线程安全的,并且因为没有方法被标记为synchronized,所以你不能保证一个Pair对象在多线程程序中不会被破坏。
PairManager类持有一个Pair对象并控制对它的访问。注意唯一的public方法是getPair( ),它是被同步控制的。对于抽象方法doTask( ),它的同步控制将在实现的时候进行处理。
至于PairManager类的结构,它的一些功能已经在基类中实现,并且其一个或多个抽象方法将在派生类中定义,这种结构在“设计模式”(Design Patterns )中称为“模板方法”(Template Method )4。设计模式使你得以把变化封装在代码里;在此,发生变化的部分是模板方法doTask( )。在PairManager1中整个doTask( )方法是被同步控制的,但在PairManager2中的doTask( )方法使用同步控制块进行同步。注意到synchronized关键字不属于方法签名的一部分,所以可以在重载方法的时候加上去。
PairManager2值得注意,store( )是一个protected方法,它不能被一般客户使用,只能被其子类使用。所以,对这个方法的调用没有必要进行同步控制,而是被放在同步控制块外面。
同步控制块必须指定一个对象才能进行同步,通常,最合理的对象就是在其上调用方法的当前对象: synchronized(this),在PairManager2中采用了这种方法。这样,当为同步控制块请求锁的时候,对象的其它同步控制方法就不能被调用了。所以其效果不过是缩小了同步控制的范围。
有时这并不符合你的要求,这时你可以创建一个单独的对象,并对其进行同步控制。下面的例子演示了当对象中的方法在不同的锁上同步的时候,两个线程可以访问同一个对象:
//: c13:SyncObject.java
// Synchronizing on another object
import com.bruceeckel.simpletest.*;
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
System.out.println("Inside f()");
// Doesn't release lock:
try {
Thread.sleep(500);
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Leaving f()");
}
public void g() {
synchronized(syncObject) {
System.out.println("Inside g()");
try {
Thread.sleep(500);
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
4参见《Design Patterns》,Gamma等著,Addison-Wesley 1995。
System.out.println("Leaving g()");
}
}
}
public class SyncObject {
private static Test monitor = new Test();
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
monitor.expect(new String[] {
"Inside g()",
"Inside f()",
"Leaving g()",
"Leaving f()"
}, Test.WAIT + Test.IGNORE_ORDER);
}
} ///:~
DualSync 对象的f( )方法在this上同步(通过在整个方法上同步),g( )的同步控制块在syncObject对象上同步。因此,两个同步控制相互独立。在main( )中通过创建调用f( )的线程演示了这一点。main( )线程用来调用g( )。从输出中你能观察到两个方法同时运行,所以它们没有在对方的同步控制上阻塞。
我们回到CriticalSection.java,通过在一个线程中运行doTask( ),在另一个线程中运行内部类PairChecker的实例,创建了PairManipulator来测试两种不同类型的PairManager。要跟踪进行测试的频率,PairChecker在每次成功的时候增加checkCounter计数。在main( )中,建立了2个PairManipulator对象,并让它们运行一段时间。当Timer时间到期时,将执行run( )方法,它将显示每个PairManipulator的结果,然后退出。运行一下程序,你能看到类似下面的结果:
pm1: Pair: x: 58892, y: 58892 checkCounter = 44974
pm2: Pair: x: 73153, y: 73153 checkCounter = 100535
尽管你每次运行的结果可能会有很大不同,但一般来说,对于PairChecker的检查频率,PairManager1.doTask( )不允许有PairManager2.doTask( )那样多。后者采用同步控制块进行同步,所以对象不加锁的时间更长。这也是宁愿使用同步控制块而不是对整个方法进行同步控制的典型原因:使得其它线程能更多地访问(在安全的情况下尽可能多)。
当然,所有的同步控制都要靠程序员的勤奋工作:必须把访问共享资源的代码段包装进一个合适的同步控制块。


l 线程状态
一个线程可以处于以下四种状态之一:
1.新建(new):线程对象已经建立,但还没有启动,所以它还不能运行。
2.就绪(Runnable):在这种状态下,只要调度程序把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度程序能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。
3.死亡(Dead):线程死亡的通常方式是从run( )方法返回。在Java 2废弃stop( )以前,你也可以调用它,但这很容易让你的程序进入不稳定状态。还有一个destroy( )方法(这个方法从来没被实现过,也许以后也不会被实现,它也属于被废止的)。在本章的后面你将学习一种与调用stop( )功能等价的方式。
4.阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于阻塞状态时,调度机制将忽略线程,不会分配给线程任何处理器时间。直到线程重新进入了就绪状态,它才有可能执行操作。
进入阻塞状态
当一个线程被阻塞时,必然存在某种原因使其不能继续运行。一个线程进入阻塞状态,可能有如下原因:
1.你通过调用sleep(milliseconds)使线程进入休眠状态,在这种情况下,线程在指定的时间内不会运行。
2.你通过调用wait( )使线程挂起。直到线程得到了notify( )或notifyAll( )消息,线程才会进入就绪状态。我们将在下一节验证这一点。
3.线程在等待某个输入/输出完成。
4.线程试图在某个对象上调用其同步控制方法,但是对象锁不可用。
在较早的代码中,你也可能看到suspend( )和resume( )用来阻塞和唤醒线程,但是在Java 2中这些方法被废止了(因为可能导致死锁),所以本书不讨论这些内容。
线程之间协作
在理解了线程之间可能存在相互冲突,以及怎样避免冲突之后,下一步就是学习怎样使线程之间相互协作。这种协作关键是通过线程之间的握手来进行的,这种握手可以通过Object的方法wait( )和notify( )来安全的实现。
等待与通知
调用sleep( )的时候锁并没有被释放,理解这一点很重要。另一方面,wait( )方法的确释放了锁,这就意味着在调用wait( )期间,可以调用线程中对象的其他同步控制方法。当一个线程在方法里遇到了对wait( )的调用的时候,线程的执行被挂起,对象上的锁被释放。
有两种形式的wait( )。第一种接受毫秒作为参数,意思与sleep( )方法里参数的意思相同,都是指“在此期间暂停” 。不同之处在于,对于wait( ):
1.在wait( )期间锁是释放的。
2.你可以通过notify( )、notifyAll( ),或者时间到期,从wait( )中恢复执行。
第二种形式的wait( )不要参数;这种用法更常见。wait( )将无限等待直到线程接收到notify( )或者notifyAll( )消息。
wait( ), notify( ),以及notifyAll( )的一个比较特殊的方面是这些方法是基类Object的一部分,而不是像Sleep( )那样属于Thread的一部分。尽管开始看起来有点奇怪,仅仅针对线程的功能却作为通用基类的一部分而实现,不过这是有道理的,因为这些功能要用到的锁也是所有对象的一部分。所以,你可以把wait( )放进任何同步控制方法里,而不用考虑这个类是继承自Thread还是实现了Runnable接口。实际上,你只能在同步控制方法或同步控制块里调用wait( ), notify( )和notifyAll( )(因为不用操作锁,所以sleep( )可以在非同步控制方法里调用)。如果你在非同步控制方法里调用这些方法,程序能通过编译,但运行的时候,你将得到IllegalMonitorStateException异常,伴随着一些含糊的消息,比如“当前线程不是拥有者”。消息的意思是,调用 wait( ), notify( )和notifyAll( )的线程在调用这些方法前必须“拥有”(获取)对象的锁。
你能够让另一个对象执行这种操作以维护其自己的锁。要这么做的话,你必须首先得到对象的锁。比如,如果你要在对象x上调用notify( ),那么你就必须在能够取得x的锁的同步控制块中这么做:
synchronized(x) {
x.notify();
}
特别地,当你在等待某个条件,这个条件必须由当前方法以外的因素才能改变的时候(典型地,这个条件被另一个线程所改变),就应该使用wait( )。你也不希望在线程里测试条件的时候空等;这也称为“忙等”,它会极大占用CPU时间。所以wait( )允许你在等待外部条件的时候,让线程休眠,只有在收到notify( )或notifyAll( )的时候线程才唤醒并对变化进行检查。所以,wait( )为在线程之间进行同步控制提供了一种方法。
例如,考虑一个餐馆,有一个厨师和一个服务员。服务员必须等待厨师准备好食物。当厨师准备好食物的时候,他通知服务员,后者将得到食物然后继续等待。这是一个线程协作的极好的例子:厨师代表了生产者,服务员代表了消费者。下面是模拟这个场景的代码:
//: c13:Restaurant.java
// The producer-consumer approach to thread cooperation.
import com.bruceeckel.simpletest.*;
class Order {
private static int i = 0;
private int count = i++;
public Order() {
if(count == 10) {
System.out.println("Out of food, closing");
System.exit(0);
}
}
public String toString() { return "Order " + count; }
}
class WaitPerson extends Thread {
private Restaurant restaurant;
public WaitPerson(Restaurant r) {
restaurant = r;
start();
}
public void run() {
while(true) {
while(restaurant.order == null)
synchronized(this) {
try {
wait();
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(
"Waitperson got " + restaurant.order);
restaurant.order = null;
}
}
}
class Chef extends Thread {
private Restaurant restaurant;
private WaitPerson waitPerson;
public Chef(Restaurant r, WaitPerson w) {
restaurant = r;
waitPerson = w;
start();
}
public void run() {
while(true) {
if(restaurant.order == null) {
restaurant.order = new Order();
System.out.print("Order up! ");
synchronized(waitPerson) {
waitPerson.notify();
}
}
try {
sleep(100);
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Restaurant {
private static Test monitor = new Test();
Order order; // Package access
public static void main(String[] args) {
Restaurant restaurant = new Restaurant();
WaitPerson waitPerson = new WaitPerson(restaurant);
Chef chef = new Chef(restaurant, waitPerson);
monitor.expect(new String[] {
"Order up! Waitperson got Order 0",
"Order up! Waitperson got Order 1",
"Order up! Waitperson got Order 2",
"Order up! Waitperson got Order 3",
"Order up! Waitperson got Order 4",
"Order up! Waitperson got Order 5",
"Order up! Waitperson got Order 6",
"Order up! Waitperson got Order 7",
"Order up! Waitperson got Order 8",
"Order up! Waitperson got Order 9",
"Out of food, closing"
}, Test.WAIT);
}
} ///:~
Order是一个简单的能自己计数的类,但要注意它也包含了终止程序的方法;当订单数累计到10时,将调用System.exit( )。
WaitPerson(服务员)必须知道自己所工作的Restaurant(餐馆),因为他们必须从餐馆的“订单窗口”取出订单restaurant.order。在run( )中,WaitPerson调用wait( )进入等待模式,停止线程的执行直到被Chef(厨师)的notify( )方法所唤醒。因为是很简单的程序,我们知道只有一个线程在等待WaitPerson对象的锁:即WaitPerson线程自己。正因为这个原因,使得调用notify( )是安全的。在更复杂的情况下,多个线程可能在等待同一个特定的锁,所以你不知道哪个线程被唤醒。解决方法是调用notifyAll( ),它将唤醒所有等待这个锁的线程。每个线程必须自己决定是否对这个通知作出反应。
注意对wait( )的调用被包装在一个while( )语句里,它在测试的正是等待的条件。开始看起来可能很奇怪--如果你在等一个订单,那么一旦你被唤醒,订单必须是可用的,对不对?问题是在多线程程序里,一些别的线程可能在WaitPerson苏醒的同时冲进来抢走订单。唯一安全的方法就是对于wait( )总是使用如下方式:
while(conditionIsNotMet) wait( );
这可以保证在你跳出等待循环之前条件将被满足,如果你被不相干的条件所通知(比如notifyAll( )),或者在你完全退出循环之前条件已经被改变,你被确保可以回来继续等待。
一个Chef对象必须知道他/她工作的餐馆(这样可以通过restaurant.order下订单)和取走食物的WaitPerson,这样才能在订单准备好的时候通知WaitPerson。在这个简化过的例子中,由Chef产生订单对象,然后通知WaitPerson订单已经准备好了。
请注意对notify( )的调用必须首先获取WaitPerson对象的锁。WaitPerson.run( )里对wait( )的调用将自动释放这个锁,所以这是可能的。因为要调用notify( )必须获取锁,这就能保证如果两个线程试图在同一个对象上调用notify( )时不会互相冲突。
上面的例子中,一个线程只有一个单一的地点来存储某个对象,这样另一个线程就可以在以后使用这个对象。然而,在一个典型的生产者-消费者实现中,你要使用先进先出的队列来存放被生产和消费的对象。要对这个问题做更多的了解,请参阅本章后面的练习。
线程间使用管道进行输入/输出
通过输入/输出在线程间进行通信通常很有用。线程库以“管道”(pipes)的形式对线程间的输入/输出提供了支持。它们在Java输入/输出库中的对应物就是PipedWriter类(允许线程向管道写)和PipedReader类(允许不同线程从同一个管道中读取)。这个模型可以看成是生产者-消费者问题的变体,这里的管道就是一个封装好的解决方案。
下面是一个简单例子,两个线程使用一个管道进行通信:
//: c13:PipedIO.java
// Using pipes for inter-thread I/O
import java.io.*;
import java.util.*;
class Sender extends Thread {
private Random rand = new Random();
private PipedWriter out = new PipedWriter();
public PipedWriter getPipedWriter() { return out; }
public void run() {
while(true) {
for(char c = 'A'; c <= 'z'; c++) {
try {
out.write(c);
sleep(rand.nextInt(500));
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
}
}
class Receiver extends Thread {
private PipedReader in;
public Receiver(Sender sender) throws IOException {
in = new PipedReader(sender.getPipedWriter());
}
public void run() {
try {
while(true) {
// Blocks until characters are there:
System.out.println("Read: " + (char)in.read());
}
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}
public class PipedIO {
public static void main(String[] args) throws Exception {
Sender sender = new Sender();
Receiver receiver = new Receiver(sender);
sender.start();
receiver.start();
new Timeout(4000, "Terminated");
}
} ///:~
Sender和Receiver代表了两个线程,它们执行某些任务并且需要互相通信。Sender创建了一个PipedWriter,它是一个单独的对象,但是对于Receiver,PipedReader的建立必须在构造器中与一个PipedWriter相关联。Sender把数据放进Writer然后休眠一段随机的时间。然而,Receiver没有调用sleep( )和wait( )。但当它调用read( )时,如果没有数据,它将自动阻塞。这样你不用使用wait( )循环,就得到了生产者-消费者的效果。
注意到sender和receiver是在main( )中启动的,即对象构造完毕以后。如果你启动了一个没有构造完毕的对象,在不同的平台上管道可能会产生不一致的行为。
更高级的协作
本章只介绍了最基本的协作方式(比如生产者―消费者,通常用wait( ),notify( )和notifyAll( )实现)。这些能解决大多数线程协作的问题,不过还有很多更高级的方法,它们在一些更深入的书籍中有介绍(尤其是在本章末尾介绍的Lea的著作)。
死锁
因为线程可以阻塞,并且对象可以具有同步控制方法,用以防止别的线程在锁还没有释放的时候就访问这个对象。所以就可能出现这种情况:某个线程在等待另一个线程,而后者又等待别的线程,这样一直下去,直到这个链条上的线程又在等待第一个线程释放锁。你将得到一个线程之间相互等待的连续循环,没有哪个线程能继续。这被称之为“死锁”(deadlock)。
如果你运行一个程序,而它马上就死锁了,你当时就能知道出了问题,并且可以跟踪下去。真正的问题在于,你的程序可能看起来工作良好,但是具有潜在的死锁危险。这时,死锁可能发生,而事先却没有任何征兆,所以它会潜伏在你的程序里,直到客户发现它出乎意料地发生(并且你可能很难重现这个问题)。因此,在编写并发程序的时候,进行仔细的程序设计以防止死锁是一个关键部分。
让我们看一下经典的死锁现象,它是由Dijkstra提出的:哲学家就餐问题。其内容是指定五个哲学家(不过这里的例子中将允许任意数目)。这些哲学家将用部分的时间思考,部分的时间就餐。当他们思考的时候,不需要任何共享资源,但当他们就餐的时候,他们坐在桌子旁并且只有有限数量的餐具。在问题的原始描述中,餐具是叉子,要吃到桌子中央盘子里的意大利面条需要用两把叉子,不过把餐具看成是筷子更合理;很明显,哲学家要就餐就需要两根筷子。
问题中引入的难点是:作为哲学家,他们很穷,所以他们只能买的起五根筷子。他们围坐在桌子周围,每人之间放一根筷子。当一个哲学家要就餐的时候,他/她必须同时得到左边和右边的筷子。如果一个哲学家左边或右边已经有人在使用筷子了,那么这个哲学家就必须等待。
注意,这个问题之所以有趣,其原因是它显示了一个程序可能看起来运行正确,但确实存在死锁的危险。要演示这一点,命令行参数允许你调整哲学家的数量以及一个影响哲学家思考时间的因子。如果你有许多哲学家,并且/或者他们花很多时间去思考,那么尽管存在死锁可能,你可能永远也看不到死锁。默认的命令行参数是倾向于使死锁尽快发生:
//: c13:DiningPhilosophers.java
// Demonstrates how deadlock can be hidden in a program.
// {Args: 5 0 deadlock 4}
import java.util.*;
class Chopstick {
private static int counter = 0;
private int number = counter++;
public String toString() {
return "Chopstick " + number;
}
}
class Philosopher extends Thread {
private static Random rand = new Random();
private static int counter = 0;
private int number = counter++;
private Chopstick leftChopstick;
private Chopstick rightChopstick;
static int ponder = 0; // Package access
public Philosopher(Chopstick left, Chopstick right) {
leftChopstick = left;
rightChopstick = right;
start();
}
public void think() {
System.out.println(this + " thinking");
if(ponder > 0)
try {
sleep(rand.nextInt(ponder));
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
public void eat() {
synchronized(leftChopstick) {
System.out.println(this + " has "
+ this.leftChopstick + " Waiting for "
+ this.rightChopstick);
synchronized(rightChopstick) {
System.out.println(this + " eating");
}
}
}
public String toString() {
return "Philosopher " + number;
}
public void run() {
while(true) {
think();
eat();
}
}
}
public class DiningPhilosophers {
public static void main(String[] args) {
if(args.length < 3) {
System.err.println("usage:/n" +
"java DiningPhilosophers numberOfPhilosophers " +
"ponderFactor deadlock timeout/n" +
"A nonzero ponderFactor will generate a random " +
"sleep time during think()./n" +
"If deadlock is not the string " +
"'deadlock', the program will not deadlock./n" +
"A nonzero timeout will stop the program after " +
"that number of seconds.");
System.exit(1);
}
Philosopher[] philosopher =
new Philosopher[Integer.parseInt(args[0])];
Philosopher.ponder = Integer.parseInt(args[1]);
Chopstick
left = new Chopstick(),
right = new Chopstick(),
first = left;
int i = 0;
while(i < philosopher.length - 1) {
philosopher[i++] =
new Philosopher(left, right);
left = right;
right = new Chopstick();
}
if(args[2].equals("deadlock"))
philosopher[i] = new Philosopher(left, first);
else // Swapping values prevents deadlock:
philosopher[i] = new Philosopher(first, left);
// Optionally break out of program:
if(args.length >= 4) {
int delay = Integer.parseInt(args[3]);
if(delay != 0)
new Timeout(delay * 1000, "Timed out");
}
}
} ///:~
Chopstick和Philosopher都使用一个自动增加的静态计数器,来给每个元素一个标识数字。每个Philosopher对象都有一个对左边和右边Chopstick对象的引用;这些是哲学家进餐之前必须得到的餐具。
静态字段ponder表示哲学家是否花费时间进行思考。如果值是非零的,那么在think( )方法里线程将休眠一段由这个值随机产生的时间。通过这种方法,就可以展示,线程(哲
学家)在其它任务(思考)上花费的时越多,那么它们在请求共享资源(筷子)的时候发生冲突的可能性就越低,这样你就能确信程序没有死锁,尽管事实上可能是有的。
在eat( )里,哲学家通过同步控制获取左边的筷子。如果筷子不可用,那么哲学家将等待(阻塞)。在获取了左边的筷子之后,再用同样的方法获取右边的筷子。在就餐完毕之后,先释放右边的筷子,然后释放左边的筷子。
在run( )中,每个哲学家不断重复思考和就餐的动作。
main( )方法至少需要三个参数,如果不满足的话,就打印一条使用信息。第三个参数可以是字符串“deadlock”,在这种情况下,使用会发生死锁的程序。如果是其它字符串就使用不产生死锁的程序。最后一个参数(可选的)是一个到期因子,表示一段时间之后将退出程序(无论是否发生了死锁)。作为本书所附代码的测试程序的一部分,并且是自动运行的程序,设置一个过期时间是很有必要的。
在创建了Philosopher数组并且设置了ponder值之后,建立了两个Chopstick对象,并且将书组的第一个元素存储在first变量里供以后使用。除最后一个元素之外,数组里的每个引用都是通过创建一个新的Philosopher对象,并传递给它左筷子和右筷子对象来进行初始化的。在每次初始化之后,左边的筷子放到右边,右边的给一个新的Chopstick对象,供下一个Philosopher使用。
在死锁版本的程序里,最后一个Philosopher被给予了左边的筷子和前面存储的第一个筷子。这是因为最后一个哲学家就坐在第一个哲学家的旁边,他们共享第一根筷子。在这种安排下,在某个时间点上就可能出现,所有的哲学家都准备就餐,并且在等待旁边的哲学家放下他们手中的筷子,此时程序死锁。
试着用不同的命令行参数运行程序,以观察程序的行为,尤其要注意那些使程序看起来可以正常运行,没有死锁的方式。
要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:
1.互斥条件:线程使用的资源中至少有一个是不能共享的。这里,一根筷子一次就只能被一个哲学家使用。
2.至少有一个进程持有一个资源,并且它在等待获取一个当前被别的进程持有的资源。也就是说,要发生死锁,哲学家必须拿着一根筷子并且等待另一根。
3.资源不能被进程抢占。所有的进程必须把资源释放作为普通事件。哲学家很有礼貌,他们不会从其他哲学家那里抢筷子。
4.必须有循环等待,这时,一个进程等待其它进程持有的资源,后者又在等待另一个进程持有的资源,这样一直下去,直到有一个进程在等待第一个进程持有的资源,使得大家都被锁住。在这里,因为每个哲学家都试图先得到左边的筷子,然后得到右边的筷子,所以发生了循环等待。在上面的例子中,针对最后一个哲学家,通过交换构造器中的初始化顺序,打破了死锁条件,使得最后一个哲学家
先拿右边的筷子,后拿左边的筷子。
因为要发生死锁的话,所有这些条件必须全部满足,所以你要防止死锁的话,只需破坏其中一个即可。在程序中,防止死锁最容易的方法是破坏条件4。有这个条件的原因是每个哲学家都试图用特定的顺序拿筷子:先左后右。正因为如此,就可能会发生“每个人都拿着左边的筷子,并等待右边的筷子”的情况,这就是循环等待条件。然而,如果最后一个哲学家被初始化成先拿右边的筷子,后拿左边的筷子,那么这个哲学家将永远不会阻止其左边的哲学家拿起他/她右边的筷子,这就打破了循环等待。这只是问题的解决方法之一,你也可以通过破坏其它条件来防止死锁(具体细节请参考更高级的线程书籍)。
Java对死锁并没有提供语言层面上的支持;能否通过小心的程序设计以避免死锁,取决于你自己。对于正在试图调试一个有死锁的程序的程序员来说,没有什么更好消息。
正确的停止方法
Java 2所引入的能够减小死锁可能性的变化就是废弃了Thread类的stop( ),suspend( )和resume( )方法。
废弃stop( )方法的原因是它不释放线程获得的锁,如果对象处于不一致的状态(受损状态),其它线程就可能读取和修改这个状态。导致产生的后果可能非常微妙,并难以检测。如果不使用stop( ),你就要使用一个标志告诉线程什么时候通过从run( )方法返回以终止自己。下面是个简单的例子:
//: c13:Stopping.java
// The safe way to stop a thread.
import java.util.*;
class CanStop extends Thread {
// Must be volatile:
private volatile boolean stop = false;
private int counter = 0;
public void run() {
while(!stop && counter < 10000) {
System.out.println(counter++);
}
if(stop)
System.out.println("Detected stop");
}
public void requestStop() { stop = true; }
}
public class Stopping {
public static void main(String[] args) {
final CanStop stoppable = new CanStop();
stoppable.start();
new Timer(true).schedule(new TimerTask() {
public void run() {
System.out.println("Requesting stop");
stoppable.requestStop();
}
}, 500); // run() after 500 milliseconds
}
} ///:~
Stop标志必须是volatile的,所以run( )方法肯定能看到它(否则的话,这个值可能在本地有缓存)。这个线程的任务是打印10000个数字,所以当counter >= 10000或某人请求停止的时候,线程终止。注意requestStop( )没有进行同步控制,因为stop是布尔型(把它设为”真”是一个原子操作)并且有volatile标志。
在main( )中,启动了CanStop线程,然后设置了一个Timer,用来在半秒后调用requestStop( )。Timer的构造器传入了一个参数true,这使其成为一个后台线程,使得它不会阻碍程序的终止。
中断阻塞线程
有时线程会阻塞,比如在等待输入的时候,并且它也不能像前面的例子那样轮询标志。这时,你可以使用Thread.interrupt( )方法来跳出阻塞代码:
//: c13:Interrupt.java
// Using interrupt() to break out of a blocked thread.
import java.util.*;
class Blocked extends Thread {
public Blocked() {
System.out.println("Starting Blocked");
start();
}
public void run() {
try {
synchronized(this) {
wait(); // Blocks
}
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("Exiting run()");
}
}
public class Interrupt {
static Blocked blocked = new Blocked();
public static void main(String[] args) {
new Timer(true).schedule(new TimerTask() {
public void run() {
System.out.println("Preparing to interrupt");
blocked.interrupt();
blocked = null; // to release it
}
}, 2000); // run() after 2000 milliseconds
}
} ///:~
Blocked.run( )中的wait( )产生了阻塞线程。当Timer到期时,调用了对象的Interrupt( )方法。然后blocked的引用被设为null,这样垃圾回收器就可以清理这个对象了(虽然在这里微不足道,但对于长期运行的程序就很重要了)。
线程组
线程组持有一个线程集合。线程组的价值可以引用Joshua Bloch5 (他是Sun公司的软件架构师,曾经修正并大大改良了JDK1.2中的集合类库)的话来概括:
“最好把线程组看成是一次不成功的尝试,你只要忽略它就好了。”
如果你已经花费了时间和精力并试图领会线程组的价值(我已经这么做过了),你可能会奇怪为什么在这个主题上Sun公司没有更多的官方声明,在此之前,多年来发生在Java身上的其它任何改变也存在着同样的问题。诺贝尔经济学奖获得者Joseph Stiglitz的生活哲学看起来可以应用到这里6。它被称为“承诺升级理论”(The Theory of Escalating Commitment):
“继续错误的后果由别人承担,承认错误的后果要由自己承担。”
线程组还是存在着些许用处的。如果线程组里的线程抛出了一个未捕获的异常,ThreadGroup.uncaughtException( )将被调用,它将向标准错误流打印栈轨迹。如果要修改这个行为,你得重载这个方法。
5 《Effective Java》,Joshua Bloch著, Addison-Wesley 2001第211页。
6 在贯穿于Java的使用经验中,许多地方都存在类似问题。嗯,为什么不那么做呢?我已经咨询过许多采用了这个理念的项目。
总结
明白什么时候应该使用并发,什么时候应该避免使用并发是非常关键的。使用它的原因主要是:要处理很多任务,它们交织在一起,能够更有效地使用计算机(包括在多个处理器上透明地分配任务的能力),能够更好地组织代码,或者更便于用户使用。资源均衡的经典案例是在等待输入/输出时使用处理器。使用户方便的经典案例是在长时间的下载过程中监视“停止”按钮是否被按下。
线程的一个额外好处是它们提供了轻量级的执行上下文切换(大约100条指令),而不是重量级的进程上下文切换(要上千条指令)。因为一个给定进程内的所有线程共享相同的内存空间,轻量级的上下文切换只是改变了程序的执行序列和局部变量。进程切换(重量级的上下文切换)必须改变所有内存空间。
多线程的主要缺陷有:
1.等待共享资源的时候性能降低。
2.需要处理线程的额外CPU耗费。
3.糟糕的程序设计导致不必要的复杂度。
4.有可能产生一些病态行为,如饿死﹑竞争﹑死锁和活锁。
5.不同平台导致的不一致性。比如,当我在编写书中的一些例子时,发现竞争条件在某些机器上很快出现,但在别的机器上根本不出现。如果你在后一种机器上做开发,那么当你发布程序的时候就要大吃一惊了。
因为多个线程可能共享一个资源,比如内存中的对象,而且你必须确定多个线程不会同时读取和改变这个资源,这就是线程产生的最大难题。这需要明智的使用synchronized关键字,它仅仅是个工具,同时它会引入潜在的死锁条件,所以要对它有透彻的理解。
此外,线程应用上也有一些特定技巧。Java 被设计用来让你建立足够多的对象来解决你的问题,至少理论上是如此。(比如,为工程上的有限元分析而创建几百万个对象,在Java中并不可行。)然而,你要创建的线程数目看起来还是有个上界,因为达到了一定数量之后,线程性能会很差。这个临界点很难检测,通常依赖于操作系统和JVM;它可以是少于一百,也可能是几千。不过通常我们只是创建少数线程来解决问题,所以这个限制并不严重;尽管对于更一般的设计来说,这可能会是一个约束。
线程导致的一个明显违反直觉的结果是:线程是可调度的,在run( )的主循环中加入对yield( )甚至是sleep( )的调用,可以使程序运行得更“快”。这绝对使问题看起来更具有技巧性,特别是更长的延迟似乎可以导致性能提升。其原因是,更短的延迟导致休眠结束,调度程序在当前线程准备好休眠之前发生中断,这就迫使调度程序暂停线程,过一段时间后重新启动线程,此时线程才能完成工作然后休眠。多余的上下文切换将导致性能
降低,而使用yield( )或sleep( )能防止这种多余的切换。要搞清楚这个问题会变得多么复杂,就要花额外的时间进行思考。 线程是进程中一个任务控制流序列,由于进程的创建和销毁需要销毁大量的资源,而多个线程之间可以共享进程数据,因此多线程是并发编程的基础。
多核心CPU可以真正实现多个任务并行执行,单核心CPU程序其实不是真正的并行运行,而是通过时间片切换来执行,由于时间片切换频繁,使用者感觉程序是在并行运行。单核心CPU中通过时间片切换执行多线程任务时,虽然需要保存线程上下文,但是由于不会被阻塞的线程所阻塞,因此相比单任务还是大大提高了程序运行效率。
1.线程的状态和切换:
线程的7种状态及其切换图如下:

2.多线程简单线程例子:
Java中实现多线程常用两种方法是:实现Runnable接口和继承Thread类。
(1).继承Thread类的多线程例子如下:
class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }
//重写Thread类的run方法
         public void run() {
              . . .
         }
     }
启动继承Thread类线程的方法:
PrimeThread p = new PrimeThread(143);
p.start();
(2).实现Runnable接口的多线程例子如下:
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
. . .
}
}
启动实现Runnable接口线程的方法:
PrimeThread p = new Thread(new PrimeThread(143));
p.start();
由于java的单继承特性和面向接口编程的原则,建议使用实现Runnable接口的方式实现java的多线程。
3.使用Executors线程池:
在JDK5中,在java.util.concurrent包中引入了Executors线程池,使得创建多线程更加方便高效,例子如下:
import java.util.concurrent.*;

public class CachedThreadPool{
public static void main(String[] args){
//创建一个缓冲线程池服务
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0; i<5; i++){
//线程池服务启动线程
exec.execute(
new Runnable(){
//使用匿名内部类实现的java线程
public void run(){
System.out.println(“Thread ” + i + “ is running”);
}
}
);
//线程池服务停止
exec.shoutdown();
}
}
}
Executors.newCachedThreadPool()方法创建缓冲线程池,即在程序运行时创建尽可能多需要的线程,之后停止创建新的线程,转而通过循环利用已经创建的线程。Executors.newFixedThreadPool(intsize)方法创建固定数目的线程池,即程序会创建指定数量的线程。缓冲线程池效率和性能高,推荐优先考虑使用。
Executors.newSingleThreadPool()创建单线程池,即固定数目为1的线程池,一般用于长时间存活的单任务,例如网络socket连接等,如果有多一个任务需要执行,则会放进队列中顺序执行。
4.获取线程的返回值:
实现Runnable接口的线程没有返回值,如果想获取线程的返回值,需要实现Callable接口,Callable是JDK5中引入的现实线程的接口,其call()方法代替Runnable接口的run方法,可以获取线程的返回值,例子如下:
import java.util.concurrent.*;
import java.util.*;

class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
public String call(){
return “result of TaskWithResult ” + id;
}
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> results = new ArrayList<Future<String>>();
for(int i=0; i<5; i++){
//将线程返回值添加到List中
results.add(exec.submit(new TaskWithResult(i)));
}
//遍历获取线程返回值
for(Future<String> fs : results){
try{
System.out.println(fs.get());
}catch(Exception e){
System.out.println(e);
}finally{
exec.shutdown();
}
}
}
}
输出结果(可能的结果,由于多线程执行顺序不确定,结果不固定):
result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
注解:使用线程池服务的submit()方法执行线程池时,会产生Future<T>对象,其参数类型是线程Callable的call()方法返回值的类型,使用Future对象的get()方法可以获取线程返回值。
5.线程休眠:
在jdk5之前,使用Thread.sleep(1000)方法可以使线程休眠1秒钟,在jdk之后,使用下面的方法使线程休眠:
TimeUnit.SECONDS.sleep(1);
线程休眠的方法是TimeUnit枚举类型中的方法。
注意:不论是Thread.sleep还是TimeUnit的sleep线程休眠方法,都要捕获InterruptedExecutors。
6.线程优先级:
线程的优先级是指线程被线程调度器调度执行的优先级顺序,优先级越高表示获取CPU允许时间的概率越大,但是并不是绝对的,因为线程调度器调度线程是不可控制的,只是一个可能性的问题。可以通过Thread线程对象的getPriority()方法获取线程的优先级,可以通过线程对象的setPriority()方法设置线程的优先级。
Java的线程优先级总共有10级,最低优先级为1,最高为10,Windows的线程优先级总共有7级并且不固定,而Sun的Soloaris操作系统有23级,因此java的线程优先级无法很好地和操作系统线程优先级映射,所有一般只使用MAX_PRIORITY(10),NORM_PRIORITY(5)和MIN_PRIORITY(1)这三个线程优先级。
7.守护线程:
守护线程(DaemonThread)是某些提供通用服务的在后台运行的程序,是优先级最低的线程。当所有的非守护线程执行结束后,程序会结束所有的守护线程而终止运行。如果当前还有非守护线程的线程在运行,则程序不会终止,而是等待其执行完成。守护进程的例子如下:
import java.util.concurrent.*;

public class SimpleDaemons implements Runnable{
public void run{
try{
System.out.println(“Start daemons”);
TimeUtil.SECONDS.sleep(1);
}catch(Exception e){
System.out.println(“sleep() interrupted”);
}finally{
System.out.println(“Finally is running”);

}
}
public static void main(String[] args) throws Exception{
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);
daemon.start();
}
}
输出结果:
Start daemons
Finally没有执行,如果注释掉daemon.setDaemon(true)设置守护进程这一句代码。
输出结果:
Start daemons
Finally is running
之所以产生这样的结果原因是,main()是这个程序中唯一的非守护线程,当没有非守护线程在运行时,JVM强制推出终止守护线程的运行。
通过Thread对象的setDaemon方法可以设置线程是否为守护线程,通过isDaemon方法可以判断线程对象是否为守护线程。
由守护线程创建的线程对象不论有没有通过setDaemon方法显式设置,都是守护线程。
8.synchronized线程同步:
编程中的共享资源问题会引起多线程的竞争,为了确保同一时刻只有一个线程独占共享资源,需要使用线程同步机制,即使用前对共享资源加锁,使用完毕之后释放锁。
Java中通过synchronized关键字实现多线程的同步,线程同步可以分为以下几种:
(1).对象方法同步:
public synchronized void methodA(){    
        System.out.println(this);    
    } 
每个对象有一个线程同步锁与之关联,同一个对象的不同线程在同一时刻只能有一个线程调用methodA方法。
(2).类所有对象方法同步:
public synchronized static void methodB(){    
        System.out.println(this);    
    } 
静态方法的线程同步锁对类的所有对象都起作用,即所有对象的线程在同一时刻只能有一个类的一个线程调用该方法。
(3).对象同步代码块:
public void methodC(){ 
        synchronized(this){ 
            System.out.println(this); 
        } 
    } 
使用当前对象作为线程同步锁,同一个对象的不同线程在同一时刻只能有一个线程调用methodC方法中的代码块。
(4).类同步代码块:
public void methodD(){ 
        synchronized(className.class){ 
            System.out.println(this); 
        } 
    } 
使用类字节码对象作为线程同步锁,类所有对象的所有线程在同一时刻只能有一个类的一个线程调用methodD的同步代码块。
注意:线程的同步是针对对象的,不论是同步方法还是同步代码块,都锁定的是对象,而非方法或代码块本身。每个对象只能有一个线程同步锁与之关联。
如果一个对象有多个线程同步方法,只要一个线程访问了其中的一个线程同步方法,其它线程就不能同时访问这个对象中任何一个线程同步方法。
9.线程锁:
JDK5之后,在java.util.concurrent.locks包中引入了线程锁机制,编程中可以显式锁定确保线程间同步,例子如下:
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class Locking{
//创建锁
private ReentrantLock lock = new ReentrantLock();
public void untimed(){
boolean captured = lock.tryLock();
try{
System.out.println(“tryLock(): ” + captured);
}finally{
if(captured){
lock.unlock();
}
}
}
public void timed(){
boolean captured = false;
try{
//对象锁定两秒钟
captured = lock.tryLock(2, TimeUnit.SECONDS);
}catch(InterruptedException e){
Throw new RuntimeException(e);
}try{
System.out.println(“tryLock(2, TimeUnit.SECONDS): ” + captured);
}finally{
if(captured){
lock.unlock();
}
}
}
public static void main(String[] args){
//主线程
final Locking al = new Locking();
al.untimed();
al.timed();
//创建新线程
new Thread(){
{//动态代码块,对象创建时执行
setDaemon(true);//当前线程设置为守护线程
}
public void run(){
//获取al对象的线程锁并锁定al对象
al.lock.lock();
    System.out.println(“acquired”);
}
}.start();
//主线程让出CPU
Thread.yield();
al.untimed();
al.timed();
}
}
输出结果:
tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
acquired
tryLock(): false
tryLock(2, TimeUnit.SECONDS): false
由于创建的守护线程锁定对象之后没有释放锁,所有主线程再也无法获取对象锁。
ReentrantLock可以通过lock()锁定对象,也可通过tryLock()方法来锁定对象,对于显式使用线程锁的方法体或代码块必须放在try-catch-finally块中,必须在finally中释放对象锁。
ReentrantLock和Synchronized功能是类似的,区别在于:
(1). Synchronized代码简单,只需要一行代码即可,不用try-catch-finally捕获异常,同时不用显式释放对象锁。
(2).ReentrantLock可以控制锁的锁定和释放状态,也可以指定锁定时间等。
10.volatile传播性:
volatile关键字确保变量的跨程序可见性,使用volatile声明的字段,只要发生了写改动,所有读取该字段的值都会跟着改变,即使使用了本地缓存仍然会被改变。
volatile字段会立即写入主内存中,所有的读取值都是从主内存中读取。
volatile关键字告诉编译器移除线程中的读写缓存,直接从内存中读写。volatile适用的情况:
字段被多个任务同时访问,至少有一个任务是写操作。
volatile字段的读写操作实现了线程同步。

.java中的原子操作类:
原子操作是指程序编译后对应于一条CPU操作指令,即原子操作是最小的不可再分指令集,编程中的原子操作是线程安全的,不需要使用进行线程同步和加锁机制来确保原子操作的线程同步。
JDK5中引入了java.util.concurrent.atomic原子操作类包,提供了常用的原子类型操作数据类型,如:AtomicInteger,AtomicLong, AtomicReference等等,原子操作类例子如下:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;

public class AtomicIntegerTest implement Runnable{
//创建一个值为0的Integer类型原子类
private AtomicInteger i = new AtomicInteger(0);
public int getValue(){
//获取Integer类型原子类的值
return i.get();
}
private void evenIncrement(){
//值增加2
i.addAndGet(2);
}
public void run(){
while(true){
evenIncrement();
}
}
public static void main(String[] args){
//创建一个定时任务,5秒钟终止运行
new Timer().schedule(new TimerTask(){
public void run(){
System.err.println(“Aborting”);
System.exit(0);
}
}, 5000);
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest at = new AtomicIntegerTest();
Exec.execute(at);
while(true){
int val = at.getValue();
//奇数
if(val % 2 != 0){
System.out.println(val);
System.exit(0);
}
}
}
}
Java中加减运算等不是原子操作,为了确保线程安全必须确保线程同步安全,使用整数类型的原子类之后,整数原子类的加减运算是原子操作,因此evenIncrement()方法不再需要synchronized关键字或者线程锁机制来确保线程安全。
注意:原子类不是Integer等通用类的通用替换方法,原子类不顶用诸如hashCode和compareTo之类的方法,因为源自变量是可变的,所以不是hash表键的好的选择。
2.线程局部变量:
防止多个线程对同一个共享的资源操作碰撞的另一种方法是使用线程局部变量,线程局部变量的机制是为共享的变量在每个线程中创建一个存储区存储变量副本。线程局部变量ThreadLocal实例通常是类中的private static字段,它们希望将状态与某一个线程相关联。例子如下:
import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadGenerator{
private static final AtomicInteger uniqueId = new AtomicInteger(0);
//创建一个线程局部变量
private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>(){
//覆盖线程局部变量的initialValue方法,为线程局部变量初始化赋值
protected Integer initialValue(){
return uniqueId.getAndIncrement();
}
}
public static int getCurrentThreadId(){
//获取线程局部变量的当前线程副本中的值
return uniqueId.get();
}
}
线程局部变量java.lang.ThreadLocal类有下面四个方法:
(1).T get():返回此线程局部变量的当前线程副本中的值。
(2).protected T initialValue():返回此线程局部变量的当前线程的初始值。
(3).void remove():移除此线程局部变量当前线程的值。
(4).void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
只要线程是活动的并且线程局部变量实例是可访问的,每个线程都保持对其线程局部变量副本的隐式引用。在线程消失之后,其线程局部变量实例的所有副本都会被垃圾回收。
3.线程的yield,sleep和wait的区别:
(1).yield:
正在运行的线程让出CPU时间片,让线程调度器运行其他优先级相同的线程。使用yield()处于可运行状态的线程有可能立刻又进入执行状态。
yield不释放对象锁,即yield线程对象中其他synchronized的数据不能被别的线程使用。
(2).sleep:
当前正在运行的线程进入阻塞状态,sleep是线程Thread类的方法,在sleep的时间内线程肯定不会再运行,sleep可使优先级低得线程得到执行的机会,也可以让同优先级和高优先级的线程有执行机会。
sleep的线程如果持有对象的线程锁,则sleep期间也不会释放线程锁,即sleep线程对象中其他synchronized的数据不能被别的线程使用。
sleep方法可以在非synchronized线程同步的方法中调用,因为sleep()不释放锁。
(3).wait:
正在运行的线程进入等待队列中,wait是Object的方法,线程调用wait方法后会释放掉所持有的对象锁,即wait线程对象中其他synchronized的数据可以被别的线程使用。
wait(), notify()和notifyAll()方法必须只能在synchronized线程同步方法中调用,因为在调用这些方法之前必须首先获得线程锁,如果在非线程同步的方法中调用时,编译时没有问题,但在运行时会抛IllegalMonitorStateException异常,异常信息是当前线程不是方法对象的监视器对象所有者。
4.线程间的通信:
Java中通过wait(),notify()和notifyAll()方法进行线程间的通信,在JDK5之后,又引入了signal()和signalAll()进行线程通信。
(1).wait()和notify(),notifyAll():
wait使得一个正在执行的线程进入阻塞状态,wait也可以象sleep一样指定时间,但是常用的是无参数的wait()方法。notify和notifyAll唤醒处于阻塞态的线程进入可运行状态,通知他们当前的CPU可用。这三个方法都是Object中的方法,不是线程Thread的特有方法。例子如下:
import java.util.concurent.*;

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 c){
car = c;
}
public void run(){
try{
while(!Thread.interrupted()){
System.out.println(“Wax On!”);
TimeUnit.SECONDS.sleep(1);
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 c){
car = c;
}
public void run(){
try{
while(!Thread.interrupted()){
car.waitForWaxing();
System.out.println(“Wax Off!”);
TimeUnit.SECONDS.sleep(1);
car.buffed();
}
}catch(InterruptedException e){
System.out.println(“Exiting via interrupt”);
}
System.out.println(“Ending Wax Off task”);
}
}
public class WaxOMatic{
public static void main(String[] args)throws Exception{
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));
exec.execute(new WaxOn(car));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
输出结果:
Wax On! Wax Off! Wax On! Wax Off! Wax On!
Exiting via interrupt
Ending Wax On task
Exiting via interrupt
Ending Wax Off task
注意:waite (),notify()和notifyAll()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
(2).await(), signal()和signalAll()进行线程通信:
JDK5中引入了线程锁Lock来实现线程同步,使用Condition将对象的监视器分解成截然不同的对象,以便通过这些对象与任意线程锁Lock组合使用,使用signal和signalAll唤醒处于阻塞状态的线程,例子如下:
import java.util.concurrent.*;
import java.util.concurrent.lock.*;

class Car{
private Lock lock = new ReentrantLock();
//获取线程锁的条件实例
private Condition condition = lock.newCondition();
private Boolean waxOn = false;
public void waxed(){
lock.lock();
try{
waxOn = true;
//唤醒等待的线程
condition.signalAll();
}finally{
lock.unlock();
}
}
public void buffed(){
lock.lock();
try{
waxOn = false;
//唤醒等待的线程
condition.signalAll();
}finally{
lock.unlock();
}
}
public void waitForWaxing()throws InterruptedException{
lock.lock();
try{
while(waxOn == false){
//使当前线程处于等待状态
condition.await();
}finally{
lock.unlock();
}
}
}
public void waitForBuffing()throws InterruptedException{
lock.lock();
try{
while(waxOn == true){
//使当前线程处于等待状态
condition.await();
}finally{
lock.unlock();
}
}
}
}
class WaxOn implements Runnable{
private Car car;
public WaxOn(Car c){
car = c;
}
public void run(){
try{
while(!Thread.interrupted()){
System.out.println(“Wax On!”);
TimeUnit.SECONDS.sleep(1);
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 c){
car = c;
}
public void run(){
try{
while(!Thread.interrupted()){
car.waitForWaxing();
System.out.println(“Wax Off!”);
TimeUnit.SECONDS.sleep(1);
car.buffed();
}
}catch(InterruptedException e){
System.out.println(“Exiting via interrupt”);
}
System.out.println(“Ending Wax Off task”);
}
}
public class WaxOMatic{
public static void main(String[] args)throws Exception{
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));
exec.execute(new WaxOn(car));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
输出结果:
Wax On! Wax Off! Wax On! Wax Off! Wax On!
Exiting via interrupt
Ending Wax On task
Exiting via interrupt
Ending Wax Off task
5.使用ScheduledThreadPoolExecutor实现定时任务:
JDK1.5之前,使用Timer实现定时任务,JDK1.5之后引入了ScheduleThreadPoolExecutor实现多线程的定时任务。例子如下:
import java.util.concurrent.*;
import java.util.*;

public class DemoSchedule{
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
//创建并在给定延迟后时间启动的一次性操作
public void schedule(Runnable event, long delay){
scheduler.schedule(event, delay, TimeUnit.SECOND);
}
//创建并在给定延迟时间之后,每个给定时间周期性执行的操作
public void repeat(Runnable event, long initialDelay, long period){
scheduler.scheduleAtFixedRate(event, initialDelay, period, TimeUnit.SECOND);
}
}
class OnetimeSchedule implement Runnable{
public void run(){
System.out.println(“One time schedule task”);
}
}
class RepeatSchedule implement Runnable{
public void run(){
System.out.println(“Repeat schedule task”);
}
}
class TestSchedule {
public static void main(String[] args){
DemoSchedule scheduler = new DemoSchedule();
scheduler.schedule(new OnetimeSchedule(), 5);
scheduler.schedule(new RepeatSchedule (), 10, 10);
}
}
输出结果:
5秒钟之后打印输出:One timeschedule task
10秒钟之后打印输出:Repeatschedule task
之后每隔10秒钟打印输出:Repeat schedule task
6.信号量Semaphore:
一个正常的线程同步锁concurrent.locks或者内置的synchronized只允许同一时间一个任务访问一个资源,而计数信号量Semaphore运行多个任务在同一时间访问一个资源。Semaphore是一个计数信号量,维护了一个许可集,通常用于限制可以访问某些资源的线程数目。例子如下:
import java.util.concurrent.*;
import java.util.*;

public class Pool<T> {
//限制的线程数目
private int size;
//存放资源集合
private List<T> items = new ArrayList<T>();
//标记资源是否被使用
private volatile boolean[] checkedOut;
private Semaphore available;
public Pool(Class<T> classObject, int size){
this.size = size;
checkedOut = new boolean[size];
//创建信号量对象
available = new Semaphore(size, true);
for(int i = 0; i < size; ++i){
try{
//加载资源对象并存放到集合中
items.add(classObject.newInstance());
}catch(Exception e){
throw new RuntimeException(e);
}
}
}
//访问资源
public T checkout() throws InterruptedException{
//从信号量中获取一个资源许可
available.acquired();
return getItem();
}
//释放资源
public void checkIn(T x){
if(releaseItem(x))
//释放一个资源许可,将其返回给信号量
available.release();
}
//获取资源
private synchronized T getItem(){
for(int i = 0; i < size; ++i){
//资源没有被使用
if(!checkedOut[i]){
//标记资源被使用
checkedOut[i] = true;
return items.get(i);
}
}
return null;
}
private synchronized boolean releaseItem(T item){
int index = items.indexOf(item);
//资源不在资源集合中
if(index == -1) return false;
//资源正在被使用
if(checkedOut[index]){
//将资源标记为不再使用
checkedOut[index] = false;
return true;
}
return false;
}
}
在访问资源前,每个线程必须首先从信号量获取许可,从而保证可以使用该资源。该线程结束后,将资源释放回资源池中并将许可返回给信号量,从而允许其他线程获取和使用该资源。
如果当前信号量中没有资源访问许可,则信号量会阻塞调用的线程直到获取一个资源许可,否则,线程将被中断。
注意:调用信号量的acquire时无法保持同步锁,因为这会阻止将该资源返回到资源池中,信号量封装所需要的同步,以限制对资源池的访问。
7.Exchanger线程同步交换器:
Exchanger类,可以用来完成线程间的数据交换。 类java.util.concurrent.Exchanger提供了一个同步点,在这个同步点,一对线程可以交换数据。每个线程通过exchange()方法的入口提供数据给他的伙伴线程,并接收他的伙伴线程提供的数据,并返回。当两个线程通过Exchanger交换了对象,这个交换对于两个线程来说都是安全的。例子如下:
//杯子类
class Cup{
private int capacity = 0;
public Cup(int capacity){
this.capacity = capacity;
}
public int getCapacity(){
return capacity;
}
public void addWaterToCup(int i){
capacity += I;
  capacity = capacity > 100 ? 100 : capacity;
}
public void drinkWaterFromCup(int i){
capacity += I;
capacity = capacity < 0 ? 0 : capacity;
}
public void isFull(){
return capacity == 100 ? true : false;
}
public void isEmpty(){
return capacity == 0 ? true : false;
}
}
import  java.util.concurrent.Exchanger;
import java.util.*;

public class TestExchanger{
Cup emptyCup = new Cup(0);
Cup fullCup = new Cup(100);
Exchanger<Cup> exchanger = new Exchanger<Cup>();

//服务员类
class Waiter implements Runnable{
private int addSpeed = 1;
public Waiter(int addSpeed){
this.addSpeed = addSpeed;
}
public void run(){
while(emptyCup != null){
try{
//如果杯子已满,则与顾客交换,服务员有获得空杯子
if(emptyCup.isFull()){
emptyCup = exchanger.exchange(emptyCup);
System.out.println(“Waiter: ” + emptyCup. getCapacity());
}else{
emptyCup.addWaterToCup(addSpeed);
System.out.println(“Waiter add ” + addSpeed +
“ and current capacity is:” + emptyCup. getCapacity());
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
}
}catch(InterruptedException e){
e.printStackTrack();
}
}
}
}
//顾客类
class Customer implements Runnable{
int drinkSpeed = 1;
public Customer(int drinkSpeed){
this.drinkSpeed = drinkSpeed;
}
public void run(){
while(fullCup != null){
try{
//如果杯子已空,则与服务员进行交换,顾客获得装满水的杯子
if(fullCup.isEmpty()){
fullCup = exchanger.exchanger(fullCup);
System.out.println(“Customer: ” + fullCup. getCapacity());
}else{
fullCup.drinkWaterFromCup(drinkSpeed);
System.out.println(“Customer drink ” + drinkSpeed +
“ and current capacity is:” + fullCup.getCapacity());
TimeUnit.SECONDS.sleep(new Random().nextInt(10));
}
}catch(InterruptedException e){
e.printStackTrack();
}
}
}
}
public static void main(String[] args){
new Thread(new Waiter(3)).start();
new Thread(new Customer(6)).start();
}
}









猜你喜欢

转载自zhyp29.iteye.com/blog/2307753