Effective Java 2 Concurrency
第66条:同步访问共享的可变数据
关键字synchronized
可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。
如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一锁保护的之前所有的修改效果。
如下程序:
|
|
这个程序会进入死循环。问题:由于没有同步,就不能保证后台线程何时"看到"主线程对stopRequested的值所做的改变
。虚拟机会进行优化:
优化前:
|
|
优化后:
|
|
解决方法1:同步读和写操作 注意这里方法的同步 不是为了互斥访问,而只是为了通信效果
|
|
方法2:使用volatile 虽然volatile不执行互斥访问,但是它可以保证任何一个线程读取该域的时候都将看到最近刚刚被写入的值。
|
|
让一个线程在短时间内修改一个数据对象,然后与其他线程共享。只同步共享对象的引用,这种对象被称作事实上不可变的。将这种对象引用从一个线程传递到其他的线程被称作安全发布。 安全发布有几种方法:
- 可以将它保存在静态域中,作为类初始化的一部分
- 可以将它保存在volatile域、final域或者通过正常锁定访问的域中
- 可以将它放到并发的集合中(见第69条)
总而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败和安全性失败。 如果只需要线程之间的交互通信,而不需要互斥,volatile就可以使用。详见:正确使用 volatile 的模式
第67条:避免过度同步
过度同步可能会导致性能降低、死锁、甚至不确定的行为。 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。即在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。这些外来的方法导致无法控制。
这里采用了一个观察者模式例子:代码 分析:
1、情况:同一个线程里调用removeObserver —> 异常 当if (element == 23) set.removeObserver(this); 会抛出ConcurrentModificationException异常。
这时因为,当notifyElementAdded调用added方法时,此时它正处于遍历observers列表的过程中,所以added方法调用removeObserver方法,从而调用了observers.remove。 我们正在遍历列表的过程中,将一个元素从列表中删除,这是非法的。
notifyElementAdded方法中的迭代是在一个同步的块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。
2、情况:开启新线程调用removeObserver —> 死锁
后台线程调用removeObserver方法时,它企图锁定observers对象,但是它无法锁定。因为主线程的notifyElementAdded已经获得对象锁。在这期间,主线程一直在等待后台线程来完成对观察者的删除,就造成了死锁。
3、解决:将外来方法的调用移出同步代码块
更好的方法:使用Java提供的并发集合:CopyOnWriteArrayList。它是ArrayList的一种变体,通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不改动,因此迭代不需要锁定,速度也非常快。
在同步区域之外调用的外来方法被称作
open call
,除了可以避免死锁之外,还可以极大的提高了并发性。如果在同步区域内调用外来方法,其他线程对受保护资源的访问就会遭到不必要的拒绝。 永远不要过度同步,在这个多核的时代,过度同步的实际成本并不是指获取死锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步另一个潜在的开销在于,他会限制VM优化代码执行的能力。
总而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。要尽量限制同步区域内部的工作量。
第68条:executor和task优先于线程
Java平台中增加了java.util.concurrent,其中包含了Executor Framework(基于接口的任务执行工具) Java并发框架Executor学习 Executor框架详解
这里主要介绍几个类:
Executors
它提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。 接口Executor只有一个方法execute,接口ExecutorService扩展了Executor并添加了一些生命周期管理的方法,如shutdown、submit等。一个Executor的生命周期有三种状态,运行 ,关闭 ,终止。
ThreadPoolExecutor
ScheduledThreadPoolExecutor
ScheduleThreadPoolExecutor是对ThreadPoolExecutor的集成。增加了定时触发线程任务的功能。 详见
第69条:并发工具优先于wait和notify
java.util.concurrent中高级的工具分为:Executor Framework、并发集合(Concurrent Collection)、同步器(Synchronizer) Executor Framework在第68条已经说明
Concurrent Collection
并发集合为标准的集合(List/Queue/Map)提供了高性能的并发实现。 如使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable。用并发Map替换老式的同步Map,极大的提高了程序的性能。
Synchronizer
同步器是一些线程能够等待另一个线程的对象,允许它们协调动作。常用的同步器CountDownLatch和Semaphore。
第70条:线程安全性的文档化
第71条:慎用延迟初始化
延迟初始化时延迟到需要域的值时才能将它初始化的这种行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法适用于静态域/实例域。 对于大多数优化一样,最好的建议是除非绝对必要,否则就不要这么做。如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。 但是当有多线程时,共享一个延迟初始化的域需要技巧。
lazy initialization holder class idiom模式
|
|
当getField第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化。这种模式优势在于,getField方法没有被同步,并且只执行一个域的访问,因此延迟初始化实际上并没有增加任何访问成本。现代的VM将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。
双重检查模式
|
|
其中局部变量result作用:确保field只在已经被初始化的情况下读取一次。虽然这不是严格需要,但是可以提升性能,并且因为给低级的并发编程应用了一些标准,显得更加优雅。
单重检查模式
|
|
用到场景:可以接受重复初始化的实例域
总结,大多数域都应该正常的进行初始化,而不是延迟初始化。 如果为了达到性能目标、或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用以上方法。 实例域:双重检查模式。 静态域:lazy initialization holder class idiom。 可以接受重复初始化的实例域:单重检查模式。
第72条:不要依赖于线程调度器
解析:在应用程序中,线程的调度不应该是 调度器 来执行的。(理解:调度器控制线程执行顺序)
任何依赖于线程调度器来达到正确性或者性能要求的任务,很有可能都是不可移植的。 如果线程没有在做有意义的工作,就不应该运行。 不要企图调用Thread.yield来修正程序,在Java规范中,Thread.yield根本不做实质性的工作,只是将控制权返回给它的调用者。所以可以使用Thread.sleep(1)替代Thread.yield来进行并发测试
不要让应用程序的正确性依赖于线程调度器,否则程序既不健壮也不具有可移植性。 不要依赖Thread.yield或者线程优先级,
线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来"修正"一个原本并不能工作的程序
。
第73条:避免使用线程组
ThreadGroup没有提供太多有用的功能,存在缺陷,应该用线程池executor替代