4.1 设计线程安全的类
在线程安全的程序中,虽然可以将程序的所有状态都保存在公有状态域中,但与那些将状态封装起来的程序相比,这些程序的线程安全性更难以得到验证,并且在修改时也更难以始终确保其线程安全性。通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。
在设计线程安全类的过程中,需要包含以下三个基本要素:
1.找出构成对象状态的所有变量
2.找出约束状态变量的不变性条件
3.建立对象状态的并发访问管理策略
4.1.2 依赖状态的操作
如果在某个操作中含有基于状态的先验条件,那么这个操作就称为 依赖状态 的操作。
在Java中,等待某个条件为真的各种内置机制(包括等待和通知等机制)都与内置加锁机制紧密相连,要想正确地使用它们并不容易。要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类(例如阻塞队列[Blocking Queue、Semphore])来实现依赖状态的行为。
4.1.3 状态的所有权
在定义哪些变量将构成对象的状态时,只考虑对象拥有的数据。所有权在Java中并没有得到充分的体现,而是属于类设计中的一个要素。如果分配并填充了一个HashMap对象,那么就相当于创建了多个对象:HashMap对象,在HashMap对象中包含的多个对象,以及在Map.Entry中可能包含的内部对象。HashMap对象的逻辑状态包括所有的Map.Entry对象以及内部对象,即使这些对象都是一些独立的对象。
许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来位置变量状态的完整性。所有权意味着控制权。然而,如果发布了某个可变对象的引用,那么就不再拥有独占控制权,最多是"共享控制权"。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象所有权(例如,同步容器封装器的工厂方法)。
容器类通常表现出一种"所有权分离"的形式,其中容器类拥有自身的状态,而客户代码则拥有容器中各个对象的状态。Servlet框架中的ServletContext就是其中一个示例。ServletContext为Servlet提供了类似于Map形式的对象容器服务,在ServletContext中可以通过名称来注册(setAttribute)或获取(getAttribute)应用程序对象。由Servlet容器实现的ServletContext对象必须是线程安全的,因为它肯定会被多个线程同时访问。当调用setAttribute和getAttribute时,Servlet不需要使用同步,但当使用保存在ServletContext中的对象时,则可能需要使用同步。这些对象由应用程序拥有,Servlet容器知识替代应用程序保管它们。与所有共享对象一样,它们必须安全地被共享。为了防止多个线程在并发访问同一个对象时产生的相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对手,或者由锁来保护的对象。
4.2 实例封闭
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(Instance Confinement)通常也简称"封闭"。当一个对象被封装到另一个对象中,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用费线程安全的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
4.2.1 Java监视器模式
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
在许多类中都使用了Java监视器模式,例如Vector和Hashtable。Java监视器模式的主要优势在于它的简单性。
public final class Counter{ @GuardedBy("this") private long value=0; public synchronized long getValue(){ return value; } public synchronized long increment(){ if(value == Long.MAX_VALUE) throw new IllegalStateException("counter overflow"); return ++value; } }
使用私有锁对象而不是对象的内置锁(或任何其他科通过公有方式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便(正确或不正确地)参与到它的同步策略中。如果客户代码错误的获得了另一个对象的锁,那么可能会产生活跃性问题。此外,要想验证某个公有访问的锁在程序中是否被正确的使用,则需要检查整个程序,而不是单个的类。
public class PrivateLock { private final Object myLock = new Object(); @GuardedBy("myLock") Widget widget; void someMethod(){ sychronized(myLock){ //访问或修改Widget的状态 } } }
4.4 在现有的线程安全类中添加功能
4.4.1 客户端加锁机制
对于由Collections.synchronizedList封装的ArrayList,这两种方法在原始类中添加一个方法或者对类进行扩展都行不通,因为客户端代码并不知道在同步封装器工厂方法中返回的List对象的类型。第三种策略是扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个"辅助类"中。
@NotThreadSafe public class ListHelper<E>{ public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public synchronized boolean putIfAbsent(E x){ boolean absent = !list.contains(x); if(absent) list.add(x); return absent; } }
这种方式不能实现线程安全性。问题在于在错误的锁上进行了同步。无论使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象。要想使这个方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。
客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的的锁来保护这段客户端代码。要使用客户端加锁,就必须知道对象X使用的是哪一个锁。
在Vector和同步封装器类的文档中指出,它通过使用Vector或封装器的内置锁来支持客户端加锁。
@ThreadSafe public class ListHelper<E>{ public List<E> list = Collections.synchronizedList(new ArrayList<E>()); ... public boolean putIfAbsent(E x){ synchronized (list){ boolean absent = !list.contains(x); if(absent) list.add(x); return absent; } } }
通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中,然而客户端加锁更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中。
客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。
4.4.2 组合
当为现有的类添加一个原子操作时,组合是更好的方法。
@ThreadSafe public class ImprovedList<T> implements List<T>{ public final List<E> list; public ImprovedList<T>(List<E> list){this.list = list} public synchronized boolean putIfAbsent(T x){ boolean contains = list.contains(x); if(contains) list.add(x); return !contains; } public synchronized void clear(){list.clear();} // ... 按照类似的方式委托List的其他方法 }ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一直的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。
相关推荐
本章主要介绍了Python中的组合数据类型,包括列表、元组、字典和集合。这些数据结构是Python编程中非常重要的部分,用于存储和操作多个数据项。 **4.1 列表** 列表是Python中最常用的数据结构之一,它是一种有序的...
第五版的《组合数学》书籍提供了系统性的理论和应用介绍,而第三章的内容通常会涉及更深入的计数技术和原理。在这个章节中,读者可能会接触到以下一系列的知识点: 1. **基本概念与定义**:首先,理解什么是组合、...
第四版的组合数学教材通常会涵盖基本概念、计数原理、二项式定理、鸽巢原理、容斥原理、排列与组合、部分有序集、生成函数、Burnside引理等核心内容。 答案详解部分可能包括对每个章节习题的解答步骤,帮助读者理解...
标题和描述中的“清华组合数学第四章答案”指向了清华大学教授的组合数学课程中的第四章节内容,该章节涉及置换群理论及其应用。这部分内容是组合数学的一个重要分支,旨在通过研究对象之间的置换来解决计数问题。从...
在第四版的《组合数学》一书中,作者深入浅出地介绍了这一领域的基本概念、定理和应用。这本书的习题答案对于学习者来说是一份宝贵的资源,可以帮助他们检验自己的理解,加深对理论知识的掌握。 1. **基本概念**:...
第4章可能讨论图论基础,包括图的基本概念(如路径、环、树和连通性),以及图的度数序列和欧拉路径。这些概念在网络分析、数据结构和算法设计中具有重要应用。 第5章可能涵盖了生成函数,这是一种强大的工具,通过...
课后习题是检验学习成果和深入理解知识的重要途径,这份资料提供了第1章至第4章的全部课后习题解答,对于学习者来说是极有价值的参考资料。 组合数学是研究有限集合中元素的不同组合方式及其性质的数学分支。它主要...
【第4章 类和对象】在Java编程中,类(Class)和对象(Object)是核心概念,构成了面向对象编程的基础。面向对象编程(Object-Oriented Programming, OOP)是一种编程范式,强调以对象作为程序设计的核心,通过封装...
这份资料包含了四个章节的答案,分别对应于第一章至第四章的内容。结合标签“组合数学”、“答案”和“习题”,我们可以深入探讨组合数学这一重要领域,并对每个章节涉及的知识点进行解析。 第一章:基础理论与计数...
《java并发编程实战》读书笔记-第3章-对象的共享,脑图形式,使用xmind8制作 包括线程安全类设计、实例封闭、线程安全性委托、现有线程安全类中添加功能和文档化同步策略等内容
4. **第四章**:二项式定理是组合恒等式的基石,它指出(a+b)^n的展开式是所有可能的a^k*b^(n-k)的组合,其中k从0到n。习题可能要求证明二项式定理,或者用它来计算特定幂次的展开项。 5. **第五章**:组合恒等式,...
组合数学是数学的一个分支,专注于研究离散对象的计数、排列与组合问题。它在计算机科学、统计学、概率论以及算法设计等领域有着广泛的应用。 ### 组合数学习题解答 这部分内容涵盖了Richard A. Brualdi教授编写的...
组合数学是数学的一个重要分支,主要研究有限集合中对象的排列、组合以及各种计数问题。这门学科在计算机科学中扮演着至关重要的角色,特别是在算法设计、概率论、图论、编码理论等领域都有广泛应用。这里提供的...
第4章 类剖析 第5章 类设计指导原则 第6章 利用对象实现设计 第7章 掌握继承和组合 第8章 框架与重用:使用接口和抽象类实现设计 第9章 构建对象 第10章 用UML创建对象模型 第11章 对象和可移植数据:XML 第...
组合数学是数学的一个分支,主要研究有限或离散对象的组合结构。其中,鸽巢原理、二项式系数、排列与组合、容斥原理、生成函数、递推关系和波利亚计数等概念是组合数学中的重要知识点。 首先,鸽巢原理也称为抽屉...
组合数学是数学的一个分支,它研究有限集合中对象的组合性质,涉及到计数问题、排列组合、二项式定理、图论等多个子领域。这本书的课后习题是学习过程中必不可少的部分,通过解决这些习题,学生能够深入理解并掌握所...
《组合数学》第四版是由Richard Generatingfunctionology所著的一本经典教材,这本书深入浅出地介绍了组合数学的基础理论和方法。 在解答《组合数学》第四版的问题时,我们通常会涉及以下知识点: 1. **组合计数**...
第五版的《组合数学》答案提供了一套详尽的解答,帮助读者深入理解和掌握相关概念。 1. **组合的定义与性质**:组合是指从n个不同元素中不考虑顺序取出k个元素的方法数,用C(n,k)或"n choose k"表示。组合的基本...
第四章可能深入到更高级的主题,如容斥原理、鸽巢原理、生成函数、递推关系或者斯特林数等。这些概念在解决复杂计数问题时非常有用。例如,容斥原理帮助我们精确计算包含与排除某些条件的元素的数量;而生成函数是一...