第21章 并发
前面所学习的都是有关顺序编程的知识:程序中的所有事物在任意时刻都只能执行一个步骤。然而,对于某些问题,能够并行地执行程序中的多个部分则是非常必要的。
本章主要介绍并发的基本知识,使得我们能够理解其概念并编写出合理的多线程程序。
21.1 并发的多面性
使用并发解决的问题大体上可以分为速度和设计可管理性两种。
21.1.1 更快的执行
通常情况下,我们对并发能够提高速度的理解会是,并发可以更好地运用多处理器。
但是,其实并发也能提高单处理器上的程序性能。其原因主要是:阻塞。 即程序中的某个任务因为该程序控制范围之外的某些条件(如I/O)而导致无法继续执行。此时,该线程将被阻塞,整个程序都将停止下来,直至外部条件发生变化。
实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程。
多文件复制是这种并发的理想示例:每个任务都作为进程在其自己的地址空间中执行,并且,它们之间没有任何彼此通信的需要,操作系统会处理确保文件正确复制的所有细节。
而像Java所使用的并发系统则会共享诸如内存和I/O这样的资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。
Java提供的是线程机制:在由执行程序表示的单一进程中创建任务。 这种方式的好处是操作系统的透明性,对于不支持多任务的操作系统,仍然可以运行Java多线程程序。
21.1.2 改进代码设计
Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。
线程能够帮助我们创建更加松散耦合的设计。
21.2 基本的线程机制
并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,其底层机制是切分CPU时间。
在使用线程时,CPU将轮流给每个任务分配其占用时间。而线程的好处是:我们在编写代码时,则不必关心它是运行在一个还是多个CPU的机器上。使用线程机制是一种建立透明的、可扩展的程序的方法,可以通过增肌CPU加快程序的运行速度。多任务和多线程往往是使用多处理器系统的最合理方式。
21.2.1 定义任务
线程可以驱动任务,下面是任务的定义方式:
public class LiftOff implements Runnable {
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" + (countDown > 0 ? countDown : "LiftOff!" + "), ");
}
public void run() {
while(countDown-- > 0) {
System.out.println(status());
Thread.yield();
}
}
}
通过实现Runnable接口并编写run()方法,就成功定义了一个任务。标识符id可以用来区分任务的多个示例。Thread.yield():建议线程调度器切换线程。
在下面的示例中,这个任务并非由单独的线程驱动的,而是被直接调用的:
public class MainThread {
public static void main(String[] args) {
LiftOff launch = new LiftOff();
launch.run();
}
}
21.2.2 Thread类
将Runnable对象转变为任务的传统方式是使用Thread类:
public class BasicThreads {
public static void main(String[] args) {
Thread t = new Thread(new LiftOff());
t.start();
System.out.println("Waiting for LiftOff");
}
}
从输出可以看到:打印的信息在任务完成之前被执行了,其原因是该任务被其他线程所执行,因此主线程能够继续顺序执行。 这种能力并不局限于主线程,任何线程都可以启动另一个线程。
下面,我们可以添加更多的线程去驱动更多的任务:
public class MoreBasicThreads {
public static void main(String[] args) {
for (int i = 0; i < 5; i++)
new Thread(new LiftOff()).start();
System.out.println("Waiting for LiftOff");
}
}
输出说明不同任务的执行在线程被换进换出时混在了一起。这种交换是由线程调度器自动控制的。如果运行程序的机器具有多处理器,线程调度器将会在这些处理器之间默默分发线程。并且,该程序的运行结果可能是变化的,因为线程调度机制是非确定性的。
21.2.3 使用Executor
Executor在客户端和任务执行之间提供了一个间接层,并且允许我们管理异步任务的执行。
下面通过使用Excutor重写上述示例,展示了Excutor的用法:
public class CachedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
ExecutorService:具有声明周期的Executor,通过构建恰当的上下文来执行Runnable对象。
ExecutorService.shutdown():防止新任务提交,当前线程在完成shutdown()被调用前所提交的所有任务后,安全退出。
并且,我们可以轻易地将前面示例中的CachedThreadPool替换为不同类型的Executor:
public class FixedThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
FixedThreadPool使用了有限的线程集来执行所提交的任务。通过它,可以一次性预先执行代价高昂的线程分配,从而不必为每个任务都固定地付出创建线程的开销。
CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。
SingleThreadExecutor:单线程执行器,如果有多个任务,那么这些任务将排队,在一个任务执行完成后执行下一个任务,所有的任务将使用相同的线程。 在下面的示例中,可以看到每个任务都是按照它们被提交的顺序依次执行:
public class SingleThreadExecutor {
public static void main(String[] args) {
ExecutorService exec = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己的悬挂任务队列。
21.2.4 从任务中产生返回值
Runnable是执行工作的独立任务,没有任何返回值。如果希望任务完成时能够返回一个值,则需要实现Callable接口而不是Runnable接口。下面是一个简单示例:
class TaskWithResult implements Callable<String> {
private int id;
public TaskWithResult(int id) {
this.id = id;
}
public String call() {
return "result of TaskWithResult " + id;
}
}
public class CallableDemo {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> results = new ArrayList<Future<String>>();
for (int i = 0; i < 10; i++)
results.add(exec.submit(new TaskWithResult(i)));
for (Future<String> future : results) {
try {
System.out.println(future.get());
} catch (InterruptedException e) {
System.out.println(e);
return;
} catch (ExecutionException e) {
System.out.println(e);
} finally {
exec.shutdown();
}
}
}
}
- ExecutorService.submit(Callable):产生一个Future对象。
- Future.get():如果任务已完成,获取执行任务后的返回值,否则,将堵塞。
- Future.isDone():查询Future是否已完成。
21.2.5 休眠
影响任务行为的一种简单方法是调用sleep():
public class SleepingTask extends LiftOff {
public void run() {
try {
while(countDown-- > 0) {
System.out.print(status());
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
System.err.println("Interrupted");
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new SleepingTask());
exec.shutdown();
}
}
对TimeUnit.sleep()的调用会抛出InterruptedException异常,由于异常无法跨线程传播回主线程,所以必须在当前执行任务的线程中处理所有异常信息。
21.2.6 优先级
线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先权更高的线程先执行,而优先级较低的线程执行的频率则会较低。
下面是一个演示优先级等级的示例:
public class SimplePriorities implements Runnable {
private int countDown = 5;
private volatile double d;
private int priority;
public SimplePriorities(int priority) {
this.priority = priority;
}
public String toString() {
return Thread.currentThread() + ": " + countDown;
}
public void run() {
Thread.currentThread().setPriority(priority);
while(true) {
for (int i = 1; i < 1000000; i++) {
d += (Math.PI + Math.E) / (double)i;
if(i % 1000 == 0)
Thread.yield();
}
System.out.println(this);
if(--countDown == 0) return;
}
}
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.currentThread():获取当前线程信息。
- Thread.setPriority():设置指定线程的优先级。
- Thread.getPriority():获取指定线程的优先级。
由于数学运算是可以中断,所以本例通过大量运算,使得线程调度机制有时间交换任务并关注优先级,使得最高优先级线程被优先选择。
21.2.7 切换线程
当run()方法的循环一次迭代的工作完成时,我们可以通过调用Thread.yield()方法通知线程调度器,不过没有任何机制保证它会被采纳。但也不能过度依赖yield(),使其被误用。
21.2.8 后台线程
后台线程是指:在程序运行时,在后退提供通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序才会终止,同时会杀死进程中的所有后台线程。
下面的示例展示了在主线程中开启多个后台线程:
public class SimpleDaemons implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch (InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);
daemon.start();
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(99);
}
}
Thread.setDaemon(true):在线程启动前,将该线程设置为后台线程。一旦主线程完成其工作,所有后台进程都将被终止。
通过编写定制的ThreadFactory可以定制由Exector创建的线程的属性(后台、优先级、名称)。下面ThreadFactory产生的所有线程均为后台线程:
public class DaemonThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread();
t.setDaemon(true);
return t;
}
}
现在,我们可以通过Executors.newCachedThreadPool(ThreadFactory)来创建指定ThreadFactory的执行器了:
public class DaemonFromFactory implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch (InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public static void main(String[] args) throws InterruptedException {
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);
}
}
Thread.isDaemon()方法可以确定一个线程是否是一个后台线程:
class Daemon implements Runnable {
private Thread[] t = new Thread[10];
public void run() {
for (int i = 0; i < t.length; i++) {
t[i] = new Thread(new DaemonSpawn());
t[i].start();
System.out.print("DaemonSpawn " + i + " started, ");
}
for (int i = 0; i < t.length; i++)
System.out.print("t[" + i + "].isDaemon() = " + t[i].isDaemon() + ", ");
while(true)
Thread.yield();
}
}
class DaemonSpawn implements Runnable {
public void run() {
while(true) {
Thread.yield();
}
}
}
public class Daemons {
public static void main(String[] args) throws InterruptedException {
Thread d = new Thread(new Daemon());
d.setDaemon(true);
d.start();
System.out.print("d.isDaemon() = " + d.isDaemon() + ", ");
TimeUnit.SECONDS.sleep(1);
}
}
可以发现:一个后台线程的所有子线程都被自动设置为后台线程。
无论在任何情况下,当程序中最后一个非后台线程结束时,后台进程都会被终止:
class ADaemon implements Runnable {
public void run() {
try {
System.out.println("Starting ADaemon");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("Exiting via InterruptedException");
} finally {
System.out.println("This should always run?");
}
}
}
public class DaemonsDontRunFinally {
public static void main(String[] args) {
Thread t = new Thread(new ADaemon());
t.setDaemon(true);
t.start();
}
}
在本例中,finally子句并未执行。一旦main()结束,JVM就会立即关闭所有的后台进程。我们无法以优雅的方式来关闭后台进程,所以非后台线程通常会是一种更好的选择,我们可以通过有序的方式关闭非后台进程。
21.2.9 编码的变体
下面我们通过继承Thread的方式定义任务,并在构造器中启动线程:
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
super(Integer.toString(++threadCount));
start();
}
public String toString() {
return "#" + getName() + "(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++)
new SimpleThread();
}
}
我们也可以定义一个自管理的Runnable:
public class SelfManaged implements Runnable {
private int countDown = 5;
public SelfManaged() { new Thread(this).start(); }
public String toString() {
return Thread.currentThread().getName() + "(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++)
new SelfManaged();
}
}
这与直接继承Thread看似没有差别,但实现接口使得我们可以继承其他的类。
下面是通过内部类的形式将线程代码隐藏在类中:
class InnerThread1 {
private int countDown = 5;
public InnerThread1(String name) {
new Thread(name) {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
}.start();
}
}
class InnerRunnable1 {
private int countDown = 5;
public InnerRunnable1(String name) {
new Thread(new Runnable() {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
public String toString() {
return Thread.currentThread().getName() + ": " + countDown;
}
}, name).start();;
}
}
class ThreadMethod {
private int countDown = 5;
private String name;
public ThreadMethod(String name) { this.name = name; }
public void runTask() {
new Thread(name) {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
}.start();
}
}
public class ThreadVariations {
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerRunnable1("InnerRunnable1");
new ThreadMethod("ThreadMethod").runTask();
}
}
本例分别演示了如何在构造器和方法中以内部类的形式创建任务并启动线程。
21.2.10 术语
通过前面的学习,我们可能会产生一个错误认知:线程就是任务。其实,它们的关系是:任务依附于线程,线程是负责驱动赋予它的任务。并且,我们似乎对Thread类实际没有任务控制权,在使用执行器时更是如此,因为执行器替我们处理了线程的创建和管理。
21.2.11 调用其他线程
如果在一个线程上调用另一个线程的join()方法,则此线程将被挂起,直到目标线程结束才继续执行。join(long millis)可以设置超时参数:
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) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}
public class Joining {
public static void main(String[] args) {
Sleeper sleepy = new Sleeper("Sleepy", 1500);
Sleeper grumpy = new Sleeper("Grumpy", 1500);
Joiner dopey = new Joiner("Dopey", sleepy);
Joiner doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
}
}
21.2.12 创建有响应的用户界面
使用线程的动机之一就是建立由响应的用户界面。下面的例子通过使用线程和不使用线程进行对比,展现了线程的优点:
class UnresponsiveUI {
private volatile double d = 1;
public UnresponsiveUI() throws Exception {
while(d > 0)
d = d + (Math.PI + Math.E) / d;
System.in.read();
}
}
public class ResponsiveUI extends Thread {
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 {
// no response
//! new UnresponsiveUI();
new ResponsiveUI();
System.in.read();
System.out.println(d);
}
}
21.2.13 捕获异常
由于线程的本质特性,使得我们无法捕获从线程中逃逸的异常。如果异常未在run()方法内被捕获,则会直接传播到控制台:
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
并且,在外界对其进行捕获是无效的:
public class NaiveExceptionHandling {
public static void main(String[] args) {
try {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch (RuntimeException e) {
System.out.println("Exception has been handled");
}
}
}
不过,Java SE5提供了一个新接口:Thread.UncaughtExceptionHandler,它允许我们在每一个Thread对象上设置一个异常处理器。并且该接口中的uncaughtException()方法会在线程因为捕获的异常而临近死亡时被调用:
class ExceptionThread2 extends Thread {
public ExceptionThread2() {
System.out.println("created " + this);
this.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("eh = " + this.getUncaughtExceptionHandler());
start();
}
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println("eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
new ExceptionThread2();
}
}
如果程序中处处都是用同一个异常处理器,则可以通过静态方法Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)将指定处理器设为默认的未捕获异常处理器:
public class SettingDefaultHandler {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
这个处理器只有在线程专有的未捕获异常处理器不存在的情况下才会被调用。
21.3 共享受限资源
单线程程序每次只做一件事,自然不存在同一时间,一个资源被多个实体共同访问。有了并发则可以同时做多件事,两个或多个线程彼此互相干涉的问题也就出现了。
21.3.1 不正确地访问资源
下面的例子中,一个类负责产生偶数,我们创建了消费该类所产生的数字:
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
public void cancel() { canceled = true; }
public boolean isCanceled() { return canceled; }
}
public class EvenChecker implements Runnable {
private IntGenerator generator;
private final int id;
public EvenChecker(IntGenerator generator, int id) {
this.generator = generator;
this.id = id;
}
public void run() {
while(!generator.isCanceled()) {
int val = generator.next();
if(val % 2 != 0) {
System.out.println(val + " not even!");
generator.cancel();
}
}
}
public static void test(IntGenerator generator, int count) {
System.out.println("Press Control-C to exit");
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++)
exec.execute(new EvenChecker(generator, i));
exec.shutdown();
}
public static void test(IntGenerator generator) {
test(generator, 10);
}
}
在本例中,线程共享的资源是IntGenerator类,EvenChecker的任务是读取并测试从IntGenerator返回的值。
下面是IntGenerator的一个实现类,可以产生一系列偶数值:
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public int next() {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
当一个任务在另一个任务执行第一个递增操作之后,但没有执行第二个递增操作之前,调用了next()方法,这将使得该值处于不恰当的状态。并且,在Java中,递增程序自身也需要多个步骤,递增过程中任务也可能会被线程挂起,即递增不是原子性的操作。
21.3.2 解决共享资源竞争
对于并发工作,我们需要某种方式来防止多个任务访问相同的资源。防止这种冲突的方法就是当资源被任何一个任务使用时,都为其上锁。
基本上所有的并发模式在解决线程冲突问题时,都是采用序列化访问共享资源的方案:在给定时刻只允许一个任务访问共享资源。这通常是通过在代码前面加上一条锁语句来实现,锁语句产生的是互相排斥的效果。
由于线程机制并不是确定性的,所以我们也无法得知当锁被开启时,下一个执行的是哪个线程。当然,可以通过yield()和setPriority()给线程调度机制提供建议,但这并非十分有效,主要还是取决于具体平台和JVM实现。
Java以关键字synchronized为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
共享资源一般是以对象形式存在的内存片段,也可以是文件、输入/输出端口,或是打印机。要控制对共享资源的访问,首先将其包装进一个对象,然后把所有要访问该对象的方法标记为synchronized。如果某个任务中调用了由synchronized标记的方法,那么在这个线程从该方法返回之前,其他所有要调用该类中任何synchronized方法的线程都会被阻塞。
下面是声明synchronized方法的方式:
public synchronized void f() {}
public synchronized void g() {}
所有对象都自动含有单一的锁,当在对象上调用其任意synchronized方法时,此对象都被加锁,此时对该对象上的其他synchronized方法的调用只能等到前一个方法调用完毕并释放了锁之后才能被调用。即:对于某个特定对象来说,其所有synchronized方法共享同一个锁。
针对每个类,也有一个锁,作为类的Class对象的一部分,所以synchronized static方法可以针对在类的范围内防止对static数据的并发访问。
在处理共享数据时需要注意:针对类中每一个操作共享数据的方法,都必须被同步。
同步控制EvenGenerator
通过在EvenGenerator.java中加入synchronized关键字,可以防止共享资源被多个线程同时操作:
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public synchronized int next() {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
使用显式的Lock对象
Java SE5还提供了显式的互斥机制:Lock对象必须被显式地创建、锁定和释放。因此,它与内建的锁形式相比,代码缺失优雅性,但对于解决某些类型的问题来说,它更加灵活。
下面用显式的Lock重写上述示例:
public class MutexEvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
EvenChecker.test(new MutexEvenGenerator());
}
}
我们在紧跟着对lock()的调用处,放置了try-finally以确保unlock()一定会被调用。并且,在try子句中的return也保证了unlock()不会被过早的调用。
尽管使用显式的Lock增加了代码量,但如果在使用synchronized时抛出了异常,则无法进行清理工作以维护系统使其处于良好状态,而使用显式的Lock对象,则可以通过finally子句将系统维护在正确的状态了。
通常情况下,只有在解决特殊问题时,才使用显式的Lock对象:
public class AttemptLocking {
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 AttemptLocking al = new AttemptLocking();
al.untimed();
al.timed();
new Thread() {
{
setDaemon(true);
}
public void run() {
al.lock.lock();
System.out.println("acquired");
}
}.start();
Thread.yield();
al.untimed();
al.timed();
}
}
- lock():获取锁,如果锁被使用,该线程将一直等待,直至锁被释放。
- tryLock():尝试获取锁,可以获取返回true,获取不到返回false。
- tryLock(long timeout, TimeUnit unit):在一定时间内获取锁。
显式的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,具有更细粒度的控制力。
21.3.3 原子性与易变性
原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在切换到其他线程之前执行完毕。
原子性可以应用于除long和double之外的所有基本类型之上的简单操作:读取和写入。由于JVM会将64位的读取和写入当作两个分离的32位操作来执行,所以long和double类型则不具备原子性。在Java SE5之后,我们也可以使用volatile关键字使它们具备原子性。
原子操作可由线程机制来包装其不可中断,但有时看似安全的原子操作,实际上也可能不安全。
在多处理器系统上,相对于单处理器系统而言,可视性问题比原子性问题要多得多。一个任务做出的修改,即使是原子性的,对其他任务也可能是不可视的。例如,修改只是暂时性地存储在本地处理器的缓存中。因此不同的任务对应用的状态有不同的视图。
volatile关键字确保了应用中的可视性:如果一个域被声明为volatile,那么只要对该域产生了写操作,所有的读操作都可以看到这个修改。即便使用了本地缓存,volatile域会立即被写入主存中,而读取操作就发生在主存中。
synchronized关键字也会导致向主存中刷新,如果一个域由synchronized方法或语句块来防护,则不必将其设置为volatile。
一个任务所做的任何写入操作对该任务来说都是可视的。
在Java中,赋值与返回操作是具有原子性的。
如果盲目应用原子性概念,则会发生错误:
public class AtomicityTest implements Runnable{
private int i = 0;
public int getValue() { return i; }
private synchronized void evenIncrement() {
i++;
i++;
}
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while(true) {
int val = at.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}
在本例中,尽管return是原子性操作,但是缺少同步使得其数值可以在evenIncrement()方法在执行第一个递增操作之后,第二个递增操作之前被获取到。解决方案是:getValue()和evenIncrement()都声明为synchronized。这样,由于锁的机制,同一时间只有一个线程可以访问到该类中的被synchronized的修饰的其中一个方法。
21.3.4 原子类
Java SE5引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供了下面形式的原子性条件更新操作:
boolean compareAndSet(expect, update);
这些类具备在机器级别上的原子性,使用它们可以进行性能调优:
public class AtomicIntegerTest implements Runnable{
private AtomicInteger i = new AtomicInteger(0);
public int getValue() { return i.get(); }
private void evenIncrement() { i.addAndGet(2); }
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args) {
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);
}
}
}
}
我们通过使用AtomicInteger重写了AtomicityTest.java,即使没有用synchronized关键字,程序也不会失败,而使用Timer则可以在5秒钟后关闭程序。但通常情况下,使用锁要更安全一些。不管是synchronized关键字,还是显式的Lock对象。
21.3.5 临界区
有时,我们只是希望防止多个线程同时访问方法内部的部分代码而不是整个方法,而这部分代码所形成的代码片段被称为临界区,它也可以使用synchronized关键字建立。
synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:
synchronized(syncObject) {
// This code can be accessed
// by only one tash 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++; }
public String toString() { return "x: " + x + ", y: " + y; }
public class PairValuesNotEqualException extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal: " + Pair.this);
}
}
public void checkState() {
if(x != y)
throw new PairValuesNotEqualException();
}
}
abstract class PairManager {
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>());
public synchronized Pair getPair() {
return new Pair(p.getX(), p.getY());
}
protected void store(Pair p) {
storage.add(p);
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {}
}
public abstract void increment();
}
class PairManaer1 extends PairManager {
public synchronized void increment() {
p.incrementX();
p.incrementY();
store(getPair());
}
}
class PairManaer2 extends PairManager {
public void increment() {
Pair temp;
synchronized (this) {
p.incrementX();
p.incrementY();
temp = p;
}
store(temp);
}
}
class PairManipulator implements Runnable {
private PairManager pm;
public PairManipulator(PairManager pm) {
this.pm = pm;
}
public void run() {
while(true)
pm.increment();
}
public String toString() {
return "Pair: " + pm.getPair() + " checkCounter = " + pm.checkCounter.get();
}
}
class PairChecker implements Runnable {
private PairManager pm;
public PairChecker(PairManager pm) {
this.pm = pm;
}
public void run() {
while(true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
}
public class CriticalSection {
static void testApproaches(PairManager pm1,PairManager pm2) {
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator p1 = new PairManipulator(pm1);
PairManipulator p2 = new PairManipulator(pm2);
PairChecker pc1 = new PairChecker(pm1);
PairChecker pc2 = new PairChecker(pm2);
exec.execute(p1);
exec.execute(p2);
exec.execute(pc1);
exec.execute(pc2);
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("Sleep interrupted");
}
System.out.println("p1: " + p1 + "\np2: " + p2);
System.exit(0);
}
public static void main(String[] args) {
PairManager pm1 = new PairManaer1();
PairManaer2 pm2 = new PairManaer2();
testApproaches(pm1, pm2);
}
}
在本例中,Pair不是线程安全的,为了使用该非线程安全的现有类,我们通过创建PairManager来持有Pair对象,并控制对它的一切访问。PairManager使用了模板方法设计模式,将变化的部分定义为抽象方法。
PairManager的两个实现类分别以同步方法和同步控制块进行线程安全的管理。我们通过睡眠的方式模拟耗时操作,两种方式的区别是:耗时操作分别在临界区内和临界区外。这样前者对锁的控制时间在递增任务上较长,而在检测任务上对锁的控制时间自然就短了。
我们也可以使用显式的Lock对象来创建临界区:
class ExplicitPairManager1 extends PairManager {
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
p.incrementX();
p.incrementY();
store(getPair());
} finally {
lock.unlock();
}
}
}
class ExplicitPairManager2 extends PairManager {
private Lock lock = new ReentrantLock();
public void increment() {
Pair pair;
lock.lock();
try {
p.incrementX();
p.incrementY();
pair = getPair();
} finally {
lock.unlock();
}
store(pair);
}
}
public class ExplicitCriticalSection {
public static void main(String[] args) {
PairManager pm1 = new ExplicitPairManager1();
PairManager pm2 = new ExplicitPairManager2();
CriticalSection.testApproaches(pm1, pm2);
}
}
21.3.6 在其他对象上同步
在CriticalSection.java的示例中,我们使用synchronized (this)对被调用的当前对象进行加锁,那么该对象中的其他同步方法和临界区在该对象被锁时将无法被调用。
当然,我们可以对其他对象进行同步,那么不同的任务则可以同时访问该对的同步方法和临界区:
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
for (int i = 0; i < 5; i++) {
System.out.println("f()");
Thread.yield();
}
}
public void g() {
for (int i = 0; i < 5; i++) {
System.out.println("g()");
Thread.yield();
}
}
}
public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
}
}
可以看到,这两个同步是相互独立的,并不会因为一个线程对其中一个同步块进行访问导致另一个线程堵塞。
21.3.7 线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享:线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。
创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现:
class Accessor implements Runnable {
private final int id;
public Accessor(int id) { this.id = id; }
public void run() {
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ": " + ThreadLocalVariableHolder.get();
}
}
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(66);
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() { return value.get(); }
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new Accessor(i));
TimeUnit.SECONDS.sleep(3);
exec.shutdownNow();
}
}
ThreadLocal对象通常当作静态域存储:
- ThreadLocal.get():返回与线程相关联的对象的副本。
- ThreadLocal.set(T value):将参数插入到为其线程存储的对象中,并返回原有对象。
当我们对ThreadLocal变量进行操作时,并不需要对其同步,ThreadLocal保证了它不会冲突。可以看到,每个单独的线程跟踪自己的计数值,即被分配了独立的存储区域。
21.4 终结任务
在前面的示例中,我们通过变量canceled来标记是否终止任务。下面的示例也通过此方式来终止任务,并演示了如何进行资源共享。
21.4.1 装饰性花园
下面的程序为了记录每天通过多个大门进入公园的总人数:
class Count {
private int count = 0;
public synchronized int increment() {
return ++count;
}
public synchronized int value() {
return count;
}
}
class Entrance implements Runnable {
private final int id;
private static Count count = new Count();
private static List<Entrance> entrances = new ArrayList<Entrance>();
private int number = 0;
private static volatile boolean canceled = false;
public static void cancel() { canceled = true; }
public Entrance(int id) {
this.id = id;
entrances.add(this);
}
public void run() {
while(!canceled) {
synchronized (this) {
++number;
}
System.out.println(this + " Total: " + count.increment());
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("sleep interrupted");
}
}
System.out.println("Stopping " + this);
}
public synchronized int getValue() { return number; }
public String toString() {
return "Entrance " + id + ": " + getValue();
}
public static int getTotalCount() {
return count.value();
}
public static int sumEntrances() {
int sum = 0;
for (Entrance entrance : entrances)
sum += entrance.getValue();
return sum;
}
}
public class OrnamentalGarden {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new Entrance(i));
TimeUnit.SECONDS.sleep(3);
Entrance.cancel();
exec.shutdown();
if(!exec.awaitTermination(250, TimeUnit.MILLISECONDS))
System.out.println("Some tasks were not terminated!");
System.out.println("Total: " + Entrance.getTotalCount());
System.out.println("Sum of Entrances: " + Entrance.sumEntrances());
}
}
在本例中,Count对象用于记录花园参观者总人数,而变量number则记录从某个大门进入花园的人数。
- 静态方法Entrance.cancel():将终止条件设置为true。
- ExecutorService.shutdown():防止新任务提交,当前线程在完成shutdown()被调用前所提交的所有任务后,安全退出。
- ExecutorService.awaitTermination(long timeout, TimeUnit unit):等待每个任务结束,如果所有任务在指定时间之前完成,返回true,否则返回false。
21.4.2 在阻塞时终结
在上述示例中,Entrance.run()在其循环中包含了对sleep()的调用,如果没有变量canceled标志,我们很有可能对处理睡眠状态的任务进行终止,而此时线程处于阻塞状态。
线程状态
一个线程可以处于以下四种状态之一:
-
新建:当线程被创建时,此时它已经分配了必需的系统资源,并执行了初始化,已经有资格获取CPU时间了,之后调度器将把该线程转变为可运行状态或阻塞状态。
-
就绪:在此状态下,只要调度器把时间片分配给线程,线程就可以运行。
-
阻塞:线程能够运行,但有某个条件阻止了它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配线程任何CPU时间,直到线程进入就绪状态。
-
死亡:处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间。
进入阻塞状态
一个任务进入阻塞状态,可能有如下原因:
- 通过调用sleep()使任务进入休眠状态。
- 通过调用wait()使线程挂起,直到线程得到了notify()或notifyAll()消息,进入就绪状态。
- 任务在等待某个输入/输出完成。
- 任务试图在某个对象上调用其同步控制方法,但对象锁已被占用。
如果我们希望终止处于阻塞状态的任务,必须强制该任务跳出阻塞状态。
21.4.3 中断
Thread.interrupt():将线程设置为中断状态。如果在中断状态下执行一个阻塞操作,则会抛出InterruptedException异常。
Executor也对此进行了支持,ExecutorService.shutdownNow():对该执行器启动的所有线程都设置为中断状态。如果希望中断单一任务,则可以通过ExecutorService.submit(Callable task)来执行任务。此时会返回一个Future对象,Future.cancel()则可以中断某个特定任务。
下面的示例用Executor展示了基本的interrupt()用法:
class SleepBlocked implements Runnable {
public void run() {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("InterruptedException");
}
System.out.println("Exiting SleepBlocked.run()");
}
}
class IOBlocked implements Runnable {
private InputStream in;
public IOBlocked(InputStream in) { this.in = in; }
public void run() {
try {
System.out.println("Waiting for read()");
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 IOBlocked.run()");
}
}
class SynchronizedBlocked implements Runnable {
public synchronized void f() {
while(true)
Thread.yield();
}
public SynchronizedBlocked() {
new Thread() {
public void run() {
f();
}
}.start();;
}
public void run() {
System.out.println("Trying to call f()");
f();
System.out.println("Exiting SynchronizedBlocked.run()");
}
}
public class Interrupting {
private static ExecutorService exec = Executors.newCachedThreadPool();
static void test(Runnable r) throws InterruptedException {
Future<?> f = exec.submit(r);
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Interrupting " + r.getClass().getName());
f.cancel(true);
System.out.println("Interrupt sent to " + r.getClass().getName());
}
public static void main(String[] args) throws Exception {
test(new SleepBlocked());
test(new IOBlocked(System.in));
test(new SynchronizedBlocked());
TimeUnit.SECONDS.sleep(3);
System.out.println("Aborting with System.exit(0)");
System.exit(0);
}
}
上面的每个任务都表示了一种不同类型的阻塞。并且,可以发现:我们可以中断由睡眠引起的阻塞,但无法中断由获取同步锁或执行I/O操作引起的阻塞。这也意味着I/O具有锁住我们多线程程序的潜在可能。
对于这类问题,也有一个有效的解决方案,即关闭导致任务阻塞的底层资源:
public class CloseResource {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
InputStream sockteInput = new Socket("localhost",8080).getInputStream();
exec.execute(new IOBlocked(sockteInput));
exec.execute(new IOBlocked(System.in));
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Shutting down all threads");
exec.shutdownNow();
TimeUnit.SECONDS.sleep(1);
System.out.println("Closing " + sockteInput.getClass().getName());
sockteInput.close();
TimeUnit.SECONDS.sleep(1);
System.out.println("Closing " + System.in.getClass().getName());
System.in.close();
}
}
该程序也显示,一旦底层资源被关闭,任务将解除阻塞。
在第18章介绍的nio类库也提供了更人性化的I/O中断:
class NIOBlocked implements Runnable {
private final SocketChannel sc;
public NIOBlocked(SocketChannel sc) { this.sc = sc; }
public void run() {
try {
System.out.println("Waiting for read() in " + this);
sc.read(ByteBuffer.allocate(1));
} catch (ClosedByInterruptException e) {
System.out.println("ClosedByInterruptException");
} catch (AsynchronousCloseException e) {
System.out.println("AsynchronousCloseException");
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("Exiting NIOBlocked.run() " + this);
}
}
public class NIOInterruption {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
InetSocketAddress address = new InetSocketAddress("localhost",8080);
SocketChannel sc1 = SocketChannel.open(address);
SocketChannel sc2 = SocketChannel.open(address);
Future<?> f = exec.submit(new NIOBlocked(sc1));
exec.execute(new NIOBlocked(sc2));
exec.shutdown();
TimeUnit.SECONDS.sleep(1);
f.cancel(true);
TimeUnit.SECONDS.sleep(1);
sc2.close();
}
}
在这里我们对任务2调用了shutdown()而不是shutdownNow(),并在最后对该任务所用的管道进行了关闭,发现被阻塞的nio通道会自动响应中断。
被互斥所阻塞
如果我们尝试在一个对象上调用其synchronized方法,而该对象锁已被占用,那么调用任务将被挂起,直至该锁可获得。下面的示例说明了同一个任务可以获得多次同一个对象锁:
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() {
public void run() {
multiLock.f1(10);
}
}.start();
}
}
无论在任何时刻,只要任务以不可中断的方式被阻断,那么都有潜在的可能锁住程序。Java SE5并发类库中添加了一个特性,即ReentrantLock上阻塞的任务具备可以被中断的能力:
class BlockedMutex {
private Lock lock = new ReentrantLock();
public BlockedMutex() {
lock.lock();
}
public void f() {
try {
lock.lockInterruptibly();
System.out.println("lock acquired in f()");
} catch (InterruptedException e) {
System.out.println("Interrupted from lock acquisition in f()");
}
}
}
class Blocked2 implements Runnable {
BlockedMutex blocked = new BlockedMutex();
public void run() {
System.out.println("Waiting for f() in BlockedMutex");
blocked.f();
System.out.println("Broken out of blocked call");
}
}
public class Interrupting2 {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new Blocked2());
t.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("Issuing t.interrupt()");
t.interrupt();
}
}
ReentrantLock.lockInterruptibly():允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
21.4.4 检查中断
只有在任务要进入阻塞操作中,或已经在阻塞操作内部时,调用interrupt()方法才会发生中断。如果线程已经经过阻塞状态,此时如果想调用interrupt()以停止某个任务,则需要通过interrupted()来检查中断状态,并清除中断状态:
class NeedsCleanup {
private final int id;
public NeedsCleanup(int ident) {
id = ident;
System.out.println("NeedsCleanup " + id);
}
public void cleanup() {
System.out.println("Cleaning up " + id);
}
}
class Blocked3 implements Runnable {
private volatile double d = 0.0;
public void run() {
try {
while(!Thread.interrupted()) {
NeedsCleanup n1 = new NeedsCleanup(1);
try {
System.out.println("Sleeping");
TimeUnit.SECONDS.sleep(1);
NeedsCleanup n2 = new NeedsCleanup(2);
try {
System.out.println("Calculating");
for (int i = 0; i < 2500000; i++)
d = d + (Math.PI + Math.E) / d;
System.out.println("Finished time-consuming operation");
} finally {
n2.cleanup();
}
} finally {
n1.cleanup();
}
}
System.out.println("Exiting via while() test");
} catch (InterruptedException e) {
System.out.println("Exiting via InterruptedException");
}
}
}
public class InterruptingIdiom {
public static void main(String[] args) throws Exception {
if(args.length != 1) {
System.out.println("usage: java InterruptingIdiom delay-in-mS");
System.exit(1);
}
Thread t = new Thread(new Blocked3());
t.start();
TimeUnit.MILLISECONDS.sleep(new Integer(args[0]));
t.interrupt();
}
}
由于在Java中缺乏自动的析构器调用,因此所有需要清理的对象创建操作后都必须紧跟try-finally子句,从而使得无论程序发生什么异常,清理都会发生。
21.5 线程之间的协作
通过前面的学习,我们知道了当使用线程来同时运行多个任务时,可以通过使用锁来同步两个任务的行为,从而使得在任意时刻,只有一个任务可以访问公共资源。
下一步是学习如何使任务彼此之间可以协作,以使得多个任务可以一起工作去解决某个问题。在某些任务中,有些是可以并行执行,而有些任务则需要在特定任务完成之后才能执行。
当任务协作时,关键问题是这些任务之间的握手。我们同样使用了互斥的特性:互斥能够确保只有一个任务可以响应某个信号,从而根除任何可能的竞争条件。在互斥之上,我们为任务添加了一种途径,可以将其自身挂起,知道某些外部条件发生变化。
21.5.1 wait()与notifyAll()
wait()使得线程挂起,并等待某个条件发生变化,通常该条件由另一个任务来改变。只有在notify()或notifyAll()发生时,该任务才会被唤醒并去检查所产生的变化。
通过前面的学习,我们知道:对sleep()和yield()的调用并不会释放对象锁。但对wait()的调用将释放锁,即其他任务可以获得该对象锁。
wait()方法有两种形式:
- wait():无限制等待,直到线程接收到notify()或notifyAll()
- wait(long timeout):在指定时间内等待,当线程接收到notify()、notifyAll()或时间到期后,该线程将继续执行。
需要注意的是:wait()、notify()以及notifyAll()都是基类Object的一部分,而不是Thread的一部分。并且,只能在同步控制方法或同步控制块内调用这些方法,即调用这些方法的前提是必须拥有对象锁。
下面的示例演示了wait()与notify()的简单用法:
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.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 c) { car = c; }
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");
}
}
public class WaxOMatic {
public static void main(String[] args) throws InterruptedException {
Car c = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(c));
exec.execute(new WaxOn(c));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
}
}
在本例中有两个任务:涂蜡和抛光,并且抛光之前首先得进行涂蜡。Car中的变量waxOn表示涂蜡状态。通过判断waxOn的状态并调用wait()方法,我们可以实现涂蜡与抛光的顺序不可逆性。
错失的信号
当两个线程使用notify()/wait()或notifyAll()/wait()进行协作时,有可能会错过某个信号。例如:
T1:
synchronized (sharedMonitor) {
<setup condition for T2>
sharedMonitor.notify();
}
T2:
while(someCondition) {
// Point 1
synchronized (sharedMonitor) {
sharedMonitor.wait();
}
}
< setup condition for T2 >是改变T2运行条件的动作。假设T2对someCondition求值并发现其为true,但在Point1处,线程调度器切换到了T1,T1将执行其设置,然后调用notify()。当T2得以继续执行时,则会盲目进入wait()。由于错失了notify(),T2将无线等待,从而产生死锁。
该问题的解决方案是防止在someCondition变量上产生竞争条件:
T2:
synchronized (sharedMonitor) {
while(someCondition) {
sharedMonitor.wait();
}
}
现在,如果T1首先执行,当控制返回T2时,它将发现条件发生变化,从而不会进入wait()。
21.5.2 notify()与notifyAll()
notify()会唤醒众多等待同一个锁的任务中的其中一个。而notifyAll()则会唤醒等待这个锁的所有任务:
class Blocker {
synchronized void waitingCall() {
try {
while(!Thread.interrupted()) {
wait();
System.out.print(Thread.currentThread() + " ");
}
} catch (InterruptedException e) {
// OK to exit this way
}
}
synchronized void prod() { notify(); }
synchronized void prodAll() { notifyAll(); }
}
class Task implements Runnable {
static Blocker blocker = new Blocker();
public void run() { blocker.waitingCall(); }
}
class Task2 implements Runnable {
static Blocker blocker = new Blocker();
public void run() { blocker.waitingCall(); }
}
public class NotifyVsNotifyAll {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++)
exec.execute(new Task());
exec.execute(new Task2());
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
boolean prod = true;
public void run() {
if(prod) {
System.out.print("\nnotify() ");
Task.blocker.prod();
prod = false;
} else {
System.out.print("\nnotifyAll() ");
Task.blocker.prodAll();
prod = true;
}
}
}, 400, 400);
TimeUnit.SECONDS.sleep(5);
timer.cancel();
System.out.println("\nTimer canceled");
TimeUnit.MILLISECONDS.sleep(500);
System.out.print("Task2.blocker.prodAll() ");
Task2.blocker.prodAll();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("\nShutting down");
exec.shutdownNow();
}
}
21.5.3 生产者与消费者
在一个饭店中,有一个厨师和服务员。服务员必须等待厨师准备好膳食,而厨师在准备好时,会通知服务员,之后服务员上菜,然后返回继续等待: