《Effective Java》——学习笔记(异常&并发)

异常

第57条:只针对异常的情况才使用异常

异常应该只用于异常的情况下:它们永远不应该用于正常的控制流

设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常

第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常

Java程序设计语言提供了三种可抛出结构:受检的异常、运行时异常和错误

如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常,通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。因此,方法中声明要抛出的每个受检的异常,都是对API用户的一种潜在指示:与异常相关联的条件是调用这个方法的一种可能的结果

如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益

错误往往被JVM保留用于表示资源不足、约束失败或者其他使程序无法继续执行的条件,由于这已经是个几乎被普遍接受的惯例,因此最好不要再实现任何新的Error子类,因此,所有未受检的抛出结构都应该是RuntimeException的子类(直接的或者间接的)

第59条:避免不必要地使用受检的异常

过分使用受检的异常会使API使用起来非常不便,如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,这时受检异常才应该被使用

第60条:优先使用标准的异常

Java平台类库提供了一组基本的未受检的异常,它们满足了绝大多数API的异常抛出需要,重用现有的异常有很多方面的好处,如更加易于学习和使用,可读性会更好等

常用的异常:

  • illegalArgumentException 当调用者传递的参数值不合适的时候,往往就会抛出这个异常
  • illegalStateException 如果因为接受对象的状态而使调用非法,通常就会抛出这个异常
  • NullPointerException 参数中传递了null
  • IndexOutOfBoundsException 下标越界
  • ConcurrentModificationException 如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在(或已经)被并发地修改,就应该抛出这个异常
  • UnsupportedOperationException 如果对象不支持所请求的操作,就会抛出这个异常

第61条:抛出与抽象相对应的异常

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法被称为异常转译

public E get(int index){
    ListIterator<E> i = listIterator(index);
    try {
        return i.next();
    } catch(NoSuchElementException e){
        throw new IndexOutOfBoundsException("Index: " + index);
    }
}

一种特殊的异常转译形式称为异常链,如果底层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适

try{
    ...
} catch(LowerLevelException cause){
    throw new HigherLevelException(cause);
}

class HigherLevelException extends Exception{
    HigherLevelException(Throwable cause){
        super(cause);
    }
}

如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获底层的原因进行失败分析

第62条:每个方法抛出的异常都要有文档

始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件,但是不要使用throws关键字将未受检的异常包含在方法的声明中

第63条:在细节消息中包含能捕获失败的信息

为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。例如,IndexOutOfBoundsException异常的细节信息应该包含下界、上界以及没有落在界内的下标值

为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些信息,然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。例如,IndexOutOfBoundsException可以有个这样的构造器:

public IndexOutOfBoundsException(int lowerBound, int upperBound, int index){
    super("Lower bound: " + lowerBound + 
          ", Upper bound: " + upperBound + 
          ", Index: " + index);

    // Save failure information for programmatic access
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

第64条:努力使失败保持原子性

失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性

对于可变对象,可以通过在执行操作之前检查参数的有效性

public Object pop(){
    if(size == 0)
        throw new EmptyStackException();

    Object result = elements[--size];
    elements[size] = null;
    return result;
}

一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生

另一种获得原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容

第65条:不要忽略异常

当API的设计者声明一个方法将抛出某个异常的时候,不应该忽略(空的catch块)它

try{
    ...
}catch(SomeException e){

}

并发

第66条:同步访问共享的可变数据

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的

如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的(long和double类型不是原子的)基本类型。要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。由于boolean域的读和写操作都是原子的,程序再访问这个域的时候不再使用同步:

public class StopThread {

    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                }
            }
        });

        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

由于上述代码没有同步,虚拟机会将这个代码:

while(!done)
    i++;

转变成这样:

if(!done)
    while(true)
        i++;

这种JVM的优化会导致活性失败:这个程序无法前进。修正这个问题的一种方式是同步访问stopRequest域,如下:

public class StopThread {

    private static boolean stopRequested;

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

    private static synchronized boolean stopRequested(){
        return stopRequested;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested()) {
                    i++;
                }
            }
        });

        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

StopThread中被同步方法的动作即使没有同步也是原子的,这些方法的同步只是为了它的通信效果,而不是为了互斥访问

也可以使用volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值:

public class StopThread {

    private static volatile boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    i++;
                }
            }
        });

        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

在使用volatile的时候务必要小心,考虑下面的方法,假设它要产生序列号:

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber(){
    return nextSerialNumber++;
}

因为增量操作符(++)不是原子的,它在nextSerialNumber域中执行两项操作:首先它读取值,然后写回一个新值。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败:这个程序会计算出错误的结果

修正如下:

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber(){
    return nextSerialNum.getAndIncrement();
}

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

简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧

第67条:避免过度同步

过度同步可能会导致性能降低、死锁,甚至不确定的行为。在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法,这样的方法是外来的,不知道该方法会做什么事情,也无法控制它,如下例:

public class ObservableSet<E> extends ForwardingSet<E> {
    public ObservableSet(Set<E> set) { super(set); }

    private final List<SetObserver<E>> observers = 
        new ArrayList<SetObserver<E>>();

    public void addObserver(SetObserver<E> observer) {
        synchronized(observers) {
            observers.add(observer);
        }
    }

    public boolean removeObserver(SetObserver<E> observer) {
        synchronized(observers) {
            return observers.remove(observer);
        }
    }

    // This method is the culprit
    private void notifyElementAdded(E element) {
        synchronized(observers) {
            for (SetObserver<E> observer : observers)
                observer.added(this, element);
        }
    }

    @Override public boolean add(E element) {
        boolean added = super.add(element);
        if (added)
            notifyElementAdded(element);
        return added;
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        boolean result = false;
        for (E element : c)
            result |= add(element);  // calls notifyElementAdded
        return result;
    }
}

Observer通过调用addObserver方法预订通知,通过调用removeObserver方法取消预订

public static void main(String[] args) {
    ObservableSet<Integer> set =
        new ObservableSet<Integer>(new HashSet<Integer>());

    set.addObserver(new SetObserver<Integer>() {
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
            if (e == 23) s.removeObserver(this);
        }
    });

    for (int i = 0; i < 100; i++)
        set.add(i);
}

上述程序在打印出0~23的数字后,并没有停止,而是抛出ConcurrentModificationException。问题在于,当notifyElementAdded调用观察者的added方法时,它正处于遍历observers列表的过程中。added方法调用可观察集合的removeObserver方法,从而调用observers.remove,企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的

可以通过将外来方法的调用移除同步的代码块来解决这个问题

private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized(observers) {
        snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer : snapshot){
        observer.added(this, element);
    }
}

通常,应该在同步区域内做尽可能少的工作,获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果必须要执行某个耗时操作,则应该设法把这个操作移动同步区域的外面

永远不要过度同步,在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间;而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟,过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力

如果一个可变的类要并发使用,应该使这个类变成是线程安全的,通过内部同步,可以获得明显比从外部锁定整个对象更高的并发性,否则,就不要在内部同步

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

如果方法修改了静态域,也必须同步对这个域的访问,即使它往往只用于单个线程

第68条:executor和task优先于线程

java平台的java.util.concurrent.Executor是一个很灵活的基于接口的任务执行工具

如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool通常是个不错的选择,因为它不需要配置,并且一般情况下能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了,在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行,如果没有线程可用,就创建一个新的线程,如果服务器负载得太重,以致它所有的CPU都安全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。因此,在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类

第69条:并发工具优先于wait和notify

正确地使用wait和notify比较困难,就应该使用更高级的并发工具来代替

并发集合为标准的集合接口(如List、Queue和Map)提供了高性能的并发实现,为了提供高并发性,这些实现在内部自己管理同步。这意味着客户端无法原子地对并发集合进行方法调用,因此有些集合接口已经通过依赖状态的修改操作进行了扩展,它将几个基本操作合并到了单个原子操作中。例如,ConcurrentMap扩展了Map接口,并添加了几个方法,包括putIfAbsent(key, value),当键没有映射时会替它插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回null

ConcurrentHashMap除了提供卓越的并发性之外,速度也非常快,可以极大地提升并发应用程序的性能

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

第70条:线程安全性的文档化

一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别

  • 不可变的——这个类的实例是不变的,所以,不需要外部的同步,这样的例子包括String、Long和BigInteger
  • 无条件的线程安全——这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步,其例子包括Random和ConcurrentHashMap
  • 有条件的线程安全——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同,这样的例子包括Collections.synchronized包装返回的集合,它们的迭代器要求外部同步
  • 非线程安全——这个类实例是可变的,为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列),这样的例子包括通用的集合实现,例如ArrayList和HashMap
  • 线程对立的——这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围,线程对立的根源通常在于,没有同步地修改静态数据,这种类是因为没有考虑到并发性而产生的后果

每个类都应该说明或者线程安全注解,清楚地在文档中说明它的线程安全属性,synchronized修饰符与这个文档毫无关系,有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁(通常情况下,指作用在实例自身上的那把锁)”。如果编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法,这样可以防止客户端程序和子类的不同步干扰,能够在后续的版本中灵活地对并发控制采用更加复杂的方法

第71条:慎用延迟初始化

延迟初始化是延迟到需要域的值时才将它初始化的这种行为。这种方法既适用于静态域,也适用于实例域,虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环

延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化

当有多个线程时,延迟初始化是需要技巧的,如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的。在大多数情况下,正常的初始化要优先于延迟初始化

如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式

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

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

当getField方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化

如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式,这种模式避免了在域被初始化之后访问这个域时的锁定开销

private volatile FieldType field;
FieldType getField(){
    FieldType result = field;
    if(result == null) { // First check (no locking)
        synchronized(this){
            result = field;
            if(result == null){ // Second check (with locking)
                field = result = computeFieldValue();
            }
        }
    }
    return result;
}

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

private volatile FieldType field;

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

第72条:不要依赖于线程调度器

要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择:它只需要运行这些可运行的线程,直到它们不再可运行为止。即使在根本不同的线程调度算法下,这些程序的行为也不会有很大的变化

第73条:避免使用线程组

线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的,如果设计的一个类需要处理线程的逻辑组,或许可以使用线程池executor

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值