第10章 并发

66.同步访问共享的可变数据

synchronized保证了原子性和可见性,volatile只保证了可见性。

让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,只同步共享对象引用的动作。然后其他线程没有进一步的同步也可以读取对象,只要它没有再被修改。这种对象呗称作事实上不可变的effectively immutable。将这种对象引用从一个线程传递到其他的线程被称作安全发布safe publication。安全发布对象引用有许多中方法:可以将它保存在静态域中,作为类初始化的一部分;可以将它保存在volatile域、final域或者通过正常锁定访问的域中;或者可以将它放到并发的集合中。

简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败liveness failure或安全性失败safety failure。这样的失败是最难以调试的。它们可能是间歇性的,且与时间有关,程序的行为在不同的VM上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。

67.避免过度同步

CopyOnWriteArrayList通过重新拷贝真个底层数组,在这里实现所有的写操作。由于内部数组永不变动,因此迭代不需要锁定,速度也非常快。

如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁lock splitting、分离锁lock striping和非阻塞nonblocking并发控制。

简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般的讲,要尽量限制同步区域内部的工作量。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要过度同步来的更重要。

68.executor和task优先于线程

可以用executor service完成更多的事情,例如,可以等待完成一项特殊的任务,可以等待一个任务集合中的任何任务或者所有任务完成(利用invokeAny或者invokeAll方法),可以等待executor service优雅地完成终止(利用awaitTermination方法),可以在任务完成时逐个地获取这些任务的结果(利用ExecutorCompletionService)等等。

工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务task。任务有两种:Runnable及Callable(它与Runnable类似,但它会返回值)。执行任务的通用机制是executor service。从本质上讲,executor framework所做的工作是执行,犹如collection framework所做的工作室聚集aggregation一样。

executor framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor。timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果timer唯一的线程抛出未被捕获的异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅地从抛出未受检异常的任务中恢复。

69.并发工具优先于wait和notify

java.util.concurrent中更高级的工具分为三类:Executor Framework、并发集合Concurrent Collection以及同步器Synchronizer。

并发集合为标准的集合接口(如List、Queue、Map)提供了高性能的并发实现。

ConcurrentMap扩展了Map接口,并添加了几个方法,包括putIfAbsent(key, value),当键没有映射时会替它插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回null。

有些集合接口以及通过阻塞操作blocking operation进行了扩展,他们会一直等待(或者阻塞)到可以成功执行为止。例如BlockingQueue扩展了Queue接口,并添加了包括take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空,就等待。这样就允许将阻塞队列用于工作队列work queue,也称作生产者-消费者队列producer-consumer queue,一个或者多个生产者线程在工作队列中添加工作项目,并且当工作项目可用时,一个或者多个消费者线程则从工作队列中取出工作项目并处理。大多数ExecutorService实现(包括ThreadPoolExecutor)都使用BlockingQueue。

同步器Synchronizer是一些使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch和Semaphore。较不常用的是CyclicBarrier和Exchanger。

CountDownLatch是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountDownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。

public static long time(Executor executor, int concurrency, final Runnable action) throws InterruptedException {
        final CountDownLatch ready = new CountDownLatch(concurrency);
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch done = new CountDownLatch(concurrency);
        for (int i = 0; i < concurrency; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    ready.countDown(); // tell timer we're ready
                    try {
                        start.await(); // wait till peers are ready
                        action.run();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    } finally {
                        done.countDown(); // tell timer we're done
                    }
                }
            });
        }
        ready.await(); // wait for all workers to be ready
        long startNanos = System.nanoTime();
        start.countDown(); // and they are off, fire
        done.await(); // wait for all workers to finish
        return System.nanoTime() - startNanos;
    }

传递给time方法的executor必须允许创建至少与指定并发级别一样多的线程,否则这个测试就永远不会结束。这就是线程饥饿死锁thread starvation deadlock。如果工作线程捕捉到InterruptedException,就会利用习惯用法Thread.currentThread().interrupt()重新断言中断,并从它的run方法中返回。这样就允许executor在必要时处理中断,事实上也理当如此。

对于间歇式的定时,始终应该优先使用System.nanoTime,而不是System.currentTimeMills。System.nanoTime更加准确也更加精确,它不受系统的实时时钟的调整锁影响。

wait方法被用来使线程等待某个条件。它必须在同步区域内部被调用,下面是使用wait方法的标准模式:

synchronized (obj) {
            while(<condition does not hold>) {
                obj.wait(); // releases lock, and reacquires on wakeup
            }
            // preform action appropriate to condition
        }

伪唤醒 spurious wakeup,在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。

在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程等待之前,notify(或者notifyAll)方法已经被调用,则无法保证该线程将会从等待着苏醒过来。

在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系。

一般情况下,你应该优先使用notifyAll,而不是使用notify,如果使用notify,请一定要小心,以确保程序的活性。

70.线程安全性的文档化

线程安全性的几种级别:

  • 不可变的immutable,这个类的实例是不变的,所以,不需要外部的同步,这样的例子包括String、Long和BigInteger。
  • 无条件的线程安全,这个类的实例是可变的,但是这个类有足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步。其例子包括Random和ConcurrentHashMap。
  • 有条件的线程安全,除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。这样的例子包括Collections.synchronized包装返回的集合,他们的迭代器要求外部同步。
  • 非线程安全,这个类的实例是可变的,为了并发地使用它们,客户端必须利用自己选择的外部同步包围每个方法调用。这样的例子包括通用的集合实现,如ArrayList和HashMap。
  • 线程对立的。这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。

71.慎用延迟初始化

如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。这种模式(也称作initialize-on-demand holder class idiom)保证了类要到被用到的时候才会被初始化。

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
static FieldType getField() {
    return FieldHolder.field;
}

当getField方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化。VM在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。

如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式。两次检查域的值,第一次检查时没有锁定,看看这个域是否被初始化了;第二次检查时有锁定。只有当第二次检查时表明这个域没有被初始化,才会调用computeFieldValue方法对这个域进行初始化。因为如果域已经被初始化就不会有锁定,域被声明为volatile很重要。

private volatile FieldType field;
    FieldType getField() {
        FieldType result = field;
        if (result == null) {
            synchronized (this) {
                result = field;
                if (result == null) {
                    field = result = computeFieldValue();
                }
            }
        }
        return result;
    }

对于可以接受重复初始化的实例域,可以考虑使用单重检查模式。

private volatile FieldType field;
    FieldType getField() {
        FieldType result = field;
        if (result == null) {
            field = result = computeFieldValue();
        }
        return result;
    }

72.不要依赖于线程调度器

线程优先级是Java平台上最不可移植的特征了。

73.避免使用线程组

results matching ""

    No results matching ""