第3章 对象的共享

同步还有别一个重要的方面:内存可见性。(个人理解,对对象的修改在其他线程能立即看到)

失效数据:读到的数据已经失效(读到的是某线程修改该对象之前的数据)

在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

-----------------------------------------------------------------------------------------

当某个不应该发布的对象被发布时, 种情况就被称为逸出(Escape)。

局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

对于基本类型的局部变量,无论如何都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,因此Java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。

ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。

当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改。
  • 对象的所有域都是final类型的。
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)。

由于程序的状态总在不断地变化,你可能认为需要全盘和不可变对象的地主不多,但实际情况并非如此。在“不可变的对象”与“不可变的对象引用”之间存在着差异。保存在不可变对象中的程序状态仍然可以更新,即通过将一个保存新状态的实例来“替换”原有的不可变对象。

许多开发人员都担心这种方法会带来性能问题,但这是没有必要的。内存分配的开销比你想象的还要低,并且不可变对象还会带来其他的性能优势,例如减少了对加锁或都保护性副本的需求,以及降低对基于“代”的垃圾收集机制的影响。

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象必须通过安全方式来发布。
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

  • 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
  • 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  • 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象仅有接口来进行访问而不需要进一步的同步。
  • 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

同步策略规定了如何将不可变性、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁来保护。要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档。

当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。

不能首先更新一个变量,然后释放锁并再次获得锁,然后再更新其他的变量。因为释放锁后,可能会使对象处于无效状态。

依赖状态的操作:在某些对象的方法中还包含一些基于状态的先验条件(Precondition)。

Java监视器模式的主要优势就在于它的简单性。

由于每次调用getLocation就要复制数据,因此将出现一种错误情况——虽然车辆的实际位置发生了变化,但返回的信息却保持不变。这种情况是好还是坏,要取决于你的需求。

Java类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类:重用能降低开发工作量、开发风险(因为现有的类都已经通过测试)以及维护成本。

“扩展”方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分页到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。

客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。

设计阶段是编写设计决策的最佳时间。这之后的几周或几个月后,一些设计细节会逐渐变得模糊,因此一定要在忘记之前将它们记录下来。

原文地址:https://www.cnblogs.com/a0000/p/4759198.html