《Effective Java》读书笔记

Chapter 10 Concurrency

Item 66: Synchronize access to shared mutable data

synchronized这个关键字不仅保证了同步,还保证了可见性(visibility)。
对于变量的读写是原子性的,除非变量类型是long或double。有一个我见过无数遍的例子就是设一个共享的boolean变量,然后从一个线程中断另一个线程的while循环。因为JVM会做优化,但它做优化的前提是假设下面这些代码都是在单线程下运行的,比如可能把while (!done) i++;优化成if (!done) while (true) i++;。从而导致while循环这个线程永远看不到boolean变量的值的改变,导致无限循环。所以你要么对那个boolean变量做synchronized,要么就volatile。但是volatile不保证原子性也不保证同步,比如volatile int i,如果多个线程都i++(注意这不是原子操作),那么i最后的结果会小于方法被调用的次数。你可以用,比如AtomicLong的getAndIncrement来实现正确结果。

Item 67: Avoid excessive synchronization

在一个synchronized块中,千万不要调用一个client提供的方法,也就是千万别调用一个designed to be overridden的方法,或者一个client传进来的function object。对于执行这个synchronized块的class来说,根本不知道那些方法会干嘛(所以作者把那些方法叫做alien methods),那些未知行为的方法可能会导致exceptions,deadlocks, 或data corruption。下面举个具体的反面教材,我们实现一个ObservableSet<E>继承自item16的ForwardingSet<E>,这个ObservableSet实现了Observer pattern,功能是当有元素被添加进来的时候通知所有订阅者:

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;
	}
	//addAll我省略了
}
public interface SetObserver<E> {
        void added(ObservableSet<E> set, E element);
}

上面在notifyElementAdded方法的定义中有一个synchronized块里面调用了一个“alien方法”。如果这个alien方法长下面这个样子,就吃瘪了:

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

这时候如果我们向Observable里面添加1到100,那么当添加完23之后,会抛出ConcurrentModificationException,因为这时候其实你还正在iterating over那个叫observers的List,然后这时候你又想从observers中删掉一个元素,当然报错了。下面还有个例子会造成deadlock,我本来都懒得写了,但是为了复习一些java多线程我忍了:

		// Observer that uses a background thread needlessly
		set.addObserver(new SetObserver<Integer>() {
			public void added(final ObservableSet<Integer> s, Integer e) {
				System.out.println(e);
				if (e == 23) {
					ExecutorService executor = Executors
							.newSingleThreadExecutor();
					final SetObserver<Integer> observer = this;
					try {
						executor.submit(new Runnable() {
							public void run() {
								s.removeObserver(observer);
							}
						}).get();
					} catch (ExecutionException ex) {
						throw new AssertionError(ex.getCause());
					} catch (InterruptedException ex) {
						throw new AssertionError(ex.getCause());
					} finally {
						executor.shutdown();
					}
				}
			}
		});

注意上面那句get()是造成死锁的关键,下面是ExecutorService.submit(Runnable task)的文档:Submits a Runnable task for execution and returns a Future representing that task. The Future's get method will return null upon successful completion.(直到完成后才会返回,返回的是null。)
所以说这里的get()如果在上面的任务没有完成之前,会一直block在这里,而上面那个任务也需要“现在这个地方执行完才能继续”。
解决方法就是:把对alien method的调用移出synchronized块就行了,比如在通知大家之前,创建一个observers的“snapshot”:

private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;
    synchronized(observers) {
        snapshot = new ArrayList<SetObserver<E>>(observers);
    }
    for (SetObserver<E> observer : snapshot)
        observer.added(this, element);//这个alien方法调用被移出来了
}

更好的办法是用CopyOnWriteArrayList作为observers的类型,这玩意儿类似ArrayList,只不过所有的写操作都是对一个fresh copy of the entire underlying array进行的。所以它本身内部的数组从不会被修改,所以对CopyOnWriteArrayList的操作不需要lock。
得出的结论是:在synchronized块的工作越少越好。另一方面,不应该过多地使用synchronized,因为这样你就放弃了多核CPU并行执行的强力,而是一定要一个一个排队(排队等的时候本来可以去干很多别的事儿的,但时间都被浪费在“等”上了)。而且也让JVM无法优化代码。所以如果不是经常要用在concurrent access的环境下,就别在内部synchronize你的类(然后记得文档说明),典型的案例就是StringBuffer(后来被StringBuilder取代了)。

Item 68: Prefer executors and tasks to threads

一个ExecutorService的shutdown()的意思貌似就是告诉它:你这个ExecutorService我已经不需要了,你可以terminate yourself gracefully after completing any work that was already on the queue(指work queue,工作队列)。
你现在应该refrain from working directly with threads,而应该work with tasks。Thread是一种抽象,代表了both the unit of work and the mechanism for executing it。而task是只代表了the unit of work的抽象,而mechanism for executing it被分离了(让executor service去做这件事儿了)。有两种task:Runnable(返回void)和Callable(返回一个值)。随便一提,ScheduledThreadPoolExecutor也可以代替java.util.Timer,虽然后者更容易使用,但前者更灵活。

Item 69: Prefer concurrency utilities to wait and notify

这里的higher-level concurrency utilities包括Executor Framework(item 68),concurrent collections和synchronizers(CountDownLatch什么的)。
concurrent collections manage their own synchronization internaly,所以性能很高,所以不应该再用,比如Hashtable或Collections.synchronizedMap。其中有些提供了state-dependent modify operations,什么意思呢?我个人理解就是:比如如果你想先“测”一下这个集合里面有没有元素,如果没有的话再插元素进去,有的话就不插了,那么如果你不用synchronized块,就是不安全的,因为也许你“测”的时候里面还没有元素,但你“插”的时候里面就有了,所以比如ConcurrentMap就提供了一个方法putIfAbsent(key, value),也是插入一对键值对,只是前提是如果这个要插入的key已经存在,就不插了,也就是说这个方法从某种程度上来说是原子性的。
Synchronizers are objects that enable threads to wait for one another, allowing them to coordinate(互相协调) their activities,常用的是CountDownLatch和Semaphore,较少用的是CyclicBarrier和Exchanger,下面举一个CountDownLatch的例子。CountDownLatch的构造函数接受一个int,比如CountDownLatch cdl = new CountDownLatch(4),意思就是必须调用四次cdl.countDown()之后,其他所有因为调用了cdl.await()而block住的线程才能继续。CountDownLatch是一次性的,就只能使用一次。比如下面这个方法,意思是如果你同时让concurrency(方法参数)个actions并行执行,需要多少时间(你可以想象成几个选手的百米赛跑,必须同时出发,然后最后一个人到达终点的时候结束计时):

// Simple framework for timing concurrent execution
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++) {//如果创建少于concurrency个,就会造成thread starvation deadlock
        executor.execute(new Runnable() {
            public void run() {
                ready.countDown(); //告诉裁判这位选手已经准备好了
                try {
                    start.await(); //等待裁判吹口哨
                    action.run();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();//让executor来处理
                } finally {
                    done.countDown(); //这位选手达到终点了
                }
            }
        });
    }
    ready.await(); //等待各个选手就位
    long startNanos = System.nanoTime();//开始计时
    start.countDown(); //吹响口哨
    done.await(); //等待最后一个选手跑完
    return System.nanoTime() - startNanos;
}

如果用wait和notify实现的话(为了维护遗留代码),会很麻烦。顺便一提,对于间歇式的定时,用System.nanoTime比System.currentTimeMillis好。
如果实在要用wait和notify,请参照以下标准写法:

// The standard idiom for using the wait method
synchronized (obj) {
    while (<condition does not hold>)
        obj.wait(); // (Releases lock, and reacquires on wakeup)
    ... // Perform action appropriate to condition
}

原因请参考Thinking in Java的笔记或者等看完Java concurrency in practice后补充。

Item 70: Document thread safety

当一个类的instances或static methods被concurrently地用的时候,会怎么表现,这一点需要你在文档中说明。
本书作者总结的thread safety级别:
一.immutable,完全不需要external synchronization。比如String, Long, and BigInteger。
二.unconditionally thread-safe,虽然类的实例是mutable的,但是其内部有足够的synchronization,所以its instances can be used concurrently without the need for any external synchronization。比如Random,ConcurrentHashMap。
三.conditionally thread-safe,类似unconditionally thread-safe, 除了有一些方法需要external synchronization for safe concurrent use(这些方法要在文档中特别说明).比如collections returned by the Collections.synchronized wrappers, whose iterators require external synchronization.
四.not thread-safe,想并发访问的话,必须在外部做同步。比如ArrayList,HashMap等。
五.thread-hostile,即使在外部做了同步也不行。这种类一般是在没做同步的情况下修改了static的data(虽然书上没细讲,但我估计是因为这样的话多个实例就可以同时修改这个static的data了,即使在修改前先lock住了自己也没用,除非是lock住对应的class object(我猜的))。
如果你的类的内部用的是一个public的lock,那么可以让client原子性地执行一系列的操作,但坏处是,比如坏人可以长时间持有这个锁不松手,让别人没法获取资源。所以我们可以把这个锁对象改成private的:private final Object lock = new Object()(注意是final的,这样可以防止你不小心改变了这个field的值)。unconditionally thread-safe classes应该用这种私有的锁,而Conditionally thread-safe classes不能,因为他们必须告诉client在调用某些方法的时候应该手动去获取的锁是什么。另外,对于classes designed for inheritance来说,应该用私有的锁,防止子类乱搞。

Item 71: Use lazy initialization judiciously

Lazy initialization对static field和instance field都可以用,这是一种优化,还是那句原则“don’t do it unless you need to”,在多线程的情况下:

// Normal initialization of an instance field
private final FieldType field = computeFieldValue();//这里的final不知道为何,书上没说

然后是Lazy initialization:

// Lazy initialization of instance field - synchronized accessor
private FieldType field;
synchronized FieldType getField() {
    if (field == null)
        field = computeFieldValue();
    return field;
}

然后对于static的field也是一样,只要把field的声明加个static就行。
如果你出于对性能的考虑,要对一个static field用lazy initialization,那么请用lazy initialization holder class这个方法,如下所示:

// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }

当getField方法第一次被调用时,由于它读取FieldHolder.field,所以FieldHolder这个类被初始化,这种方法的精妙在于根本不需要写synchronized,JVM为了初始化FieldHolder这个类会同步对field的访问。
如果你出于性能考虑,要对一个instance field用lazy initialization,那么就用double-check idiom(双重检查方法):

// Double-check idiom for lazy initialization of instance fields
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;
}

上面的result这个local variable只为为了优化,它确保在field已经被initialized之后的情况下,field只被读取一次(我个人理解:因为如果不用这个result的话,要读取field两次,第一次测试它是不是null,第二次return它)。虽然不是严格要求,但是作者说在他的机器上这样写会把速度提升25%(我觉得有点神奇且无法理解)。虽然你也可以对static field用这个双重检查的方法,但是没必要因为azy initialization holder class那个方法是更好的选择。而大多数情况下,请用Normal initialization。

Item 72: Don’t depend on the thread scheduler

当有多个线程是runnable(我的理解是:可以run的线程)的时候,由thread scheduler决定哪些线程run,run多久,操作系统会让这个决定尽量公平,但是不同操作系统有不同的策略,我们不应该依赖这种策略的细节。任何依赖于thread scheduler来达到正确性或性能要求的程序都可能是nonportable的。
最好保证runnable threads的数量不远远大于处理器数量,主要的技巧就是to have each thread do some useful work and then wait for more(我不是特别懂,感觉像在说异步,也就是一个线程干完一部分活儿后,如果要“等待”别的什么事儿先完成才能继续的话,就返回线程池,等待被分配其他活儿,当那件事完成后收到通知后,线程池会再分配一个线程去完成剩下的活儿,这样一来你的runnable threads(我觉得也可以理解为工作线程)就可以比较少(因为大家都在尽全力工作没有阻塞或等待,否则你可能因为工作线程不够用,只能增加更多的工作线程)。)。对Executor Framework来说,上面那句话的意思就是恰当地指定你的线程池大小,并使每个task适当的小,并且互相独立。不要“busy-wait”,就比如:while (true){if(...)break;}。
当你的程序中的某些线程,相比另一些线程来说,得不到足够的CPU时间的时候,不要试图用Thread.yield来解决问题,Thread.yield的结果是不可预料的,也许会更差,也许会完全没效果。也不要试图通过调整线程优先级来“解决”问题。有时候可能会在测试期间做一下并发测试(concurrency testing),这时候你应该用Thread.sleep(1)来代替Thread.yield(不要用Thread.sleep(0)因为会立刻返回)。

Item 73: Avoid thread groups

thread groups是早期用于applets的玩意儿,大概功能就是能把Thread的某些基本功能应用到一组线程上。但是你只要记住:这玩意儿过时了!无视他们的存在就行!你应该用thread pool executors(Item 68)。

原文地址:https://www.cnblogs.com/raytheweak/p/7253031.html