迄今为止最好的手机用的IDEA版本是哪个版本?

参考了很多童老师团队的翻译,自己也看英文对照,有修改

迄今为止,我们讨论了线程安全和同步的一些基础内容。但是我们不希望每 次编写多线程程序的时候都要仔细分析每一个内存访问操作以确保我们的整个 程序是线程安全的(这样做也不太现实)。我们希望能够将一些线程安全组件组 合成更大的线程安全组件,从而构成整个程序。本章主要涉及那些使线程安全得以实现的类的构造方式和避免它们体内的线程安全被意外破坏的方式。

4.1设计一个线程安全类

虽然将程序中所有状态信息都保存在 public static修饰的字段中也可以写出线程安全的应用程序,但是相比于使用合适封装的程序,这样做很难验证程序的线程安全性,并且程序修改后很难保证它还是线程安全的。封装使得不需要检查整个程序的代码就能确定一个类是否是线程安全的。

线程安全类的设计过程应该包含如下三个基本元素: 

·识别组成对象状态的变量 

·识别对象状态变量之间的约束 

·建立一个管理对象状态的并发存取的策略

一个对象的状态源于它的字段。如果对象的字段全部是八个基础数据类型,那么这些字段就构成了对象的状态。下面的 Counter 类只有一个 long 类型的字段 value,因此 value 就构成了 Counter 对象的状态。一个对象的某个状态有n个字段组成,那么这个状态实际就是n元组的字段值构成的;一个对象的状态是2D面上的点的坐标,那么它就是(x,y)组成的。如果一个对象的某些字段是指向其他对象的引用的话,那么该对象的状态还包含被引用对象的状态字段。例如一个 LinkedList 对象的状态包含所有列表中节点对象的状态。

同步策略定义了一个对象如何协调控制对它的状态的访问,依次保证不破坏不变关系和后置条件。同步策略指定了使用哪些方法组合(包括不变类、线程封闭、锁等)来实现类的线程安全性以及那些变量应该被哪些锁对象保护。为了确保你写的线程安全类能够被分析和维护,你应该在类注释中记下你使用了什么样的同步策略。

4.1.1收集同步需求

要想将一个类设计成线程安全的,必须确保它在多线程访问条件下还能维持 其约束条件,这就需要搞清楚对象的状态关系。对象和变量的所有可能的状态组成其状态空间:即他们可呈现的状态的范围。状态空间越小就越容易分析,因此你应该尽量将所有的字段都声明为 final 的。

很多类都有约束条件,用以确定对象在某些的状态下是否是合法的。在Counter类里,value这个字段是long类型的。Long类型变量的取值范围是从Long.MIN_VALUE到Long.MAX_VALUE,但是Counter类对value值进行了限制,不能取负值。

类似地,一些操作可能会有后置条件,确认状态转换的时候结果无效。如果Counter的当前状态是17,那么唯一有效的下一个状态是18。当下一个状态是由当前状态推断出来的,那么这些个操作就一定是一个复合操作。当然并不是所有操作都利用了约束关系进行状态转换的限制;比如更新一个表示当前温度的变量,它的前一个状态就不会影响计算。

通过不变关系和后置条件在状态转换过程中设置约束,这样就会产生额外的同步或者封装需求。如果某些状态是不合法的,那么这些潜在的状态变量一定要被封装,否则客户端代码可能会把这些不合法的状态设置到对象中。如果一个操作有出现无效状态转换出现的可能性,那么就需要把它设计成原子性的。另一方面,如果一个类并未利用任何约束,我们还是可以放宽封装和序列化的相关要求的,这样能获得更好的弹性和更好的性能。

一个类里可能会有约束了多个状态变量的不变关系。像4.10图里的NumberRange类,就典型的展现出维护数字范围上下边界的两个变量的情况。这些变量必须遵循约束条件:下边界的值小于等于上边界的值。多变量间的不变约束关系产生了原子操作的需求:相关联的变量一定要通过一个单一的原子操作获取或者更新。你不能更新一个变量,然后释放和重新获得锁去更新其他的,这样做会导致在锁被释放的时候,对象处于一个不合法的状态。当多个变量参与到一个不变约束关系中,那么对相关变量访问的时候,在整个操作期间都要持有锁来保护。

如果你没有理解一个对象的不变关系和后置条件,你就无法确保线程安全。对状态变量的有效值或者转换具有约束条件,那么就会产生原子性操作和封装的需求。

4.1.2状态依赖操作

类的不变约束关系和方法后置条件约束了一个对象的有效状态和状态转化过程。有的时候对象还会涉及到方法前置条件。 例如你不能删除一个空队列中的元素,在执行删除元素方法之前,队列对象必须处于非空状态。具有基于状态的前置条件的操作被称为状态依赖操作。

在单线程程序中,如果一个方法的前置条件不满足,则该方法将会运行失败, 但是在并发程序中,可能需要等待其他线程的动作在稍后将前置条件变为 true。因为并发程序增加了通过等待实现前置条件被设定为true的这种可能性,所以接下来就能继续执行本线程的操作。

在java中有效率的等待条件变为true这步是由有内置机制的--wait 和 notify 方法--是和对象的内部锁紧密绑定的,它们很难使用。要实现这种等待条件变为true的操作, 最好使用一些 Java 类库中的类,例如阻塞式队列或者信号,来提供被期望的状态依赖行为。阻塞类库(像BlockingQueue,Semaphore,或者其他的同步类)将会在第五章被介绍。在第十四章中将会介绍使用最低级别的由平台和类库提供的机制,来创建状态依赖类。

我们在4.1章中曾暗示一个对象的某个状态可能是对象视图中字段的子集。为什么可能是个子集?在什么条件下,来自对象的字段都是可达的?

当我们要定义哪些变量组成了对象的状态,我们想要只考虑对象所拥有的数据。所有权这个概念在语言中并没有明确体现出来,但是在类的设计中确实很重要的元素。如果你要创建了一个 HashMap 对象,并向其中添加了一些键值对,那么事实上你创建了很多对象:一个 HashMap 对象,一组被HashMap使用的 Map.Entry 对象,可能还有其他一些内部对象。HashMap 对象的逻辑状态由这些 Map.Entry 对象和其他一些内部对象的状态组成,即使他们是作为不同的对象实现的。

不管怎样,垃圾回收器使我们不用过多地考虑对象所有权的问题。在C++里当你向一个方法传递一个对象时,你要相当仔细的考虑你是否转化所有权,是短期借给还是长期联合拥有所有权。在java里,所有这些所有权模型都是可能出现的,但是垃圾回收器减少了在索引共享时经常出现的问题,避免了对所有权过多地考虑。

很多时候,对象所有权和封装紧密相连--对象封装并拥有他自己的状态。给定状态变量的拥有者决定用何种锁方式来维护状态变量的完整性。所有权意味着控制权,但是一旦你将一个可变对象的引用发布出去,其他类也拥有了控制权,顶多你能有“共享的所有权”。一般来说一个类的方法和构造函数不拥有传入参数引用的对象的控制权(不要修改参数对象的状态),除非该方法是专门被明确的设计用来传递控制权的(像synchronized集合包装工厂方法)。

集合类往往展现出“所有权分裂”这种形式,集合拥有自己的状态,但是客户端代码拥有集合中存储的元素的状态。例如 servlet框架中ServletContext 类。它就提供了类似于 Map 的功能,作为容器服务servlets。它提供了 setAttribute 和 getAttribute 方法,用于按名字注册和取回对象。ServletContext 这个类是由servlet容器实现的,一定要是线程安全的,因为它必然会被多线程访问。在调用 setAttribute 和 getAttribute 方法不需要使用同步机制,但是使用 ServletContext 类中存放的对象就需要使用同步机制了。这些对象被应用所拥有,并作为应用的相关状态的代表被servlet容器安全存储维护。就像所有共享对象一样,他们一定要被安全地共享;为了阻止多线程并发访问相同对象造成的干扰,他们一定要是线程安全的,或者有效不可改变,或者明确的被锁保护。

即使一个对象不是线程安全的,你也可以使用一些技术,使其可安全用在并发程序中。你可以确保它只会被单一的线程访问(线程封闭技术)或者使用锁来保护所有对该对象的访问操作。

封装通过推动实例封闭简化了线程安全类的设计。当一个对象被封装在另一个对象中的时候,所有访问该被封装对象的访问路径都是可见的,因此相比于在整个程序中访问对象更容易分析。组合使用实例封闭和锁可以设计出线程安全类。

将数据封装到对象中,就是封闭了对该数据的访问,只能通过对象方法。这样就很容易确保被封装数据的存取操作被特定的锁保护。

被封闭的对象不能逃逸其应该在的范围。一个对象可能局限于一个类的实例(像私有的类成员)、一个归属范围(例如局部变量)或者是一个线程(例如一个对象被从一个方法中传递到另一个方法中,只在一个线程内进行操作,不应该跨线程共享)。对象当然自己是跑不出设定的范围的,--当然,除非开发者帮了他们,开发者发布对象时超出了他们应该的范围就会造成逸出的后果。

下面代码中的 PersonSet 类演示了即使在组成它的状态变量不是线程安全的前提下,如何使用实例封闭和锁来设计出线程安全类。PersonSet 对象的状态由一个 HashSet 对象 mySet 来表示。尽管状态变量 mySet 不是线程安全的,但是由于 mySet 是 private 的,因此这个 HashSet 对象被封闭在类 PersonSet 对象中。能够访问 mySet 的路径只有两条:addPerson 方法和 containsPerson 方法,由于这两个方法都有锁保护。所有的状态都被内置锁保护,所以 PersonSet 类是 线程安全的。

这个例子并没有设想Person对象是否是线程安全的,但是如果它是可变的,那么从PersonSet中获取Person对象进行访问的时候就需要额外的同步机制。最可靠的做法就是使Person线程安全;最不靠谱的做法就是用锁来保护,确保客户端在访问Person之前都要获得合适的锁,遵循这个协议。

实例封闭是创建线程安全类的一个最简单的方式。它也允许灵活地选择加锁 策略。PersonSet使用它自己的内置锁来保护他的状态,当然人和锁,只要被一致性的使用都能实现这样的功能。实例封闭也允许不同的状态变量被不同的锁保护。(例如一个类使用多个锁对象来保护他的状态,见236页的ServerStatus)

在平台内部类库中有许多封闭的例子,包括一些单独唯一的类,能够把非线程安全类转变成线程安全类。像ArrayList和HashMap这种基础的集合类都不是线程安全的,但是类库中提供了包装工厂方法(Collections.synchronizedList和friends),通过这些方法它们可以在多线程环境下安全的使用。这些工厂使用了装饰者模式,用一个sunchronized封装对象来包装集合;这种封装实现了把每个合适的接口对应的方法都实现为synchronized方法,并将请求转发给底层的集合对象。只要这个封装对象还控制着唯一能够访问底层集合的引用,(底层集合被封闭到包装对象中)那么这个封装对象就是线程安全的。Javadoc文档中对于这些方法做出了提示:所有对底层集合的访问都要通过封装对象。

当然,通过发布一个本来被期望封闭的对象也可能会违背封闭的初衷。如果一个对象本来是准备被封装到特殊区域内,那么如果让它从那个范围内逃逸就是一个bug。通过发布别的对象,例如iterator和内部类的实例,也会间接的发布封闭对象,造成它的逃逸。

封闭使构建线程安全类变得简单,因为一个类的状态被封闭后在不需要检验整个程序的情况下就能分析是否是线程安全的。

遵循实例封闭的原则,你可以使用 Java 监视器模式来设计线程安全类。你 需要将所有可变状态域都封装起来,然后用对象的内部锁来保护它们。

4.1 节中的 Counter 类就是使用 Java 监视器模式设计的类。它封装了一个状态变量,所有的访问都要通过Counter里的synchronized方法。

Java监视器模式被许多类库中的类使用,像Vector和HashTable。有时候也需要更复杂的同步策略;第十一章就展示了如何通过细粒度锁策略改善扩展性。Java监视器模式最主要的优点就是简单。

Java 监视器模式只是个习惯用法,事实上不一定非要用对象的内部锁,其他锁也可以,只要被一致使用就行。如下代码演示了使用私有锁来保护对象状态的情况。

使用私有锁对象而不是内部锁有很大的好处(或者是其他公共可以访问的锁)。使用私有锁使得客户端代码无法获取该锁对象,可是要是用public这样的锁就会允许客户端代码参与到同步策略中--是正确或者错误的。客户端不合理的拿到了其他对象的锁可能会造成活跃性问题,并且如果要验证公共可访问的锁使用的合不合理,需要检验整个程序而不是单单一个类。

4.2.2例子:跟踪车队里的车辆

4.1 节的 Counter 类太简洁琐碎了,我们来看一个稍微复杂一点的例子。我们要创建一个车辆追踪器类,用来分派车队车辆,像出租车,警车,卡车之类的。首先我们将使用 Java 监视器模式来创建它,然后看看如何放松一些对封装需求,同时保持线程安全性。

每个车辆都由一个 String 字符串标识,并且每个车辆都由一个坐标(x,y) 表示其位置。VehicleTracker 类封装了所有已知车辆的标识符和位置,使得这些车辆在model-view-controller类型的GUI应用中成为一种非常合适的数据模型,在应用中这些对象会被一个视图线程和多个更新线程共享。视图线程会获取车辆的名字和位置并把他们发送给展示的地方:

类似地,更新线程会根据GPS设备修改车辆的位置或者是由一个调度员通过GUI接口手动输入:

尽管视图线程和更新线程会同时并发地访问数据模型,但是这一定是线程安全的。在图4.4中就展示了用java监视器模式实现的车辆追踪,并且使用了4.5中的MutablePoint作为车辆位置的代表。

当我们需要返回给调用者一个车辆位置的时候,我们会通过MutablePoint的复制构造函数或者deepcopy方法来把合适的值复制拷贝返回,会创建一个新的Map集合,里面的值都是从老的Map中键和值复制出来的。

这种实现方式通过在返回客户端之前复制可变的数据来维护线程安全,在大多数情况下这都不会引起性能问题,但是当列表非常大的时候就可能有问题了。保护性拷贝引起的另一个问题是, getLocations 方法返回的 Map 是不会随着 MonitorVehicleTracker 对象的状态 变化而变化的。这种特性好于不好取决于你的需求。当对于位置集合有内部一致性需求的时候,这种方式可能是有益的。但在一些情况下,比如调用者需要每个车辆最新的位置信息,此时返回一致性快照是错误的,或者说是一个缺陷,因此需要经常不断地刷新快照。

除了那些最细碎的对象,大多数对象都是组合对象。Java 监视器模式适合 用于从底层开始构建线程安全类或者封装一个或多个非线程安全子组件来构建 线程安全类。但是,如果子组件就是线程安全的,我们还需要再加上一层线程安 全性吗?答案是这需要具体情况具体对待。在一些案例中由线程安全组件组成的组合物是线程安全的(图4.7和图4.9),但在另一些情况下这仅仅是一个好的开始(图4.10)。

4.3.1例子:使用委托的车辆跟踪

就像很多委托所做的那样,我们现在来构建一个委托给线程安全类的车辆跟踪新版本。我们在一个Map中存储车辆的位置,所以就从实现一个线程安全的Map--ConcurrentHashMap开始。正如图4.6中我们会用不可变的Point替代MutablePoint来存储位置信息。

因为Point是不可变的,所以是线程安全的。不可变的值可以被自由的共享和发布,所以我们在返回他们的时候不再需要复制了。

如果我们使用了原来的MutablePoint替代Point类,我们可能就会因为在getLocations方法中发布了可变状态的引用,导致封装被打破变得不再线程安全。注意到了这一点,所以我们对车辆跟踪类轻微地改变了一些行为。正如对比能看到的监视器版本返回了位置的快照,委托版本返回了一个不可修改的但是“活跃”状态车辆位置视图。这就意味着如果线程A调用了getLocations方法,线程B稍后修改了一些点位的信息,那么这些修改也会被反映在返回给A的Map中。正如我们之前说明的,根据你的需求,这种方式可能是使你受益(最新的数据)或者使你有些损失(与车队不符的潜在可能性)。

如果需要的是一个车队不可修改的数据视图,getLocations方法可以返回locations这个Map的浅复制数据。由于Map的内容是不可修改的,所以只有Map的构造会被复制,就像图4.8那样。(返回了一个普通的HashMap,getLocations方法并未承诺返回一个线程安全的Map)。

4.3.2独立的状态变量

上一节的代理技术将线程安全责任代理给单个线程安全的状态变量。我们也可以将线程安全责任代理给多个线程安全的状态变量,只要这些状态变量是相互独立的。独立的意思是说组合类不在多个状态变量之上加任何约束条件。

如下代码中的 VisualComponent 类是图形化组件,用来让客户注册鼠标键盘触发事件。它包含了已注册的每种类型的监听者的列表,这样当一个事件发生,就会找到合适的监听者去执行。但是鼠标监听者集合和键盘监听者集合没什么关系,它们是相互独立的。因此 VisualComponent 类可以将它的线程安全性责任代理给这两个底层线程安全集合。

上例中每个列表都是 CopyOnWriteArrayList 类型的,这是个线程安全类, 非常适合用于管理监听器列表(见章节5.2.3)。由于两个状态变量都是线程安全的,并且没有约束条件加在两者之上,因此 VisualComponent 类可以将线程安全性责任代理给它们,换句话说,VisualComponent 虽然没有使用锁,但仍是线程安全类。

4.3.3当委托失效的时候

大多数组合类并不像上例所示的那么简单,它们在多个状态变量之间加约束条件,例如如下的NumberRange类,使用了两个AutomicInteger来管理状态,但是它要求lower的值总是小于或等于upper的值。

方法都尝试尊重这种不变约束,可惜做的太差了。他们两个都是“检查然后执行”的执行序列,但是却并没有使用锁使二者是原子性的。如果原本Numberrange的范围是0-10,此时一个线程调用setLower设置较小值为5,另一个线程调用setUpper设置较大值为4,那么在某些较为幸运的时刻,两个方法里的检查操作都通过了,之后修改的那步也执行了,我们看到的范围就变成了5-4,一个不合法的范围。所以尽管底层AutomicInteger是线程安全的,但是组合类可就不是了。因为底层两个状态变量lower和upper并不是独立的,所以NumberRanger不能简简单单把线程安全委托给线程安全状态变量。

NumberRange可以通过使用锁维护不变的约束来实现线程安全,例如用一个公共锁来保护lower和upper。还必须要避免把lower和upper发布给客户端,避免到他们的约束关系。

如果一个类有复合的行为操作,就像NumberRange里的做法,单独的委托也不再是实现线程安全的合适方法了。在这些情况下,类一定要它自己的锁,来确保复合操作是原子性的,除非整个复合操作都被委托给底层的状态变量了。

如果一个类是由多个独立的线程安全状态变量组成的并且没有任何需要无效状态过渡判断的操作,那么就可以把线程安全委托给底层状态变量了。

上边讨论的这个问题和我们之前在3.1.4章节里描述的volatile变量的规则之一很相似:只有在某个变量未参与到其他变量的不变约束条件的时候,用volatile才是合适的。

4.3.4发布底层状态变量

如果你将组合对象的线程安全性代理给状态,那么在什么情况下你可以发 布这个状态来让其他类也能修改他们呢?这取决于类里面是否在这些状态上加约束条件。例如 Counter 类中的 value 本来底层的变量是可以取任意值,但是Counter约束它不能是负数,并且增加的那步操作被限制一套有效的状态值内。如果你发布了这个value字段,则客户端代码就有可能将其设置为负数,从而使 Counter 对象处于非法的状态。另一方面,如果一个变量代表了当前温度或者上一个用户登录的ID,在另一个类中在任意时间内对进行修改,可能并不会违背约束关系,这个时候发布这个变量就是可以被接受的(这其实并不是一个好主意,因为发布可变的变量逼迫着未来的开发人员以及压迫子类的能力,但是让这个类非线程安全也不是必须的)。

如果一个状态变量是线程安全的,并且它的值并未受到任何约束,对它的任何操作也没有被禁止的转化过渡状态,那么它就可以被安全的发布。

4.3.5例子:车辆跟踪发布它的状态

让我们来构建车辆追踪器类的另一个版本,这个版本我们发布了它底层的可变状态。我们又一次需要对接口做点修改了,才能容纳这些改变。这次使用的位置对象是线程安全的可变对象,代码如下:

get 方法返回了一个包含两个元素的数组,如果将其拆分为 getX 和 getY 两个方法,就有可能存在两个方法调用之间x,y坐标值其中一个被其他线程改变的情况:车辆从未到达过的location位置坐标。使用 SafePoint 类, 我们就可以构建出一个可发布底层可变状态的且未破坏线程安全的车辆追踪器类,代码如下:

这个类将线程安全性代理给底层的ConcurrentHashMap,但是这次 Map 中的值对象是 SafePoint 类型,这是个线程安全的可变类而不是不可变的。getLocations 方法返回了一个不能修改的 Map的拷贝,调用者不能向这个 Map 中插入键值对,或者从该 Map 中删除键值对,但是可以通过改变map里车辆对象的状态来改变返回的Map。又一次正如之前说的,根据你的需求来判断Map这种“活着”的状态是有益的还是有害的。PublishingVehicleTracker 类是线程安全的。但是如果为PublishingVehicleTracker类加上约束,来决定某些车辆位置是否有效就不一定是线程安全的了。如果需要能够否决车辆位置的改变后者在位置改变时采取一些行为,这个类的方式就不再合适了。

4.4为已存在的线程安全类添加功能

编程的时候往往会发现 Java 类库中已经存在了许多有用的“积木”(线程安全类),相比于创建新的类,重用这些类库中的类更受人喜欢:重用可以降低开发难度,减少开发风险(因为已经存在的组件已经被检验过了),维护的花销。有时一个线程类拥有所有我们想进行的操作,可是大多数时候我们还要添加一些功能,并维持其线程安全性。

例如我们需要一个线程安全 List,让其拥有一个原子的“如果是空就放置进去”操作。Java 类库中的 Synchronized List 几乎已经能完成所有的工作,它们提供了 contains 和 add 等方法,我们只需要添加一个“如果是空就放置进去”方法就可以了。

“如果是空就放置进去”这个概念直接点说--在想集合中添加一个元素前先判断下是否存在这个元素,如果已经存在就不要添加进去。(你的“检查然后执行”这个警告铃应该被扔掉了)对类的线程安全的需求隐式的添加了另一个需求--像“如果是空就放置进去”这样的操作一定是原子性的。合理的解释是:如果你向一个List中添加X对象两次,并且原来这个集合里不包含这个对象,结果也只会有一个对象出现在集合中。但是如果“如果是空就放置进去”这样的操作不是原子性的,那么某些不幸的时刻,两个线程都会发现集合是没有X的,都添加了X,结果造成了X的两个拷贝。

最简单的办法是到原有的class文件中添加一个新的需要的原子操作,但是你没有源码,也不允许修改 Java 类库中的类。如果你能修改源代码,你需要理解实现类的同步策略,这样才能在保证和原有设计一致的基础上进行功能提高。直接向一个类中添加新的方法意味着那个类中,实现同步策略的所有代码都仍要保留在源文件中,使其更容易理解和维护。

另一个办法是扩展这个类(假设它是可扩展的), 下面的代码中 BetterVector 类扩展了 Java 类库中的 Vector 类,添加了 putIfAbsent 方法。扩展 Vector 类很直接了当的,但是不是所有的类都会向子类暴露足够多的状态值,以支持这种扩展。

此外,扩展法比直接修改源码的方法更脆弱,因为现在你将同步策略放在多 个分隔的被维护的源文件中实现。如果底层class类通过选择了不同的lock来保护它的各个状态来改变同步策略,那么子类就会悄悄地巧妙的被破坏掉,因为子类没有使用正确的锁来控制对基础的类的状态的并发访问。(Vector的同步策略由其规格要求确定,所有BetterVector不会受这个问题的影响)

一个ArrayList由Collections.synchronizedList封装后,上边那两种方法--添加一个方法到原有的类中或者扩展原来的类--都不能使用了,因为客户端代码不知道从同步封装工厂返回的这个List对象里面的结构是什么样的。于是有了第三种方法,通过在“helper”类中使用扩展代码,在没有扩展类的基础上来扩展它的功能。

下面的代码演示了这个方法,不过这个实现是有问题的,不能保证线程安全性。

内部锁。ListHelper只是给我们一种同步的假象;各个list的操作尽管被同步控制,但是却是被不同的锁控制,这意味着在List中putIfAbsent方法和其他操作方法之间并不是原子性不被干扰的关系。因此,无法保证当一个线程在执行 putIfAbsent 方法的时候另一个线程不会执行 list 中的其它方法。

要解决这个问题,putIfAbsent 方法必须使用与 list 使用相同的锁,通过使用客户端锁或者外部锁来实现。客户端锁意味着在保护客户端代码时,使用对象X作为锁来保护X自己的状态的线程安全。为了使用客户端锁,你必须知道对象X使用了什么锁。

虽不是直接说明的,但是Vector 类和 Synchronized 包装类的规格说明中提到它们支持将它们的内部锁用作客户端锁。下面的代码演示了修改后的版本。

这种方式仍然很脆弱(比扩展一个类还脆弱),它将类C的锁的代码放到了与类 C 关系不大的类中。对于这种没有提交锁策略的类,在使用客户端锁执行的时候一定要小心。

客户端锁和继承类的方式有很多共同之处--他们都把派生出来的类和基础类的实现联结起来。就像继承违反了实现封装的原则,客户端锁也违反了同步策略的封装理念。

在被传入一个list到构造函数之后,客户就不会使用底层的list里的方法,而是通过ImprovedList 进行访问和操作)

ImprovedList 类使用其内部锁添加了额外的一层同步机制。它不关心底层的 List 是否是线程安全的,因为它提供了他自己一致的锁来实现线程安全,即使内部的List不是线程安全的,或更换了锁的实现,都不会影响到 ImprovedList 类的线程安全性。虽然额外的一层会对性能造成小的损失,但是这样设计出的类比去模拟其他的类的锁策略要靠谱的多。事实上我们使用了 Java 监视器模式封装了一个 List 对象,并提供了自己的安全策略, 只要不将这个底层的List 对象发布出去,就能保证 ImprovedList 的线程安全性。

4.5用文档记录下使用的同步策略

文档注释是管理线程安全性的最有力的工具之一(不得不说,未被充分利用的工具)。用户在使用类的时候会查看文档注释,以确定其是否是线程安全的。维护者修改代码之前也需要查看文档注释,以理解该类使用了什么样的同步策略,避免维护的时候不经意间违背了安全。不幸的是,文档阅读者们在文档中找到的信息比他们希望得到的要少得多。

为类的客户记录这个类的线程安全保证;为类的维护者记录他的同步策略。

每一次使用synchronized,volatile或者其他的线程安全类都是对同步策略的一种反应,同步策略能够确保面对并发访问的时候被访问数据的完整性。同步策略是程序的一部分,应该被记录在文档注释中。当然,最好在设计类的时候将同步策略记录下来,不然过几周之后就就会忘掉当初的设计意图。

选择同步策略并不那么容易,你需要做很多决定:哪些变量用 volatile 修饰,哪些变量用锁保护,用哪个锁保护哪个变量,哪些对象应该是不可变的或者需要封闭在单线程中,哪些操作应该具有原子性等等。这些细节实现是非常严格的,你必须将其记录下来,给日后的维护者使用。但是一些对类中公可被观察的锁行为造成影响的,也应该被作为特殊情况记录下来。

你至少应该在文档注释中说明这个类是否是线程安全的。是不是必须先获取 某个锁才能使用它?是不是有特殊的锁会影响它的行为?不要让用户冒着风险去猜。如果你创建的类不想支持客户端锁就明确写出来。如果你想要客户能够在你的类基础上去创建原子性操作,就像4.4章我们说过的那样,你需要记录下他们需要获得哪个锁来保证安全。如果你使用了锁来保护状态,你也应该记录下来,之后的维护者需要这些信息。在这方面@GuardedBy 标注用起来比较顺手。如果你在维护线程安全时使用了精妙的方式,也要记录下来,因为对于维护者来说这些操作并不是显而易见的。

目前的现状是,在线程安全文档中甚至即使是 Java 平台库中的类都没有很好的记录线程安全性方面的信息。有多少次你看一个类的Javadoc时会怀疑这个类是不是线程安全的?大多数类都没有提供任何线索。很多官方的 Java 技术规格说明例如 Servlet 和 JDBC 也没有详细记录这方面的信息。

虽然理智告诉我们规格文档中没有说明的时候,我们不能假设那些行为是存在的,可是为了完成工作,我们总是要面对选择糟糕的假设。当一个类看上去该是线程安全的,但 Documentation 中没有提到的时候,我们应该将其假定为线程安全的吗?我们能够假设通过先获得锁再访问对象这样是线程安全的么?(只有当我们控制好所有访问这个对象的代码,这种风险技巧才能起作用;否则,它也仅仅是提供了线程安全的错觉)没有令人满意的选择。

更糟的是,当我们进行这样的猜测的时候我们的直觉往往是错的。例如 java.text.SimpleDateFormat 类不是线程安全的,但是在 JDK1.4 之前 Javadoc 都没有明确说明这一点。当 JDK1.4 发布之后,很多开发者感到很惊讶,他们一直认为这个类是线程安全的。有多少程序错误的构建的非线程安全对象的共享实例,并且在多线程中使用,在高负载的情况下可能会造成错误的结果。

一种解决这类问题的办法是,只要文档中没有说这个类是线程安全的,就默 认其为非线程安全的。另一方面,如果不假定 HttpSession 类是线程安全的,就 没法开发基于 Servlet 的应用程序,虽然这种假设有很大问题。因此,你定义的类应该明确说明其是否是线程安全的,不要让你的客户或同事去猜。

4.5.1解读模糊不清的文档

很多 Java 技术规格说明都不愿说明它的一些接口是否是线程安全的和接口的实现需求,或者至少没有准备好如何说明。例如 ServletContext、HttpSession 或者 DataSource。这些接口一般是被容器提供者或者数据库供应商实现的,你看不到他们的源代码。此外你也不想依赖于某个特定的JDBC driver的特定实现细节--你想遵循标准规格说明,这样就可以兼容不同的具体实现。但是在 JDBC 规格说明中根本就没提到“并发”或者“线程”,Servlet 规格说明中也很少提到,那该怎么办呢?

这样一来你只有猜了,一种比较好的能够提高你猜测准确度的方法是,假定你是规格说明的实现者(例如一个容器或者数据库开发商),而仅仅不是一个使用者。Servlet总是被一个容器管理的线程调用的,假设它会被不止一个这样的线程访问因此是安全的,容器知道确实如此。Servlet容器使某些对象为多个servlets提供服务,例如HttpSession或者ServletContext。所以servlet容器会等待这些对象被并发访问,因为它创建了多个线程,并调用了像Servlet.service这样的方法,合理的访问ServletContext。

既然这些类一般是用在多线程环境中的,我们应该假定它们是线程安全的,即使规格说明中没有明确说明这一点。另外,如果要使用客户端锁来保护 HttpSession 和 ServletContext 对象,用哪个锁来保护?文档没有说明,也很难猜到。根据ServletContext和HttpSession 的规格说明和访问教程,你会发现最合理的猜测就是,不需要使用任何客户端锁。

规格说明没有告诉我们机制--如何协调对这些共享属性的并发访问。因此这些存储在代表web应用的容器中的对象,应该是线程安全的或者是有效不可变的。如果所有的容器所做的就是存储这些代表了web应用的属性,那么当来自servlet应用代码的访问发生的时候就需要确保他们被锁一致性的保护起来。但是因为容器可能会想要把HttpSession中的对象序列化,为了复制或者钝化等目的,这时servlet容器就可能不太清楚你的锁机制了,需要你自己实现线程安全。

对于 JDBC 中的 DataSource 类也可以做类似推断。一个 DataSource 对象表示一个数据库连接池,里面是可重复使用的数据库连接。DataSource为应用提供了服务,在单线程环境中使用它就没什么道理了。很难想象不设计多线程中调用getConnection方法的一个使用案例。并且,和servlet一样,JDBC的规格说明也没有说明要使用任何客户端锁来保护 DataSource 对象。因此尽管规格说明中没有明说 DataSource 是线程安全的,也没有要求容器开发商提供一个线程安全 的实现,就像我们讨论的--“如果不是线程安全那不是扯淡么”,这样我们就没有选择只能假设 DataSource 是线程安全的,DataSource.getConnection 方法调用不需要任何客户端锁的保护。

另一方面对于由DataSource分配的JDBC连接对象,我们不会做出相同的推论,因为在他们被返回给池子之前,他们并不一定是准备被很多行为共享的。所以如果一个行为跨多线程获得了一个JDBC连接,就必须使用同步机制进行保护对这对象的访问(在大多数应用中,使用JDBC连接的都是被限定于特别的线程中来实现的)。

再过10天就是我老婆生日了.
因为是20岁.所以想要给她过个大的.
但是很悲催地在地铁上被偷了很多现金和iphone和itouch所以现在很困窘.
本想着给她放场盛大的烟花的.但是我这经济能力....实在是...
20岁之前她没遇见我,20岁想要准备20份礼物.不用很贵价.但是真的很需要心意..


大家不要弱弱给一个就飘走了 TAT

希望各位大神用过的神马浪漫点的idea可以给鄙人出出主意,

版权所有:沈阳市铁西区爱莲产后护理中心   技术支持:

我要回帖

更多关于 迄今为止最好的手机 的文章

 

随机推荐