Java 并发深入学习二

并发学习第一章,包括并发概念,线程基本机制。

并发学习第三章,死锁、新控件、仿真与其他

3 共享受限资源

当两个或者多个线程彼此互相干涉问题也就出现了。如果不防范这种冲突,就可能发生两个线程同时访问同一个银行账户,或向同一个打印机打印。

3.1 不正确地访问资源

public abstract class IntGenerator {
    private volatile boolean canceled = false;//volatile类型保证可视性
    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 g,int ident){
        generator = g;
        id = ident;
    }
    @Override
    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 gp, 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(gp, i));
        }
        exec.shutdown();
    }
    public static void test(IntGenerator gp){
        test(gp,10);
    }
}

第一个偶数生成器

//产生一系列偶数
public class EvenGenerator extends IntGenerator {
    private int currentEvenValue = 0;
    @Override
    public int next() {
        ++currentEvenValue;//危险的地方
        ++currentEvenValue;
        return currentEvenValue;
    }
    public static void main(String args[]){
        EvenChecker.test(new EvenGenerator());
    }
}
>>> output :Press Control-C to exit
3947 not even!
3949 not even!

在代码注释处,这个值处于“不恰当”的状态。
程序失败,因为各个EvenChecker任务在EvenGenerator处于“不恰当的”状态时,仍能访问其中的信息。这个错误很难被捕捉到,如果希望更快地发现失败,可以尝试将yield()放在两个递增操作之间。
注意,在JAVA中递增不是原子性操作。因此,如果不保护任务,即使单一的递增也是不安全的。

3.2 解决共享资源竞争

前面的例子展示了使用线程时的一个基本问题:你永远都不知道一个线程何时在运行。对于并发工作,需要某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况。

防止冲突的方法就是当资源被一个任务使用的时候,在其加上。第一个访问某项资源的任务必须锁定这项资源。

基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。因为锁语句产生了一种相排斥的效果,这种机制常常称为互斥量(mutex)

Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务执行到被sychronized保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。

共享资源一般是以对象形式存在的内存片段,也可以是文件。输入/输出端口,或者是打印机。要控制对共享资源的访问,可以先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized

注意,在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域,这样就会产生冲突。

一个任务可以多次获取对象的锁。

针对每个类,也有一个锁,synchronized static方法可以在类的 范围内防止对static数据的并发访问。

同步规则: 如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。

如果你在类超过一个方法在处理临界数据,那么你必须同步所有的相关的方法。如果你只同步一个方法,其他方法就会随意的忽略这个对象所,并可以在无任何惩罚的情况下被调用。
重要的一点:每个访问临界共享资源的方法都必须被同步,否则它们就不会正确地工作。

同步控制EvenGenerator

//使用synchronized关键词同步
public class SynchronizedEvenGenerator extends IntGenerator {

    private int currentEvenValue = 0 ;

    public synchronized int next(){
        ++ currentEvenValue;
        Thread.yield();
        ++ currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args){
        EvenChecker.test(new SynchronizedEvenGenerator());
    }
}

使用显式的Lock对象
Java SE5包含locks中的显式的互斥机制。Lock对象必须被显式的创建,锁定和释放。与内建锁相比,缺乏优雅性。但是对于解决某些类型的问题来说,它更加灵活。

public class MutexEvenGenerator extends IntGenerator {
  private int currentEvenValue = 0;
  private Lock lock = new ReentrantLock();//declare
  public int next() {
    lock.lock();//lock
    try {
      ++currentEvenValue;
      Thread.yield(); // Cause failure faster
      ++currentEvenValue;
      return currentEvenValue;// return语句一定要在try块中出现,以确保unlock不会过早发生,从而把数据暴露给第二个任务。
    } finally {
      lock.unlock();//unlock
    }
  }
  public static void main(String[] args) {
    EvenChecker.test(new MutexEvenGenerator());
  }
} ///:~

try-finally所需代码比synchronized关键字要多,但是这也代表了显式的Lock对象的优点之一。如果synchronized失败了,那么就会抛出一个异常。但是你没有机会去做任何清理工作,以维护系统时其处于良好状态。有了显式的Lock对象,你就可以使用finally子句将系统维护在正确的状态了。

大体上,当你使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的Lock对象。例如,用Synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,你必须使用concurrent类库。

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);//尝试2秒获取锁,如果获取不了就做其他事。
        } 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(); // True -- lock is available
        al.timed();   // True -- lock is available
        // Now create a separate task to grab the lock:
        new Thread() {
            {
                setDaemon(true);
            }

            public void run() {
                al.lock.lock();
                System.out.println("acquired");
            }
        }.start();
        Thread.yield(); // Give the 2nd task a chance
        al.untimed(); // False -- lock grabbed by task
        al.timed();   // False -- lock grabbed by task
    }
} /* Output:
tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
acquired
tryLock(): false
tryLock(2, TimeUnit.SECONDS): false
*///:~

3.3 原子性与易变性

原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前执行完毕。

原子性可以应用于除longdouble之外的所有基本类型之上的“简单操作”。对于这2个类型之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存。但是JVM可以将64位(long和double变量)的读取和写入当做两个分离的32位操作来执行,不同的任务可能看到不正确的结果。

当定义longdouble变量时,如果使用volatile关键字,就会获得(简单的复制与返回操作)原子性。

volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所以的读操作就都可以看到这个修改。即使使用了本地缓存,情况也确实如此,volatile域会立刻被写入主存中,而读取操作就发生在主存中。

原子性易变性是不同的概念。在非volatile域上的原子操作不必刷新到主存中去,因此其他读取该域任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为volatile的。

使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域

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);
      }
    }
  }
} /* Output: (Sample)
191583767
*///:~

另一个更简单的例子:

public class SerialNumberGenerator {
    private static volatile int serialNumber = 0;
    public static int nextSerialNumber() {
        return serialNumber++; // Not thread-safe
    }
} ///:~

java的递增不是原子性的,涉及一个读操作和一个写操作,即使在这么简单的操作中,也为线程问题留下了空间。

验证代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// 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 final int SIZE = 10;
    private static CircularSet serials =
            new CircularSet(1000);
    private static ExecutorService exec =
            Executors.newCachedThreadPool();

    static class SerialChecker implements Runnable {
        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) throws Exception {
        for (int i = 0; i < SIZE; i++)
            exec.execute(new SerialChecker());
        // Stop after n seconds if there's an argument:
        if (args.length > 0) {
            TimeUnit.SECONDS.sleep(new Integer(args[0]));
            System.out.println("No duplicates detected");
            System.exit(0);
        }
    }
} /* Output: (Sample)
Duplicate: 8468656
*///:~

为了解决这个问题,需要在nextSerialNumber()方法钱添加synchronized关键字。

对基本类型的读取和赋值操作被认为是安全的原子性操作。当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问它们。对这个问题作出假设是棘手和危险的,明智的做法就是遵循Brian的同步规则(不要用原子性来代替同步)。

3.4 原子类

Java SE5引入了诸如AtomicIntegerAtomicLongAtomicReference等特殊的原子性变量类,它们提供了夏沫这种形式的原子性条件更新操作:

boolean compareAndSet(expectedValue,updateValue)

这些类被调整为可以使用在某些现代处理器上的可获得的,并且是在机器级别上的原子性,因此在使用它们时,通常不需要担心。

对于常规变成来说,它们很少会派上用场,但是在涉及性能调优时,它们就大有用武之地。

重写AtomicityTest:

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); // Terminate after 5 seconds
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicIntegerTest ait = new AtomicIntegerTest();
        exec.execute(ait);
        while (true) {
            int val = ait.getValue();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
} ///:~

用AtomicInteger重写MutexEvenGenerator

public class AtomicEvenGenerator extends IntGenerator {
    private AtomicInteger currentEvenValue =
            new AtomicInteger(0);

    public int next() {
        return currentEvenValue.addAndGet(2);//修改
    }

    public static void main(String[] args) {
        EvenChecker.test(new AtomicEvenGenerator());
    }
} ///:~

3.5 临界区

有时,只是希望防止多个线程同时访问方法内部的部分代码而不是访问整个方法。通过这种方式分离出来的代码段被称为临界区(critical section),它也使用synchronized关键字建立。
这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:

synchronized(syncObject){
    //this code can be accessed by only one task at a time
}

这也被称为同步控制块;在进入此段代码前,必须得到syncObject对象的锁。
例子:

class Pair {//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(50);
        } catch (InterruptedException ignore) {
        }
    }
    public abstract void increment();
}

class PairManager1 extends PairManager {//整个方法受到同步控制
    @Override
    public  synchronized void increment() {
        p.incrementX();
        p.incrementY();
        store(getPair());
    }
}

class PairManager2 extends PairManager {//使用同步控制块进行控制
    @Override
    public void increment() {
        Pair temp;
        synchronized (this) {
            p.incrementX();
            p.incrementY();
            temp = getPair();
        }
        store(temp);
    }
}

class PairManipulator implements Runnable {
//用来测试PairManager的increment方法
    private PairManager pm;

    public PairManipulator(PairManager pm) {
        this.pm = pm;
    }

    @Override
    public void run() {
        while (true)
            pm.increment();
    }

    public String toString(){
        return "Pare :" + pm.getPair() + " checkCounter = " + pm.checkCounter.get();
    }

}

class PairChecker implements Runnable {
//用来记录跟踪允许测试的频率
    private PairManager pm;

    public PairChecker(PairManager pm) {
        this.pm = pm;
    }

    @Override
    public void run() {
        while (true) {
            pm.checkCounter.incrementAndGet();
            pm.getPair().checkState();
        }
    }
}

public class CriticalSection {
    static void testApproaches(PairManager pman1, PairManager pman2) {
        ExecutorService exec = Executors.newCachedThreadPool();
        PairManipulator
                pm1 = new PairManipulator(pman1),
                pm2 = new PairManipulator(pman2);
        PairChecker
                pcheck1 = new PairChecker(pman1),
                pcheck2 = new PairChecker(pman2);
        exec.execute(pm1);
        exec.execute(pm2);
        exec.execute(pcheck1);
        exec.execute(pcheck2);
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            System.out.println("Sleep interrupted");
        }
        System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
        System.exit(0);
    }

    ;

    public static void main(String[] args) {
        PairManager pman1 = new PairManager1(),
                pman2 = new PairManager2();
        testApproaches(pman1, pman2);
    }
}
/* output : 
pm1: Pare :x : 14, y: 14 checkCounter = 48
pm2: Pare :x : 14, y: 14 checkCounter = 6657045
*/

采用同步块进行同步的pm2不加锁的时间更长。
所以多使用同步控制块而不是对整个方法进行同步,使得其他线程能更多的访问(保证安全的前提下)。

  • 使用显式的Lock对象来创建临界区
// Synchronize the entire method:
class ExplicitPairManager1 extends PairManager {
    private Lock lock = new ReentrantLock();

    public synchronized void increment() {
        lock.lock();
        try {
            p.incrementX();
            p.incrementY();
            store(getPair());
        } finally {
            lock.unlock();
        }
    }
}

// Use a critical section:
class ExplicitPairManager2 extends PairManager {
    private Lock lock = new ReentrantLock();

    public void increment() {
        Pair temp;
        lock.lock();
        try {
            p.incrementX();
            p.incrementY();
            temp = getPair();
        } finally {
            lock.unlock();
        }
        store(temp);
    }
}

public class ExplicitCriticalSection {
    public static void main(String[] args) throws Exception {
        PairManager
                pman1 = new ExplicitPairManager1(),
                pman2 = new ExplicitPairManager2();
        CriticalSection.testApproaches(pman1, pman2);
    }
} /* Output: (Sample)
pm1: Pair: x: 15, y: 15 checkCounter = 174035
pm2: Pair: x: 16, y: 16 checkCounter = 2608588
*///:~

个人尝试的时候出现了线程不安全的情况,不知何故。

3.6 在其他对象上同步

synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this)。

有时必须在另一个对象上同步,如果要这么做,必须确保所有相关任务都是在同一个对象上同步的。下面的示例演示了两个任务可以同时进入同一个对象,只要这个对象上的方法是在不同的锁上同步:

class DualSynch {
    private Object syncObject = new Object();

    public synchronized void f() {
        for (int i = 0; i < 5; i++) {
            print("f()");
            Thread.yield();
        }
    }

    public void g() {
        synchronized (syncObject) {
            for (int i = 0; i < 5; i++) {
                print("g()");
                Thread.yield();
            }
        }
    }
}

public class SyncObject {
    public static void main(String[] args) {
        final DualSynch ds = new DualSynch();
        new Thread() {
            public void run() {
                ds.f();
            }
        }.start();
        ds.g();
    }
} /* Output: (Sample)
g()
f()
g()
f()
g()
f()
g()
f()
g()
f()
*///:~

此代码没有导致任何堵塞。

3.7 线程的本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机智,可以为使用相同变量的每个不同的线程创建不同的存储。因此,如果你有5个线程都要使用变量x所表示的对象,那线程本地存储就会生成5个用于x的不同的存储块。主要是,它们使得你可以将状态与线程关联起来。

创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现,如下:

package concurrency;//: concurrency/ThreadLocalVariableHolder.java
// Automatically giving each thread its own storage.

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Accessor implements Runnable {
    private final int id;

    public Accessor(int idn) {
        id = idn;
    }

    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            ThreadLocalVariableHolder.increment();
            System.out.println(this);
            Thread.yield();
        }
    }

    public String toString() {
        return "#" + id + ": " +
                ThreadLocalVariableHolder.get();
    }
}

public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value =
            new ThreadLocal<Integer>() {
                private Random rand = new Random(47);

                protected synchronized Integer initialValue() {
                    return rand.nextInt(10000);
                }
            };

    public static void increment() {
        value.set(value.get() + 1);
    }

    public static int get() {
        return value.get();
    }

    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++)
            exec.execute(new Accessor(i));
        TimeUnit.SECONDS.sleep(3);  // Run for a while
        exec.shutdownNow();         // All Accessors will quit
    }
} /* Output: (Sample)
#0: 9259
#1: 556
#2: 6694
#3: 1862
#4: 962
#0: 9260
#1: 557
#2: 6695
#3: 1863
#4: 963
...
*///:~

ThreadLocal对象通常当做静态域存储。在创建ThreadLocal时,你只能通过get()和set()方法来访问该对象的内容,其中,get()方法将返回与线程相关联的对象的副本,而set()会将参数插入到为其线程存储的对象中,并返回存储中原有的对象。

注意,get()方法和increment()方法都不是synchronized的,因为ThreadLocal保证不会出现竞争条件。

当运行这个程序的时候,我可以看到每个单独的线程都被分配了自己存储,因为它们每个都需要跟踪自己的计数值,畸变只有一个ThreadLocalVariableHolder对象。

4 终结任务

在某种情况下,任务需要突然的终止,本章学习到有关这种终止的各类话题和问题

4.1 装饰性花园例子

在这个仿真程序中,花园委员会希望了解每天通过多个大门进入公园的总人数。每个大门都有一个十字转门或者其他形式的计数器,并且任何一个十字转门计数器递增时,就表示公园的总人数计数值也会递增。
总计数类:

class Count {
    private int count = 0;
    private Random rand = new Random(47);

    // Remove the synchronized keyword to see counting fail:
    public synchronized int increment() {
        int temp = count;
        if (rand.nextBoolean()) // Yield half the time
            Thread.yield();//如果该方法去除同步关键词,yield()方法可以让同步问题更快发生。
        return (count = ++temp);
    }

    public synchronized int value() {
        return count;
    }
}

大门类:

class Entrance implements Runnable {
    private static Count count = new Count();
    private static List<Entrance> entrances =
            new ArrayList<Entrance>();
    private int number = 0;
    // Doesn't need synchronization to read:
    private final int id;
    //volatile布尔标志,只会被读取和赋值(不会与其他域组一起被读取)
    private static volatile boolean canceled = false;

    // Atomic operation on a volatile field:
    public static void cancel() {
        canceled = true;
    }

    public Entrance(int id) {
        this.id = id;
        // Keep this task in a list. Also prevents
        // garbage collection of dead tasks:
        entrances.add(this);
    }

    //递增number与count对象,然后休眠100毫秒
    public void run() {
        while (!canceled) {
            synchronized (this) {
                ++number;
            }
            print(this + " Total: " + count.increment());
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                print("sleep interrupted");
            }
        }
        print("Stopping " + this);
    }

    public synchronized int getValue() {
        return number;
    }

    public String toString() {
        return "Entrance " + id + ": " + getValue();
    }

    public static int getTotalCount() {
        return count.value();
    }

    public static int sumEntrances() {
        int sum = 0;
        for (Entrance entrance : entrances)
            sum += entrance.getValue();
        return sum;
    }
}

主函数与执行结果:

public class OrnamentalGarden {
    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++)
            exec.execute(new Entrance(i));
        // Run for a while, then stop and collect the data: 三秒后main向Entrance发生cancel信息
        TimeUnit.SECONDS.sleep(3);
        Entrance.cancel();
        exec.shutdown();
        /*exec.awaitTermination方法等待每个任务结束*/
        if (!exec.awaitTermination(250, TimeUnit.MILLISECONDS))
            print("Some tasks were not terminated!");
        print("Total: " + Entrance.getTotalCount());
        print("Sum of Entrances: " + Entrance.sumEntrances());
    }
} /* Output: (Sample)
Entrance 0: 1 Total: 1
Entrance 2: 1 Total: 3
Entrance 1: 1 Total: 2
Entrance 4: 1 Total: 5
...
Entrance 3: 29 Total: 143
Entrance 0: 29 Total: 144
Stopping Entrance 2: 30
Stopping Entrance 1: 30
Stopping Entrance 0: 30
Stopping Entrance 3: 30
Stopping Entrance 4: 30
Total: 150
Sum of Entrances: 150
*///:~

ExecutorService.awaitTermination()等待每个任务结束,如果在参数时间内全部结束,则返回true,否则返回false。

4.2 在阻塞时终结

前面示例中run调用了sleep()导致任务从执行状态变为阻塞状态,有时我们必须终止被阻塞的任务。

线程状态

  • 新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。此刻线程已经有资格获取CPU时间了,之后调度器将把这个线程转变为可运行状态(就绪)或阻塞状态。
  • 就绪(Runnable):在这种状态下,只要调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行。
  • 阻塞(Blocked):线程能够运行,但由某个条件组织它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,才可能执行操作。
  • 死亡(Dead):处于死亡或者终止状态的线程将不再可调度的,并且再也不会得到CPU时间,它的任务已经结束。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断,你将要看到这一点。

进入阻塞状态
一个任务进入阻塞状态,可能有如下原因:

  • 通过调用sleep(time)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。
  • 通过调用wait()使线程挂起。直到线程得到了notify()或者notifyAll()消息(在Java SE5中可用等价的signal()或signalAll()消息)
  • 任务在等待某个输入/输出完成
  • 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用。

在较早的代码中可以看到suspend()resume()来阻塞和唤醒线程,但这些方法以及被现代Java废除。(因为可能导致死锁)

stop()方法也已经被废止了,因为它不释放线程获得的锁,如果线程处于不一致状态,其他任务可以在这种状态下浏览并修改它们。

4.3 中断

当打断被阻塞的任务时,可能需要清理资源。正因为这一点,在任务的run()方法中间打断,更像是抛出的异常,因此在Java线程中的这种类型的异常中断中用到了异常,必须仔细考虑代码的执行路径,并仔细编写catch子句以正确清除所有事物。

Thread类包含interrupt()方法,因此可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,如果试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位。

新的concurrent类库尽量避免对Thread对象的直接操作,所以用Executor来执行操作。如果在Executor调用shutDownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。如果只希望中断某一个单一的任务,那需要通过submit()而不是commit()来启动任务。submit()将返回一个泛型Future<?>,其中有一个未修饰的参数,因为你永远都不会在其上调用get() ——持有这种Future的关键在于你可以在其上调用cancel(),并因此可以用它来关闭特定的任务。下面示例:

sleep()引起的堵塞:

class SleepBlocked implements Runnable {
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(100);
        } catch (InterruptedException e) {
            print("InterruptedException");
        }
        print("Exiting SleepBlocked.run()");
    }
}

等待输入/输出引起的堵塞:

class IOBlocked implements Runnable {
    private InputStream in;

    public IOBlocked(InputStream is) {
        in = is;
    }

    public void run() {
        try {
            print("Waiting for read():");
            in.read();
        } catch (IOException e) {
            if (Thread.currentThread().isInterrupted()) {
                print("Interrupted from blocked I/O");
            } else {
                throw new RuntimeException(e);
            }
        }
        print("Exiting IOBlocked.run()");
    }
}

因死锁引起的堵塞

class SynchronizedBlocked implements Runnable {
    public synchronized void f() {
        while (true) // Never releases lock
            Thread.yield();
    }

    public SynchronizedBlocked() {
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
    }

    public void run() {
        print("Trying to call f()");
        f();
        print("Exiting SynchronizedBlocked.run()");
    }
}

测试类即输出:

public class Interrupting {
    private static ExecutorService exec =
            Executors.newCachedThreadPool();

    static void test(Runnable r) throws InterruptedException {
        Future<?> f = exec.submit(r);
        TimeUnit.MILLISECONDS.sleep(100);
        print("Interrupting " + r.getClass().getName());
        f.cancel(true); // Interrupts if running
        print("Interrupt sent to " + r.getClass().getName());
    }

    public static void main(String[] args) throws Exception {
        test(new SleepBlocked());
        test(new IOBlocked(System.in));
        test(new SynchronizedBlocked());
        TimeUnit.SECONDS.sleep(3);
        print("Aborting with System.exit(0)");
        System.exit(0); // ... since last 2 interrupts failed
    }
} /* Output: (95% match)
Interrupting SleepBlocked
InterruptedException
Exiting SleepBlocked.run()
Interrupt sent to SleepBlocked
Waiting for read():
Interrupting IOBlocked
Interrupt sent to IOBlocked
Trying to call f()
Interrupting SynchronizedBlocked
Interrupt sent to SynchronizedBlocked
Aborting with System.exit(0)
*///:~

通过这个输出可以发现,Sleep()是可中断堵塞,而IOSynchronized都是不可中断堵塞,从代码也可以看出,后2个不可中断堵塞都不需要InterruptedException处理器。

I/O锁住多线程程序对于Web程序是很严重的,对于这个问题,有一个笨拙但有效的解决方案,即在关闭任务在其上发生阻塞的底层资源(如下例的输入源):

public class CloseResource {
    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        ServerSocket server = new ServerSocket(8080);
        InputStream socketInput =
                new Socket("localhost", 8080).getInputStream();
        exec.execute(new IOBlocked(socketInput));
        exec.execute(new IOBlocked(System.in));
        TimeUnit.MILLISECONDS.sleep(100);
        print("Shutting down all threads");
        exec.shutdownNow();
        TimeUnit.SECONDS.sleep(1);
        print("Closing " + socketInput.getClass().getName());
        socketInput.close(); // Releases blocked thread
        TimeUnit.SECONDS.sleep(1);
        print("Closing " + System.in.getClass().getName());
        System.in.close(); // Releases blocked thread
    }
} /* Output: (85% match)
Waiting for read():
Waiting for read():
Shutting down all threads
Closing java.net.SocketInputStream
Interrupted from blocked I/O
Exiting IOBlocked.run()
Closing java.io.BufferedInputStream
Exiting IOBlocked.run()
*///:~

在nio类提供了更人性化的I/O中断,被阻塞的通道会自动地响应中断:

class NIOBlocked implements Runnable {
    private final SocketChannel sc;

    public NIOBlocked(SocketChannel sc) {
        this.sc = sc;
    }

    public void run() {
        try {
            print("Waiting for read() in " + this);
            sc.read(ByteBuffer.allocate(1));
        } catch (ClosedByInterruptException e) {
            print("ClosedByInterruptException");
        } catch (AsynchronousCloseException e) {
            print("AsynchronousCloseException");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        print("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 isa =
                new InetSocketAddress("localhost", 8080);
        SocketChannel sc1 = SocketChannel.open(isa);
        SocketChannel sc2 = SocketChannel.open(isa);
        Future<?> f = exec.submit(new NIOBlocked(sc1));
        exec.execute(new NIOBlocked(sc2));
        exec.shutdown();
        TimeUnit.SECONDS.sleep(1);
        // Produce an interrupt via cancel:
        f.cancel(true);
        TimeUnit.SECONDS.sleep(1);
        // Release the block by closing the channel:
        sc2.close();
    }
} /* Output: (Sample)
Waiting for read() in NIOBlocked@7a84e4
Waiting for read() in NIOBlocked@15c7850
ClosedByInterruptException
Exiting NIOBlocked.run() NIOBlocked@15c7850
AsynchronousCloseException
Exiting NIOBlocked.run() NIOBlocked@7a84e4
*///:~

被互斥阻塞
下面这个例子显示同一个互斥如何被同一个任务多次获得:

public class MultiLock {
    public synchronized void f1(int count) {
        if (count-- > 0) {
            print("f1() calling f2() with count " + count);
            f2(count);
        }
    }

    public synchronized void f2(int count) {
        if (count-- > 0) {
            print("f2() calling f1() with count " + count);
            f1(count);
        }
    }

    public static void main(String[] args) throws Exception {
        final MultiLock multiLock = new MultiLock();
        new Thread() {
            public void run() {
                multiLock.f1(10);
            }
        }.start();
    }
} /* Output:
f1() calling f2() with count 9
f2() calling f1() with count 8
f1() calling f2() with count 7
f2() calling f1() with count 6
f1() calling f2() with count 5
f2() calling f1() with count 4
f1() calling f2() with count 3
f2() calling f1() with count 2
f1() calling f2() with count 1
f2() calling f1() with count 0
*///:~

看上去像是会因为互斥导致死锁,但结果不是这样。由于这个任务第一次调用f1()方法的时候就获得了multiLock对象的锁,因此能再对f2()调用中再次获得这个对象锁。一个任务应能够调用在同一个对象中的其他synchronized方法,因这个任务已经持有锁。

Java SE5并发库添加了一个特性:

ReentrantLock上阻塞的任务具备可以被中断的能力,这与在synchronized方法或临界区上阻塞的任务完全不同。

例子:
造成死锁的类:

class BlockedMutex {
    private Lock lock = new ReentrantLock();

    public BlockedMutex() {
        // Acquire it right away, to demonstrate interruption
        // of a task blocked on a ReentrantLock:
        lock.lock();
    }

    public void f() {
        try {
            // This will never be available to a second task
            lock.lockInterruptibly(); // Special call
            print("lock acquired in f()");
        } catch (InterruptedException e) {
            print("Interrupted from lock acquisition in f()");
        }
    }
}

任务:

class Blocked2 implements Runnable {
    BlockedMutex blocked = new BlockedMutex();

    public void run() {
        print("Waiting for f() in BlockedMutex");
        blocked.f();
        print("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();
    }
} /* Output:
Waiting for f() in BlockedMutex
Issuing t.interrupt()
Interrupted from lock acquisition in f()
Broken out of blocked call
*///:~

4.4 检查中断

在线程上调用interrupt()时,唯一发生作用的时刻就是任务要进入阻塞操作中,或者已经在阻塞操作的内部。如果run()没有机会进入阻塞时,就需要第二种方式来退出。

这种机会是由中断状态来表示,其状态可以通过调用interrupt()来设置。

可以通过调用interrupted()来检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断这个问题通知你两次,你可以由单一的InterruptedException或者单一的成功的Thread.interrupted()测试来得到这种通知。如果想要再次检查以了解是否被中断,则可以在调用Thread.interrupted()时将结果存储起来。

class NeedsCleanup {
    private final int id;

    public NeedsCleanup(int ident) {
        id = ident;
        print("NeedsCleanup " + id);
    }

    public void cleanup() {
        print("Cleaning up " + id);
    }
}

class Blocked3 implements Runnable {
    private volatile double d = 0.0;

    public void run() {
        try {
            while (!Thread.interrupted()) {
                // point1
                NeedsCleanup n1 = new NeedsCleanup(1);
                // Start try-finally immediately after definition
                // of n1, to guarantee proper cleanup of n1:
                try {
                    print("Sleeping");
                    TimeUnit.SECONDS.sleep(1);
                    // point2
                    NeedsCleanup n2 = new NeedsCleanup(2);
                    // Guarantee proper cleanup of n2:
                    try {
                        print("Calculating");
                        // A time-consuming, non-blocking operation:
                        for (int i = 1; i < 2500000; i++)
                            d = d + (Math.PI + Math.E) / d;
                        print("Finished time-consuming operation");
                    } finally {
                        n2.cleanup();
                    }
                } finally {
                    n1.cleanup();
                }
            }
            print("Exiting via while() test");
        } catch (InterruptedException e) {
            print("Exiting via InterruptedException");
        }
    }
}

public class InterruptingIdiom {
    public static void main(String[] args) throws Exception {
//        if (args.length != 1) {
//            print("usage: java InterruptingIdiom delay-in-mS");
//            System.exit(1);
//        }
        Thread t = new Thread(new Blocked3());
        t.start();
        TimeUnit.SECONDS.sleep(5);
        t.interrupt();
    }
} /* Output: (Sample)
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
Finished time-consuming operation
Cleaning up 2
Cleaning up 1
NeedsCleanup 1
Sleeping
Cleaning up 1
Exiting via InterruptedException
*///:~

所有的任务run中创建的NeedsCleanup资源都必须在其后面紧跟try-finally子句,保证cleanup()方法总是会被调用。
需要注意的是,如果interrupt()发生在point2注释后(即非阻塞操作时),输出:

NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
Finished time-consuming operation
Cleaning up 2
Cleaning up 1
Exiting via while() test

即操作顺序是中断循环,销毁本地对象,从while退出。如果发生在point1与point2之间,即有阻塞情况时,则如例子中的输出一样,任务经由InterruptedException退出。

需要注意的是,设计用来响应interrupt()的类必须建立一种策略,来确保它保持一致的状态。这意味着所有需要清理的对象创建操作后面,都必须紧跟try-finally子句,无论从run()循环中如何退出,清理都会发生。

5 线程之间的协作

这一章学习多个任务如何一起工作去解决某个问题。

当任务协作时,关键问题是这些任务之间的握手。

为了实现这种握手,我们使用了相同的基础特性:互斥。在这种情况下,互斥能够确保只有一个任务可以响应某个信号,这样就可以根除任何可能的竞争条件。

这种握手可以通过Object的方法wait()和notify()来安全的实现。Java SE5的并发库还提供了具有await()和signal()方法的Condition对象。

5.1 wait()与notifyAll()

wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。因此wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或者notifyAll()发生时,这个任务才会被唤醒并去检查所产生的变化。因此,wait()提供了一种在任务之间对活动同步的方式。
wait(带毫秒参数)与sleep()、yield()的区别
- 在wait()期间对象锁是释放的。
- 可以通过notify(),notifyAll()或者令时间到期,从wait()中恢复执行。

wait(),notify()以及notifyAll()都是基类Object的一部分。
只有在同步控制块或同步控制方法里调用这3个方法,否则在运行时,会抛出IllegalMonitorStateException异常,即调用这些方法任务必须“拥有”对象的锁

例子:

synchronized(x){
    x.notifyAll();
}

这里有个简单的协作示例,通过2个任务(抛光以及涂蜡)来协作操作一辆车。

汽车类:

class Car {
    private boolean waxOn = false;

    public synchronized void waxed() {
        waxOn = true; // Ready to buff
        notifyAll();
    }

    public synchronized void buffed() {
        waxOn = false; // Ready for another coat of wax
        notifyAll();
    }

    public synchronized void waitForWaxing()
            throws InterruptedException {
        while (waxOn == false)//注意使用的是while
            wait();
    }

    public synchronized void waitForBuffing()
            throws InterruptedException {
        while (waxOn == true)//注意使用的是while
            wait();
    }
}

涂蜡任务:

class WaxOn implements Runnable {
    private Car car;

    public WaxOn(Car c) {
        car = c;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                printnb("Wax On! ");
                TimeUnit.MILLISECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }
        } catch (InterruptedException e) {
            print("Exiting via interrupt");
        }
        print("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();
                printnb("Wax Off! ");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();
            }
        } catch (InterruptedException e) {
            print("Exiting via interrupt");
        }
        print("Ending Wax Off task");
    }
}

运行与结果:

public class WaxOMatic2 {
    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();
    }
} /* Output: (90% match)
Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Wax Off! Wax On! Exiting via interrupt
Ending Wax Off task
Exiting via interrupt
Ending Wax On task
*///:~

注意一定要用一个检查条件的while()来包围wait(),这很重要,因为:

  • 你可能有多个任务出于相同的原因在等待同一个锁,而第一个唤醒任务可能会改变这种情况。如果属于这种情况,那么这个任务应该被再次刮起,直至其感兴趣的条件发生变化。
  • 在这个任务从其wait()中被唤醒的时刻,有可能会有某个其他的任务已经做出了改变,从而使得这个任务在此时不能执行。
  • 也有可能某些任务处于不同的原因在等待你的对象上的锁。在这种情况下,需要检查是否已经由正确的原因唤醒。

错失的信号

当两个线程使用notify()/wait()或notifyAll()/wait()进行协作时,有可能会错过某个信号。

以下情况下会因错失信号产生死锁:
T1:

synchronized(shareMonitor){
    shareMonitor.notify();
}

T2:

while(someCondition){
synchronized(sharedMonitor){
    shareMonitor.wait();
    }
}

T2可能意识不到条件已发生变化而继续wait从而死锁,修改T2:

synchronized(sharedMonitor){
  while(someCondition)
    shareMonitor.wait();
}

5.2 notify()与notifyAll()

使用notify()而不是notifyAll()是一种优化。使用notify()时,众多的等待的任务只会有一个被唤醒。
当只有一个任务会出于wait()状态时,他们是等价的。
如果希望使用notify(),必须保证被唤醒的任务是恰当的任务、所有任务必须等待相同的条件、并且条件变化时只会有一个任务能够从中受益、这些限制对可能存在的子类都必须总是起作用。如果这些条件不满足就必须使用notifyAll()而不是notify()。

java notifyAll()的描述“唤醒所有正在等待的任务”是不准确的,notifyAll()因某个特定的锁被调用时,只会唤醒所有等待这个锁的任务。

5.3 生产者与消费者

考虑一个饭店,有一个厨师和一个服务员。服务员必须等待厨师准备好膳食,当厨师准备好时,他会通知服务员,之后服务员上菜,然后返回继续等待。

食物:

class Meal {
    private final int orderNum;

    public Meal(int orderNum) {
        this.orderNum = orderNum;
    }

    public String toString() {
        return "Meal " + orderNum;
    }
}

服务员(消费者)

class WaitPerson implements Runnable {
    private Restaurant restaurant;

    public WaitPerson(Restaurant r) {
        restaurant = r;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                synchronized (this) {
                    while (restaurant.meal == null)
                        wait(); // ... for the chef to produce a meal
                }
                print("Waitperson got " + restaurant.meal);
                synchronized (restaurant.chef) {
                    restaurant.meal = null;
                    restaurant.chef.notifyAll(); // Ready for another
                }
            }
        } catch (InterruptedException e) {
            print("WaitPerson interrupted");
        }
    }
}

厨师(生产者)

class Chef implements Runnable {
    private Restaurant restaurant;
    private int count = 0;

    public Chef(Restaurant r) {
        restaurant = r;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                synchronized (this) {
                    while (restaurant.meal != null)
                        wait(); // ... for the meal to be taken
                }
                if (++count == 10) {
                    print("Out of food, closing");
                    restaurant.exec.shutdownNow();
                }
                printnb("Order up! ");
                synchronized (restaurant.waitPerson) {
                    restaurant.meal = new Meal(count);
                    restaurant.waitPerson.notifyAll();
                }
                TimeUnit.MILLISECONDS.sleep(100);//如果没有sleep引起的堵塞,该程序会从while判断退出
            }
        } catch (InterruptedException e) {
            print("Chef interrupted");
        }
    }
}

餐馆 (主函数与输出):

public class Restaurant {
    Meal meal;
    ExecutorService exec = Executors.newCachedThreadPool();
    WaitPerson waitPerson = new WaitPerson(this);
    Chef chef = new Chef(this);

    public Restaurant() {
        exec.execute(chef);
        exec.execute(waitPerson);
    }

    public static void main(String[] args) {
        new Restaurant();
    }
} /* Output:
Order up! Waitperson got Meal 1
Order up! Waitperson got Meal 2
Order up! Waitperson got Meal 3
Order up! Waitperson got Meal 4
Order up! Waitperson got Meal 5
Order up! Waitperson got Meal 6
Order up! Waitperson got Meal 7
Order up! Waitperson got Meal 8
Order up! Waitperson got Meal 9
Out of food, closing
WaitPerson interrupted
Order up! Chef interrupted
*///:~

使用显示的LockCondition对象

Java SE5提供了额外的显示工具。使用互斥并且允许任务挂起的基本类是Condition,可以通过在Condition上调用await()来挂起一个任务。当外界条件变化时可以通过signal()来通知这个任务,从而唤醒一个任务,或者调用signalAll()来唤醒所有Condition上所有被其自身挂起的任务。
修改后的例子:

class Car {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean waxOn = false;//表示涂蜡-抛光状态

    public void waxed() {//涂蜡
        lock.lock();
        try {
            waxOn = true; // Ready to buff
            condition.signalAll();//协作操作
        } finally {
            lock.unlock();
        }
    }

    public void buffed() {//抛光
        lock.lock();
        try {
            waxOn = false; // Ready for another coat of wax
            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();
        }
    }
}

注意和之前不同的地方,所有对lock的调用都紧跟try-finally子句,用来保证所有情况下都可以释放锁。
另这个解决方案比之前的更加复杂,而且这种复杂性并没有带来更多的收获,所以LockCondition对象只有在更加困难的多线程问题中才是必须的。

生产者-消费者队列

在许多情况下,使用更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时候都只允许一个任务插入或删除元素。
BlockingQueue接口中提供了这个队列,该接口有大量的标准实现。通常可以用LinkedBlockingQueue(任意数量),还可以用ArrayBlockingQueue(有限数量)、SynchronousQueue(单个)。
如果消费者任务试图从队列中获取对象,而该队列此时为空,那么这些队列会挂起消费者任务,当有更多元素可用时恢复消费者任务。阻塞队列可以解决非常大量的问题,与之前的方式相比,简单可靠很多。

关键代码:

class LiftOffRunner implements Runnable {
    private BlockingQueue<LiftOff> rockets;

    public LiftOffRunner(BlockingQueue<LiftOff> queue) {
        rockets = queue;
    }

    public void add(LiftOff lo) {
        try {
            rockets.put(lo);
        } catch (InterruptedException e) {
            print("Interrupted during put()");
        }
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                LiftOff rocket = rockets.take();
                rocket.run(); // Use this thread
            }
        } catch (InterruptedException e) {
            print("Waking from take()");
        }
        print("Exiting LiftOffRunner");
    }
}

吐司BlockingQueue示例

一个机器具有3个任务:一个制作吐司、一个给吐司抹黄油,另一个在抹过黄油的吐司上涂果酱。

吐司类:

class Toast {
    public enum Status {DRY, BUTTERED, JAMMED}

    private Status status = Status.DRY;
    private final int id;

    public Toast(int idn) {
        id = idn;
    }

    public void butter() {
        status = Status.BUTTERED;
    }

    public void jam() {
        status = Status.JAMMED;
    }

    public Status getStatus() {
        return status;
    }

    public int getId() {
        return id;
    }

    public String toString() {
        return "Toast " + id + ": " + status;
    }
}

同步队列:

class ToastQueue extends LinkedBlockingQueue<Toast> {
}

制作任务:

class Toaster implements Runnable {
    private ToastQueue toastQueue;
    private int count = 0;
    private Random rand = new Random(47);

    public Toaster(ToastQueue tq) {
        toastQueue = tq;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                TimeUnit.MILLISECONDS.sleep(
                        100 + rand.nextInt(500));
                // Make toast
                Toast t = new Toast(count++);
                print(t);
                // Insert into queue
                toastQueue.put(t);
            }
        } catch (InterruptedException e) {
            print("Toaster interrupted");
        }
        print("Toaster off");
    }
}

抹黄油任务:

class Butterer implements Runnable {
    private ToastQueue dryQueue, butteredQueue;

    public Butterer(ToastQueue dry, ToastQueue buttered) {
        dryQueue = dry;
        butteredQueue = buttered;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                // Blocks until next piece of toast is available:
                Toast t = dryQueue.take();
                t.butter();
                print(t);
                butteredQueue.put(t);
            }
        } catch (InterruptedException e) {
            print("Butterer interrupted");
        }
        print("Butterer off");
    }
}

涂果酱任务

// Apply jam to buttered toast:
class Jammer implements Runnable {
    private ToastQueue butteredQueue, finishedQueue;

    public Jammer(ToastQueue buttered, ToastQueue finished) {
        butteredQueue = buttered;
        finishedQueue = finished;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                // Blocks until next piece of toast is available:
                Toast t = butteredQueue.take();
                t.jam();
                print(t);
                finishedQueue.put(t);
            }
        } catch (InterruptedException e) {
            print("Jammer interrupted");
        }
        print("Jammer off");
    }
}

吃吐司任务

// Consume the toast:
class Eater implements Runnable {
    private ToastQueue finishedQueue;
    private int counter = 0;

    public Eater(ToastQueue finished) {
        finishedQueue = finished;
    }

    public void run() {
        try {
            while (!Thread.interrupted()) {
                // Blocks until next piece of toast is available:
                Toast t = finishedQueue.take();
                // Verify that the toast is coming in order,
                // and that all pieces are getting jammed:
                if (t.getId() != counter++ ||
                        t.getStatus() != Toast.Status.JAMMED) {
                    print(">>>> Error: " + t);
                    System.exit(1);
                } else
                    print("Chomp! " + t);
            }
        } catch (InterruptedException e) {
            print("Eater interrupted");
        }
        print("Eater off");
    }
}

主函数:

public class ToastOMatic {
    public static void main(String[] args) throws Exception {
        ToastQueue dryQueue = new ToastQueue(),
                butteredQueue = new ToastQueue(),
                finishedQueue = new ToastQueue();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new Toaster(dryQueue));
        exec.execute(new Butterer(dryQueue, butteredQueue));
        exec.execute(new Jammer(butteredQueue, finishedQueue));
        exec.execute(new Eater(finishedQueue));
        TimeUnit.SECONDS.sleep(5);
        exec.shutdownNow();
    }
}

相比之前的依赖任务握手来完成协作,这种方法要简单并且安全多了。

任务间使用管道进行输入/输出

通过输入/输出在线程间进行通信通常很有用。提供线程功能的类库以“管道”形式对线程间的输入/输出提供了支持。
他们在Java I/O库对应的是PipedWriterPipedReader。这个模型可以看成“生产者-消费者”问题的变体,这里管道是一个封装好的解决方案。
在引入BlockingQueue之前的Java版本,通过管道进行任务间的通信很常见

发生消息类:

class Sender implements Runnable {
    private Random rand = new Random(47);
    private PipedWriter out = new PipedWriter();

    public PipedWriter getPipedWriter() {
        return out;
    }

    public void run() {
        try {
            while (true)
                for (char c = 'A'; c <= 'z'; c++) {
                    out.write(c);
                    TimeUnit.MILLISECONDS.sleep(rand.nextInt(500));
                }
        } catch (IOException e) {
            print(e + " Sender write exception");
        } catch (InterruptedException e) {
            print(e + " Sender sleep interrupted");
        }
    }
}

接收类:

class Receiver implements Runnable {
    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:
                printnb("Read: " + (char) in.read() + ", ");
            }
        } catch (IOException e) {
            print(e + " Receiver read exception");
        }
    }
}

主函数与输出:

public class PipedIO {
    public static void main(String[] args) throws Exception {
        Sender sender = new Sender();
        Receiver receiver = new Receiver(sender);
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(sender);
        exec.execute(receiver);
        TimeUnit.SECONDS.sleep(4);
        exec.shutdownNow();
    }
} /* Output: (65% match)
Read: A, Read: B, Read: C, Read: D, Read: E, Read: F, Read: G, Read: H, Read: I, Read: J, Read: K, Read: L, Read: M, java.lang.InterruptedException: sleep interrupted Sender sleep interrupted
java.io.InterruptedIOException Receiver read exception
*///:~

功能代码很简单,需要注意的是:
sender和receiver是在main()中启动的,如果启动了一个没有构造完毕的对象,那在不同的平台会出现不同的行为,使用同步队列会使得程序更加健壮以及简单。
在shutdownNow()被调用时,可以看出pipedReader引起的死锁是可中断的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值