Effective Java(九)

九、并发

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

        同步的语义不仅包含互斥,还包含可见性,可见性保证了进入同步方法或同步代码块的每个线程,都看到由同一个保护的之前所有的修改效果。
        Java语言规范保证读/写一个变量是原子的,除非这个变量的类型是long或double。也即是说,读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改这个变量也是如此。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程是可见的,即它不保证可见性。因此,为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。

public class ThreadTest {
    private static boolean stopRequested; //原子操作

    public static void main(String[] args) throws Exception{
        Thread thread = new Thread (new Runnable() {
            public void run() {
                int i=0;
                while(!stopRequested) {
                    i++;
                }
            }
        });
        thread.start();
        Thread.sleep(1000);
        stopRequested = true;
    }
}

        上面这段代码中,由于boolean域的读和写操作都是原子操作,你可能期待这个程序运行大约1秒钟后,主线程将stopRequested设置为true,致使thread线程的循环终止。但事实上这个程序永远也不会停止:thread线程永远在循环。问题在于,thread的线程不能“看到”主线程对stopRequested所做的改变。

        修正这个问题的一种方式是同步访问stopRequest域:

public class ThreadTest {
    private static volatile boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }
    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args) throws Exception{
        Thread thread = new Thread (new Runnable() {
            public void run() {
                int i=0;
                while(!stopRequested()) {
                    i++;
                }
            }
        });
        thread.start();
        Thread.sleep(1000);
        requestStop();
    }
}

        倘若仅需要获得通信效果,不需要互斥访问,仅使用volatile修饰即可,它可保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值,也即是保证可见性

private static volatile boolean stopRequested;

        volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。

        使用volatile时必须小心,考虑下面的代码:

private static volatile int nextNumber = 0;
public static int generateNumber() {
    return nextNumber++;
}

        修正的办法是在方法的声明中增加synchronized修饰符或使用原子操作类AtomicLong,如:

private static final AtomicLong nextNumber = new AtomicLong();

public static long generateNumber() {
    return nextNumber.getAndIncrement();
}

2. 避免过度同步

        过度同步会导致性能降低、死锁,甚至不确定的行为

        在一个被同步的区域内部,不要调用设计成要被override的方法,或者是由客户端以函数对象的形式提供的方法。从包含该同步区域的类的角度来说,这些方法是外来的,这个类不知道该外来方法会做什么事情,也无法控制它,从同步区域中调用外来方法会导致异常、死锁、数据破坏。

        通常,应该在同步区域内做尽可能少的工作。

        过度同步将影响程序的性能,原因:

  • 程序将失去并行的机会
  • cpu需要确保每个核有一个一致的内存视图而导致延迟
  • 限制了JVM优化代码的能力

3. executor和task优先于线程

        在Java1.5后,java平台增加了Executor Framework,这是一个灵活的基于接口的任务执行工具。它创建了一个工作队列用于执行任务。如:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable {
    @Override
    public void run() {
        .....
    }
});
....
executor.shutdown(); //终止前允许执行以前提交的任务

        若想让多个线程来处理这个队列中的任务,可以使用

Executors.newCachedThreadPool()

        在引入Executor之前,要使用Thread去执行工作任务,Thread既是工作单元,又是执行机制。
        引入Executor后,将工作单元和执行机制分开了,现在关键的抽象是工作单元,也叫任务(Task),任务有两种:RunnableCallable。执行任务的通用机制是Executor。
        Executor和Task的框架结构为:

        

 

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

        直接使用waitnotify就像用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的功能。没有理由在新代码中使用wait和notify,即使有,也是极少的。如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait。一般情况下,你应该优先使用notifyAll,而不是使用notify。如果使用notify,请一定要小心,以确保程序的活性。

        Java1.5开始,java.util.concurrent中提供了更高级的并发工具来实现更好的并发控制,这些工具可分为三类:
        (1)Executor Framework
        (2)并发集合
        (3)同步器

        并发集合可分为阻塞非阻塞两类。

常见的非阻塞并发集合有

  • ConcurrentHashMap
  • ConcurrentSkipListMap
  • ConcurrentSkipListSet
  • ConcurrentLinkedQueue
  • ConcurrentLinkedDeque
  • CopyOnWriteArrayList
  • CopyOnWriteArraySet

        常见的并发阻塞队列有

  • ArrayBlockingQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue
  • DelayQueue
  • SynchronousQueue
  • LinkedTrasnsferQueue
  • LinkedBlockingDeque

        同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。有如下几种:

  • CountDownLatch
  • Semaphore
  • CyclicBarrier
  • Exchanger
  • Phaser

5. 线程安全性的文档化

        一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别
常见的安全性级别有:
(1)不可变的
        类的实例是不可变的,不需要外部同步。如:String、Long、BigInteger。
(2)无条件的线程安全
        类的实例是可变的,但这个类有着足够的内部同步,它的实例可以被并发使用,无需任何外部同步。如:Random、ConcurrentHashMap。
(3)有条件的线程安全
        除了有些方法需要外部同步外,此安全级别与无条件的线程安全相同。如:Collections.synchronized包装返回的集合,它们的迭代器iterator要求外部同步。
(4)非线程安全
        类的实例是可变的,为并发使用它们,客户必须利用自己选择的外部同步器包围每个方法调用(或者调用序列)。如非并发容器类ArrayList、HashMap等。
(5)线程对立的
        类不能安全地被多个线程并发使用,即使所有方法都被外部同步包围。

        在文档中描述一个有条件的线程安全类要特别小心,必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁。

        类的线程安全说明通常放在它的文档注释中,但是带有特殊线程安全属性的方法则应该在它们自己的文档注释中说明它们的属性。

6. 慎用延时初始化

        延迟初始化指的是延迟到需要域的值时才对它初始化的行为。如果永远不需要这个值,这个域就永远不会被初始化,这种方法适用于静态域,也适用于实例域。

        当有多个线程,延迟初始化是需要技巧的。如果两个或多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的bug。

//直接初始化
private final FieldType field = computeFieldValue();

//延迟初始化,必须同步
private FieldType field;
synchronized FieldType getField() {
    if(field == null) 
        field = computeFieldValue();
    return field;
}

        如果出于性能的考虑而需要对静态域使用延迟初始化,就是用lazy initialization holder class模式,这种模式保证了类被用到的时候才会被初始化:

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

static FieldType getField() {
    return FieldHolder.field;
}

        如果出于性能的考虑而需要对实例域使用延迟初始化,就是用双重检查模式(double check idiom)

private volatile FieldType field;

FieldType getField() {
    //result变量的作用是确保field只在已经被初始化的情况下读取一次
    //同不使用局部变量相比,可提升不少性能
    FieldType result = field;
    if (result == null) {
        synchronized(this) {
            if (result == null) {
                result = computeFieldValue();
            }
        }
    }
    return result;
}

        如果需要延迟初始化一个可以接受重复初始化的实例域,可以使用单重检查模式

private volatile FieldType field;

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

        延迟初始化就像一把双刃剑,它降低了初始化类或创建实例的开销,却增加了访问被延迟初始化的域的开销。最好的建议是:除非绝对必要,否则就不要这么做。       

7. 不要依赖线程调度器

        当有多个线程可以运行时,由线程调度器决定哪些线程将会运行,以及运行多长时间。这种调度策略是由OS决定的,且不同的OS策略可能完全不同,因此,编写良好的程序不应该依赖于策略的细节,以免影响程序的可移植性。
        要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量

        保持可运行线程数量尽可能少的主要方法是:让每个线程做有意义的工作,然后等待更多有意义的工作。如果线程没有在做有意义的工作,就不应该运行。线程不应该一直处于忙-等(busy-wait)的状态,即反复地检查一个共享对象,以等待某些事情发生。忙-等的做法会极大地增加处理器的负担,降低了同一机器上其他线程可以完成的有用工作量。

        如果某些线程无法获得足够的CPU时间,不要企图通过调用Thread.yield()来修正程序,它并不能保证什么。

8. 避免使用线程组

        线程组(ThreadGroup)的初衷是作为一种隔离applet的安全机制,但实际上,它从未完成设计之初预期的任何安全功能,并且ThreadGroup API非常脆弱,有各种缺陷,它们之所以没有被修正,是因为线程组本身已经过时了,根本没有修正的必要了。因此,在实际的开发中,要避免使用线程组。

        如果你正在设计的一个类需要处理线程的逻辑组,应该使用线程池executor

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值