9.并发_EJ
第66条: 同步访问共享可变的数据
所谓同步指的发出一个调用时,如果没有得到结果就不返回,直到有结果后再返回。另外相对应的是异步,指的是发出一个调用时就立即返回而不在乎此时有没有结果。
同步和异步关注的是“消息通信机制”,通常我们提到同步的时候实际上只理解了它一部分或者干脆理解为“互斥”,这是不全对的,例如Java中synchronized关键字,经常听到教育我们说,要互斥访问某个共享变量且需要保证它线程安全的时候就用synchronized关键字。
互斥表示当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。同步不仅包含这层意义还包含:它可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有修改效果。
对于synchronized我相信几乎人人都知道它是线程安全的重要保证,这里不再叙述它如何保证。着重强调几个术语:活性失败:线程A对某变量值的修改,可能没有立即在线程B体现出来。这是由于Java内存模型造成的原因,一个线程修改某个变量后并不会立即写入主存而是写到线程自身所维护的内存中,这个时候导致另一个线程从主存中取出的值并不是最新的,使用synchronized可保证这种可见性,当然还有volatile关键字。安全性失败:例如i++操作并不是原子的,而是先+1再复制,这就有两个动作,而这两个动作的完成很有可能导致中间穿插两个线程,这个时候就会导致程序计算结果出错。
一个活性失败的例子:
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; } }
这个程序永远不会停止。因为虚拟机的优化,stopRequested值的变化不能马上被另一线程获得。修正这个问题的一种方式是同步访问stopRequested域,或用volatile修饰该域。
简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步,以确保线程安全,程序正确运行。
第67条: 避免过度同步
对于在同步区域的代码,千万不要擅自调用其他方法,特别是会被重写的方法,因为这会导致你无法控制这个方法会做什么,严重则有可能导致死锁和异常。通常,应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。
举个例子加深理解:
public interface SetObserver<E> { //invoke when an element is added to the observable set void added(ObservableSet<E> set, E element); }
//Broken - invokes alien method from synchronized block! public class ObservableSet<E> extends ForwardingSet<E> { public ObservableSet(Set<E> set){ super(set); } private final List<SetObserver<E>> observers = new ArrayList<>(); public void addObserver(SetObserver<E> observer){ synchronized (observers){ observers.add(observer); } } public boolean removeObserver(SetObserver<E> observer){ synchronized (observers){ return observers.remove(observer); } } public void notifyElementAdded(E element){ synchronized (observers){ for(SetObserver<E> observer : observers){ observer.added(this, element); } } } /* *解决ConcurrentModificationException和死锁的方案 * public void notifyElementAdded(E element){ List<SetObserver<E>> snapshot = null; synchronized (observers){ snapshot = new ArrayList<>(observers); } for(SetObserver<E> observer : snapshot){ observer.added(this, element); } } */ @Override public boolean add(E e) { boolean added = super.add(e); if(added){ notifyElementAdded(e); } return added; } @Override public boolean addAll(Collection<? extends E> c) { boolean result = false; for (E element : c){ result |= add(element); } return result; } public static void main(String[] args) { ObservableSet<Integer> set = new ObservableSet<>(new HashSet<Integer>()); //java.util.ConcurrentModificationException set.addObserver(new SetObserver<Integer>() { @Override public void added(ObservableSet<Integer> set, Integer element) { System.out.println(element); if(element == 23){ set.removeObserver(this); } } }); //会发生死锁 /* set.addObserver(new SetObserver<Integer>() { @Override public void added(final ObservableSet<Integer> set, Integer element) { System.out.println(element); if(element == 23){ ExecutorService executor = Executors.newSingleThreadExecutor(); final SetObserver<Integer> observer = this; try { executor.submit(new Runnable() { @Override public void run() { set.removeObserver(observer); } }).get(); } catch (InterruptedException e) { throw new AssertionError(e.getCause()); } catch (ExecutionException e) { throw new AssertionError(e.getCause()); }finally { executor.shutdown(); } } } });*/ for (int i = 0; i < 100; i++){ set.add(i); } } }
第68条: executor和task优先于线程
之所以推荐executor和task原因就在于这样便于管理。
第69条: 并发工具优先于wait和notify
随着JDK的发展,基于原始的同步操作wait和notify已不再提倡使用,因为基础所以很多东西需要自己去保证,越来越多并发工具类的出现应该转而学习如何使用更为高效和易用的并发工具。
第70条: 线程安全性的文档化
书中提到了很有意思的情景,有人会下意识的去查看API文档此方法是否包含synchronized关键字,如不包含则认为不是线程安全,如包含则认为是线程安全。实际上线程安全不能“要么全有要么全无”,它有多种级别:
不可变的——也就是有final修饰的类,例如String、Long,它们就不用外部同步。
无条件的线程安全——这个类没有final修饰,但其内部已经保证了线程安全,例如并发包中的并发集合类,同样它们无需外部同步。
有条件的线程安全——这个有的方法需要外部同步,而有的方法则和“无条件的线程安全”一样无需外部同步。
非线程安全——这就是最“普通”的类了,内部的任何方法想要保证安全性就必须要外部同步。
线程对立的——这种类就可以忽略不计了,这个类本身不是线程安全,并且就算外部同样同样也不是线程安全的,JDK中很少很少,几乎不计,自身也不会写出这样的类,或者也不要写出这种类。
可见队员线程是否安全不能仅仅做安全与不安全这种笼统的概念,更不能根据synchronized关键字来判断是否线程安全。你应该在文档注释中注明是以上哪种级别的线程安全,如果是有条件的线程安全不仅需要注明哪些方法需要外部同步,同时还需要注明需要获取什么对象锁。
第71条: 慎用延迟初始化
延迟初始化又称懒加载或者懒汉式,这在单例模式中很常见。
众所周知单例模式大致分为懒汉式和饿汉式,这两种方式各有其优缺点。对于饿汉式会在初始化类或者创建实例的时候就进行初始化操作,而对于懒汉式则相反它只有在实际用到访问的时候才进行初始化。
至于用何种方式通常来讲并没有太大的讲究,几乎是看个人习惯。而此书却单独列了一条来说明延迟初始化使用不当所带来的危害。
1) 使用延迟初始化时一定要考虑它的线程安全性,通常此时会利用synchronized进行同步。
2) 若需要对静态域使用延迟初始化,且需要考虑性能,则使用lazy initialization holder class模式:
public class Singleton { private static class SingletonHolder { private static Singleton singleton = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return SingletonHolder.singleton; } }
这种模式不同于传统的延迟加载,当调用getInstance时候第一次读取SingletonHolder.singleton,导致SingletonHolder类得到初始化,这个类在装载并被初始化的时候会初始化它的静态域即Singleton实例,getInstance方法并没有使用被synchronized同步,并且只是执行一个域的访问这种延迟初始化的方式实际上并没有增加任何访问成本。
3) 若需要对实例域使用延迟初始化,且需要考虑性能,则使用双重检查模式,这种模式其实也是为了避免synchronized带来的锁定开销:
public class Singleton { private volatile Singleton singleton; private Singleton() { } public Singleton getInstance() { Singleton result = singleton; if (result == null) { synchronized (this) { result = singleton; if (result == null) { singleton = result = new Singleton(); } } } return result; } }
通常用的比较多的可能是对静态域应用双重检查模式。
最后书中的建议就是正常地进行初始化,而对于延迟初始化则徐亚慎重考虑它的性能和安全性。
第72条: 不要依赖线程调度
不要依赖指的是不要将正确性依赖于线程调度器。例如:调整线程优先级,线程的优先级是依赖于操作系统的并不可取;调用Thrad.yield是的线程获得CPU执行机会,这也不可取。所以不要将程序的正确性依赖于线程调度器。
第73条: 避免使用线程组
ThreadGroup之前都没听过,反正不要用就是的了。