:::info
💡 根据 遗忘曲线:如果没有记录和回顾,6天后便会忘记75%的内容
读书笔记正是帮助你记录和回顾的工具,不必拘泥于形式,其核心是:记录、翻看、思考
:::
| 书名 | Java 并发编程实战 |
|---|---|
| 作者 | Doug Lea |
| 状态 | 阅读中 |
| 简介 | 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。 |
思维导图
用思维导图,结构化记录本书的核心观点。
第二章 线程安全性
第三章 对象的共享
第四章 对象的组合
书摘
第二章及第三章总体介绍了关于线程安全与同步的一些基础知识。然而,我们并不希望对每一次内存访问都进行分析以确保是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或程序。
本章将介绍一些组合模式,这些模式能够使一个类更容易称为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。
4.1 设计线程安全的类
在线程安全的程序中,虽然可以将程序的所有状态都保存在公有的静态域中,但与那些将状态封装起来的程序相比,这些程序的线程安全性更难以得到验证,并且在修改时也更难以始终确保其线程安全性。通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象的所有变量;
- 找出约束状态变量的不变性条件;
- 建立对象状态的并发访问管理策略。
4.1.1 收集同步需求
要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。对象与变量都有一个状态空间,即所有可能的取值。状态空间越小,就越容易判断线程的状态。final类型的域使用得越多,就越能简化对象可能状态得分析过程。
由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外得同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能会是对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等需求,以便获得更高的灵活性或性能。
在类中也可以包含同时约束多个状态变量的不变性条件。
如果不了解对象的可变性条件与后验条件,那么就不能确保该对象的线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。
4.1.2 依赖状态的操作
类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换时有效的。在某些对象的方法中还包含了一些基于状态的先验条件(Precondition)。例如,不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。如果在某个操作中包含基于状态的先验条件,那么这个操作就称为依赖状态的操作。
在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但在并发程序中,先验条件可能会由于其他线程执行的操作而变成真。在并发程序中要一直等到先验条件为真,然后再执行该操作。
4.1.3 状态的所有权
4.1节曾指出,如果以某个对象为根节点构造一张对象图,那么该对象的状态将是对象图中所有对象包含的域的一个子集。为什么是一个“子集”?在从对象可以达到的所有域中,需要满足哪些条件才不属于对象状态的一部分?
在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。所有权(Ownership)在Java中并没有得到充分的体现,而是属于类设计中的一个要素。如果分配并填充了一个HashMap对象,那么就相当于创建了多个对象:HashMap对象,在HashMap对象中包含的多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map.Entry对象以及内部对象,即使这些对象都是一些独立的对象。
无论如何,垃圾回收机制使我们避免了如何处理所有权的问题。在C++中,当把一个对象传递给某个方法时,必须认真考虑这种操作是否传递对象的所有权,是短期的所有权还是长期的所有权。在Java中同样邨彩这些所有权模型,只不过垃圾回收器为我们减少了许多在引用共享方面常见的错误,因此降低了在所有权处理上的开销。
许多情况下,所有权域封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占的控制权,最多是“共享控制权”。对于从构造函数或者方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权(例如,同步容器封装器的工厂方法)。
容器类通常表现出一种“所有权分离”的形式,其中容器类拥有其自身的状态,而客户代码则拥有容器中各个对象的状态。Servlet框架中的ServletContext就是其中一个示例。ServletContext为Servlet提供了类似Map形式的对象容器服务,在Servlet容器实现的ServletContext对象必须是线程安全的,因为它肯定会被多个线程同时访问。当调用setAttribute和getAttribute时,Servlet不需要使用同步,但当使用保存在ServletContext中的对象时,则可能需要使用同步。这些对象由应用程序拥有,Servlet容器只是替用应用程序保管他们。与所有共享对象一样,他们必须安全地被共享。为了防止多个线程在并发访问同一个对象时产生相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对象,或者由锁来保护的对象。
4.2 实例封闭
如果某个对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。
封装简化了线程安全类的访问过程,它提供了一种实例封闭机制(Instance Confinement),通常也简称为“封闭”[CPJ 2.3.3]。当一个对象被封装到另一个对象时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析,通过将封闭机制与合适的加锁策略结合起来,可以确保线程安全的方式来使用非线程安全的对象。
要将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
如何封装?
被封闭对象一定不能超出它们既定的作用域。对象可以封闭在类的一个实例(例如作为类的一个私有成员)中,或者封闭在某个作用域内(例如作为一个局部变量),再或者封闭在线程内(例如某个线程中将对象从一个方法传递到另一个方法,而不是在多个线程之间共享该对象)。当然,对象本身不会逸出——出现逸出情况的原因通常是由于开发人员在发布对象时超出了对象既定的作用域。
实例封闭是构建线程安全类的一个最简单的方式,它还使得在锁策略的选择上拥有了更多的灵活性。在PersonSet程序demo中使用了它的内置锁来保护它的状态,但对于其他形式的锁来说,只要自始至终都使用同一个锁,就可以保护状态。实例封闭还使得不同的状态变量可以由不同的锁来保护。
在Java平台的类库中还有很多线程封闭的实例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。一些基本的容器类并非线程安全的,例如ArrayList和HashMap,但类库提供了包装器工厂方法(例如Collections.synchronized List及其他类似方法),使得这些非线程安全的类可以在多线程环境中安全地使用。这些工厂方法通过“装饰器(Decorator)”模式(Gramma et.al., 1995)将容器类封装在一个同步的容器对象上。只要包装器对象拥有对底层容器对象的唯一引用(即把底层对象封闭在包装器中),那么它就是线程安全的。在这些方法的Javadoc中指出,对底层容器对象的所有访问必须通过包装器来进行。
当然,如果将一个本该被封闭的对象发布出去,那么也能破坏封闭性。如果一个对象本应该封闭在特定的作用域中,那么让该对象逸出作用域就是一个错误。当发布其他对象时,例如迭代器或内部的类实例,可能间接地发布被封闭对象,同样会使被封闭对象逸出。
封闭机制更易于构造线程安全地类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。
4.2.1 Java监视器模式
从线程封闭原则及其逻辑推论可以得出Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态封装起来,并由对象自己的内置锁来保护。PS:虽然Java监视器模式来自于Hoare对监视机制的研究工作(Hoare,1974),但这种模式与真正的监视器类之间存在一些重要的差异。进入和推出同步代码块的字节指令也称为monitorenter和monitorexit,而Java的内置锁也称为监视器锁或监视器。
在许多类中都使用了Java监视器模式,例如Vector和Hashtable。在某些情况下,程序需要一种更复杂的同步策略。第11章将介绍如何通过细粒度的加锁策略来提高可伸缩性。Java监视器模式的主要优势在于他的简单性。
Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都是用该锁对象,都可以用来保护对象的状态。
public class PrivateLock{private final Object myLock = new Object();@GuardedBy("myLock") Widget widget;void someMethod(){synchronized(myLock){//访问或修改widget对象的状态}}}
使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方式访问的锁),可以将锁封装起来使客户代码无法得到锁,但可以通过公有方法来访问锁。如果客户代码错误地获得了另一个对象的锁,那么可能会产生活跃性(性能)问题。
4.2.2 示例:车辆跟踪
@ThreadSafepublic class MonitorVehicleTracker {@GuardedBy("this")private final Map<String, MutablePoint> locations;public MonitorVehicleTracker(Map<String, MutablePoint> points) {locations = deepCopy(points);}public synchronized Map<String, MutablePoint> getLocations() {return deepCopy(locations);}public synchronized MutablePoint getLocation(String key) {MutablePoint point = locations.get(key);return point == null ? null : new MutablePoint(point);}public synchronized void setLocation(String id, int x, int y) {if (id == null) {return;}MutablePoint point = locations.get(id);if (point == null) {throw new IllegalArgumentException("No such ID: " + id);}point.setPoint(x, y);}private Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> points) {if (points == null) {return Maps.newHashMap();}Map<String, MutablePoint> result = Maps.newHashMapWithExpectedSize(points.size());for (String key : points.keySet()) {result.put(key, new MutablePoint(points.get(key)));}return Collections.unmodifiableMap(result);}}@NotThreadSafepublic class MutablePoint {private int x, y;public MutablePoint(int x, int y) {this.x = x;this.y = y;}public MutablePoint() {}public MutablePoint(MutablePoint point) {if (point == null) {throw new IllegalArgumentException("param is null");}int[] pointArray = point.getPointArray();x = pointArray[0];y = pointArray[1];}public int[] getPointArray() {int[] ret = new int[2];ret[0] = x;ret[1] = y;return ret;}public void setPoint(int x, int y) {this.x = x;this.y = y;}}
4.3 线程安全性的委托
4.3.1 示例:基于委托的车辆追踪器
/*** 没有任何显示的同步,所有对状态的访问都由ConcurrentMap管理,而且map所有的键值都是不可变的* ConcurrentMap的key 是final修饰的** 由于Point类是不可变的,因此它是线程安全的。不可变的值可以被自由地分享与发布,因此返回location时不需要复制。** 注释1、2意味着:* 如果线程A调用【getLocations】而线程B在随后修改了【setLocation】某些点的位置,* 那么在返回给线程A的Map中将反映出这些变化* 优点:更新数据* 缺点:可能导致不一致的车辆位置视图**/@ThreadSafepublic class DelegatingVehicleTracker {private final ConcurrentMap<String, Point> locations;private final Map<String,Point> unmodifiableMap;public DelegatingVehicleTracker(Map<String,Point> points) {locations = new ConcurrentHashMap<>(points);unmodifiableMap = Collections.unmodifiableMap(points);}//1、在使用监视器模式的车辆追踪器中 返回的是 车辆的位置的快照public Map<String, Point> getLocations() {// return Collections.unmodifiableMap(new HashMap<String,Point>(locations)); //返回locations的静态拷贝return unmodifiableMap; //返回实时拷贝}//2、而在使用委托的车辆追踪器中 返回的是 一个不可修改 但却是实时的车辆位置视图public Point getLocation(String id) {return locations.get(id);}public void setLocation(String id, int x, int y) {if(locations.replace(id, new Point(x,y)) == null)throw new IllegalArgumentException("invalid vehicle name : ");}}
4.3.2 独立的状态变量
/*** keyListeners,mouseListeners每个状态都是线程安全的,并且两个状态之间不存在耦合关系* 所以VisualComponent可以将他的线程安全性委托给keyListeners和mouseListeners*/public class VisualComponent {//CopyOnWriteArrayList是一个线程安全列表,特别适合于管理监听器列表private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();public void addKeyListener(KeyListener listener) {keyListeners.add(listener);}public void addMouseListener(MouseListener mouseListener) {mouseListeners.add(mouseListener);}public void removeKeyListener(KeyListener keyListener) {keyListeners.remove(keyListener);}public void removeMouseListener(MouseListener mouseListener) {mouseListeners.remove(mouseListener);}}
4.3.3 当委托失效时
/*** 假设:取值范围是(0,10),如果一个线程调用setLower(5), 一个线程调用setUpper(4),* 那么:在一些错误的时序中,这两个调用都将通过检查,并且都能设置成功。* 结果:得到的取值范围就是(5,4),这是一个无效的状态* 所以:虽然AtomicInteger是线程安全的,但经过组合得到的类确不是。* 由于:lower和upper不是彼此独立的,* 因此:NumberRange不能将线程安全性委托给它的线程安全状态变量。*/import java.util.concurrent.atomic.AtomicInteger;public class NumberRange {//不变性条件 lower < upperprivate final AtomicInteger lower = new AtomicInteger(0);private final AtomicInteger upper = new AtomicInteger(0);public void setLower(int i) {//注意:不安全的“先检查后执行”if(i > upper.get())throw new IllegalArgumentException("cant't set lower to " + i + " > upper");lower.set(i);}public void setUpper(int i) {//注意:不安全的“先检查后执行”if(i < lower.get())throw new IllegalArgumentException("cant't set lower to " + i + " < lower");lower.set(i);}public boolean isInRange(int i) {return (i >= lower.get() && i <= upper.get());}}
4.3.4 发布底层的状态变量
4.3.5 示例:发布状态的车辆追踪器
/*** 【发布底层的可变状态】* 当把线程安全性委托给某个对象的底层状态变量时,* 在什么情况下才可以发布这些变量从而使其它类可以修改他们?* 答案仍然取决于在类中对这些变量施加了哪些不变性条件*/@ThreadSafepublic class SafePoint {private int x,y;public SafePoint(int x, int y) {this.x = x;this.y = y;}private SafePoint(int[] a) { this(a[0],a[1]); }/*如果将拷贝的构造函数改为this.(p.x,p.y),则会产生竞态条件,因为其是public*而private的构造函数则可以避免这种竞态条件*/public SafePoint(SafePoint p) { this(p.get()); }/** 如果为x和y分别提供get 方法,那么获得这2个不同坐标的操作之间,* x,y的值发生变化,从而导致调用者看到不一致的值:车辆从来没有到达过(x,y)。** 通过使用SafePoint,可以构造一个发布其底层可变状态的车辆追踪器,* 还能确保其线程安全性不被破坏。*/public synchronized int[] get() {return new int[] {x,y};}public synchronized void set(int x,int y) {this.x = x;this.y = y;}}
/*** 程序清单4-7的差别:* 1、底层状态类不同* 4-7: Point:状态不可变的* 4-12:SafePoint:线程安全且底层状态可变的* 2.* 4-7:setLocation()中replace()* 4-12:setLocation()中containKey()*/@ThreadSafepublic class PublishingVehicleTracker {private final Map<String,SafePoint> locations;private final Map<String,SafePoint> unmodifyMap;public PublishingVehicleTracker(Map<String, SafePoint> locations) {this.locations = new ConcurrentHashMap<String, SafePoint>(locations);this.unmodifyMap = Collections.unmodifiableMap(this.locations);}public Map<String,SafePoint> getLocations() {return unmodifyMap;}public SafePoint getLocation(String id) {return locations.get(id);}public void setLocation(String id,int x, int y) {if(!locations.containsKey(id))throw new IllegalArgumentException("invalid vehicle name: " + id);locations.get(id).set(x, y);}}
4.4 在现有的线程安全类中添加功能
4.4.1 客户端加锁机制
4.4.2 组合
4.5 将同步策略文档化
第五章 基础构建模块
序
读后感
观点1
读完该书后,受益的核心观点与说明…
观点2
读完该书后,受益的核心观点与说明…
观点3
读完该书后,受益的核心观点与说明…
书摘
- 该书的金句摘录…
- 该书的金句摘录…
- 该书的金句摘录…
相关资料
可通过“⌘+K”插入引用链接链接,或使用“本地文件”引入源文件。
