@@ 关键字 synchronized 可以用于实现原子性或确定 “ 临界区(Critical Section ) ” ,
同时同步还有另一个重要的方面:内存可见性(Memory Visibility)。
@@ 可以通过显式地同步或者类库中内置的同步来保证对象被安全地发布。
》》可见性
@@ 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。
@@ 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
@@ 在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行
一些意向不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行
判断,几乎无法得出正确的结论。
@@ 只要有数据在多个线程之间共享,就使用正确的同步。
### 失效数据
@@ 虽然在 Web 应用程序中失效的命中计数器可能不会导致太糟糕的情况,但在其他情况
中,失效可能会导致一些严重的安全问题或者活跃性问题。
### 非原子的 64 位操作
@@ 当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由
之前的某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-
thin-air safety)。
@@ 最低安全性适用于绝大多数变量,但是存在一个例外:非 volatile 类型的 64位数值变量
(double 和 long )
------------ Java 内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非 volatile
类型的 long 和 double 变量,JVM 允许将 64 位的读操作或写操作分解为两个 32 位的
操作。
------------- 当读取一个非 volatile 类型的 long变量时,如果对该变量的读操作和写操作在不同的
线程中执行,那么很可能会读取到某个值的高 32 位和另一个值的低 32 位。
------------- 即使不考虑失效数据问题,在多线程程序中使用共享且可变的 long 和 double 等类型
的变量也是不安全的,除非用关键字 volatile 来声明它们,或者用锁保护起来。
### 加锁与可见性
@@ 内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。
@@ 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量
的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
### Volatile 变量
@@ Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知
到其他线程。
------------ 当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此
不会将该变量上的操作与其他内存操作一起重排序。
------------ volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile
类型的变量时总会返回最新写入的值。
@@ 在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量
是一种比 synchronized 关键字更轻量级的同步机制。
@@ 从内存可见性的角度来看,写入 volatile 变量相当于退出同步代码块,而读取 volatile 变量
相当于进入同步代码块。然而,我们并不建议过度依赖 volatile 变量提供的可见性。如果在代码中
依赖 volatile 变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难以理解。
@@ 仅当 volatile 变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证
时需要对可见性进行复杂的判断,那么就不要使用 volatile 变量。
@@ volatile 变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的
可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)。
@@ 虽然 volatile 变量很方便,但也存在一些局限性。volatile 变量通常用做某个操作完成 、 发生中断
或者状态的标志。
@@ 加锁机制既可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性。
@@ 当且仅当满足以下所有条件时,才应该使用 volatile 变量:
----- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
----- 该变量不会与其他状态变量一起纳入不变性条件。
----- 在访问变量时不需要加锁。
》》发布与逸出
@@ “ 发布(Publish) ”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。
@@ 当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。
@@ 发布对象的最简单的方法是将对象的引用保存到一个公有的静态变量中,以便任何类和
线程都能看见该对象。(如下代码示例)
---------------------------------------------------------------------------------------------------------------
public static Set<Secret> knowSecrets ;
public void initialize ( ) {
knowSecrets = new HashSet<Secret>( ) ;
}
--------------------------------------------------------------------------------------------------------------
@@ 当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如
果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都
会被发布。
@@ 当某个对象逸出后,你必须假设有某个类或线程可能会误用该对象。这正是需要使用封装
的最主要原因:封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件
变得更难。
@@ 当从构造函数发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于
构造函数的最后一行也是如此。如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确
构造。
@@ 不要在构造过程中使 this 引用逸出。
@@ 在构造过程中使 this 引用逸出的一个常见错误是,在构造函数中启动一个线程。
--------- 在构造函数中创建线程并没有错误,但是最好不要立即启动它,而是通过一个 start 或
initialize 方法来启动。
@@ 在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法),同样会
导致 this 引用在构造函数中逸出。
@@ 如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造
函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程。
》》线程封闭
@@ 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享
数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭(Thread Confinement),
它是实现线程安全性的最简单方式之一。
@@ 当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身
不是线程安全的。
@@ 在 Swing 中大量使用了线程封闭技术。Swing 的可视化组件和数据模型对象都不是线程安全
的,Swing 通过将它们封闭到 Swing 的事件分发线程中来实现线程安全性。
@@ 为了简化对 Swing 的使用, Swing 还提供了 invokeLater 机制,用于将一个 Runnable
实例调度到事件线程中执行。Swing 应用程序的许多并发错误都是由于错误地在另一个线程中使用
了这些被封闭的对象。
@@ 线程封闭技术的另一种常见应用是 JDBC 的 Connection 对象。
--------- 在典型的服务器应用程序中,线程从连接池中获得一个 Connection 对象,并且用该对象来
处理请求,使用完后再将对象返还给连接池。
由于大多数请求(例如 Servlet 请求或 EJB 调用等)都是由单个线程采用同步的方式来处理,
并且在 Connection 对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式
在处理请求时隐含地将 Connection 对象封闭在线程中。
@@ 线程封闭是在程序设计中的一个考虑因素,必须在程序中实现。Java 语言及其核心库提供了
一些机制来帮助维持线程封闭,例如 局部变量和 ThreadLocal 类,但即便如此,程序员仍然需要
负责确保封闭在线程中的对象不会从线程中逸出。
### Ad-hoc 线程封闭
@@ Ad-hoc 线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc 线程封闭是
非常脆弱的。
事实上,对线程封闭对象(例如,GUI 应用程序中的可视化组件或数据模型等)的引用通常
保存在公有变量中。
@@ 当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。
在某些情况下,单线程子系统提供的简便性要胜过 Ad-hoc 线程封闭技术的脆弱性。
@@ 由于 Ad-hoc 线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该
使用更强的线程封闭技术(例如,栈封闭 或 ThreadLocal 类)。
### 栈封闭
@@ 栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。
@@ 局部变量的固有属性之一就是封闭在执行线程中,它们位于执行线程的栈中,其他线程无法
访问这个栈。
@@ 栈封闭(也被称为线程内部使用或线程局部使用,不要与核心类库中的 ThreadLocal 混淆)
比 Ad-hoc 线程封闭更易于维护,也更加健壮。
@@ 在维持对象引用的栈封闭性时,程序员需要多做一些工作以确保被引用的对象不会逸出。
@@ 如果在线程内部上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。然而,
要小心的是,只有编写代码的开发人员才知道哪些对象需要被封闭到执行线程中,以及被封闭
的对象是否是线程安全的。如果没有明确地说明这些需求,那么后续的维护人员很容易错误地
使对象逸出。
### ThreadLocal 类
@@ 维持线程封闭性的一种更规范方法是使用 ThreadLocal ,这个类能使线程中的某个值与保存
值的对象关联起来。ThreadLocal 提供了 get 与 set 等访问接口或方法,这些方法为每个使用该变量
的线程都存有一个独立的副本,因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。
@@ ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
@@ 通过将 JDBC 的连接保存到 ThreadLocal 对象中,每个线程都会拥有属于自己的连接。
示例:使用 ThreadLocal 来维持线程封闭性
-----------------------------------------------------------------------------------------------------------------------------
public static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>( ){
public Connection initialValue( ){
return DriverManager.getConnection( DB_URL ) ;
}
};
public static Connection getConnection ( ){
return connectionHolder.get( ) ;
}
----------------------------------------------------------------------------------------------------------------------------
补充:(1)、当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望
避免在每次执行时都重新分配该临时对象,就可以使用上面的这项技术。
(2)、当某个线程初次调用 ThreadLocal.get 方法时,就会调用 initialValue( ) 来
获取初始值。
@@ 假设你需要将一个单线程应用程序移植到多线程环境中,通过将共享的全局变量转换为
ThreadLocal 对象(如果全局变量的语义允许),可以维持线程安全性。然而,如果将应用程序
范围内的换粗转换为线程局部的缓存,就不会有太大的作用。
@@ 在实现应用程序框架时大量使用了 ThreadLocal 。例如,在 EJB 调用期间,J2EE 需要
容器需要将一个事务上下文(Transaction Context)与某个执行中的线程关联起来。通过将事务
上下文保存在静态的 ThreadLocal 对象中,可以很容易地实现这个功能:当框架代码需要判断当
前运行的是哪一个事务时,只需要从这个 ThreadLocal 对象中读取事务上下文。这种机制很方便,
因为它避免了每个方法都要传递执行上下文信息,然而这也将使用该机制的代码与框架耦合在一起。
@@ ThreadLocal 变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,
因此在使用时要格外小心。
》》不变性
@@ 满足同步需求的另一种方法是使用不可变对象(Immutable Object) 。
@@ 线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造函数创建的,只要它们
的状态不改变,那么这些不变性条件就能得以维持。
@@ 不可变对象一定是线程安全的。
@@ 可以安全地共享和发布不可变对象,而无须创建保护性的副本。
@@ 不可变性并不等于将对象中所有的域都声明为 final 类型,即使对象中所有的域都是 final
类型的,这个对象也仍然是可变的,因为在 final 类型的域中可以保存对可变对象的引用。
@@ 当满足以下条件时,对象才是不可变的:
------------ 对象创建以后其状态就不能修改。
------------ 对象的所有域都是 final 类型。
------------ 对象是正确创建的(在对象的创建期间, this 引用没有逸出)。
@@ “ 不可变对象 ” 和 “ 不可变对象的引用 ” 之间存在差异。
### Final 域
@@ final 类型的域是不能修改的(但如果 final 域所引用的对象是可变的,那么这些被引用的对象
是可以修改的)。
@@ 在 Java 内存模型中, final 域还有着特殊的语义:final 域能确保初始化过程的安全性,从而
可以不受限制地访问不可变对象,并在共享这些对象时无须同步。
@@ 仅包含一个或两个可变状态的 “ 基本不可变 ” 对象仍然比包含多个可变状态的对象简单。
@@ 正如 “ 除非需要更高的可见性,否则应将所有的域都声明为私有域 ” 是一个良好的编程习惯,
“ 除非需要某个域是可变的, 否则应该将其声明为 final 域 ” 也是一个良好的编程习惯。
### 示例:使用 Volatile 类型来发布不可变对象
@@ 每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类
来包含这些数据。(如下代码)
对数值及其因数分解结果进行缓存的不可变容器类
------------------------------------------------------------------------------------------------------------------------------------
@Immutable
class OneValueCache {
private final BigInteger lastNumber ;
private final BigInteger[ ] lastFactors ;
public OneValueCache ( BigInteger i , BigInteger[ ] factors ){
lastNumber = i ;
lastFactors = Arrays.copyOf( factors , factors.length ) ;
}
public BigInteger[ ] getFactors ( BigInteger i ){
if( lastNumber == null || ! lastNumber.equals( i ) ){
return nulll;
}else{
return Arrays.copyOf( lastFactors , lastFactors.length ) ;
}
}
}
---------------------------------------------------------------------------------------------------------------------------------------
@@ 对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在
一个不可变对象中来消除。
如果是一个可变的对象,那么就必须使用锁来确保原子性。
@@ 如果是一个不可变对象,那么当线程获得了对该对象的引用后,就必须担心另一个线程会
修改对象的状态。
如果要更新这些变量,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然
会看到对象处于一致的状态。
@@ 通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个 volatile 类型的引用
来确保可见性。
》》安全发布
### 不正确的发布:正确的对象被破坏
@@ 如果没有足够的同步,那么当在多个线程间共享数据时将发生一些非常奇怪的事情。
### 不可变对象与初始化安全性
@@ 由于不可变对象是一种非常重要的对象,因此 Java 内存模型为不可变对象的共享提供了
特殊的初始化安全性保证。
-------- 为了确保对象状态能呈现出一致的视图,就必须使用同步。
-------- 即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。
-------- 为了维持上面的初始化安全性的保证,必须满足不可变性的所有需求:
状态不可修改,所有的域都是 final 类型, 以及正确的构造过程。
@@ 任何线程都可以在不需要额外同步情况下安全地访问不可变对象,即使在发布这些对象时
没有使用同步。
然而,如果 final 类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态
时然需要同步。
### 安全发布的常用模式
@@ 可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须
使用同步。
@@ 要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确
构造的对象可以通过以下方式来安全地发布:
------------ 在静态初始化函数中初始化一个对象引用
------------ 将对象的引用保存在 volatile 类型的域 或者 AtomicReferance 对象中
----------- 将对象的引用保存在某个正确构造对象的 final 类型域中
----------- 将对象的引用保存到由锁保护的域中
@@ 在线程安全容器内部的同步意味着,在将对象放到某个容器。
@@ 尽管 Javadoc 在这个主题上没有给出很清晰的说明,但线程安全库中的容器类提供了
以下的安全发布保证:
----------- 通过将一个键或者值放入 Hashtable 、 synchronizedMap 或者 ConcurrentMap 中,
可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器
访问)
----------- 通过将某个元素放入 Vector 、 CopyOnWriterArrayList 、CopyOnWriterArraySet 、
synchronizedList 或者 synchronizedSet 中,可以将元素安全地发布到任何从这些容器
中访问该元素的线程。
---------- 通过将某个元素放入 BlockingQueue 或者 ConcurrentLinkedQueue 中,可以将该元素
安全地发布到任何从这些队列中访问该元素的线程。
@@ 通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder( 42 ) ;
静态初始化器由 JVM 在类的初始化阶段执行。由于在 JVM 内部存在着同步机制,因此通
过这种方式初始化的任何对象都可以被安全地发布。
### 事实不可变对象
@@ 所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时
的状态对于所有线程也将是可见的,并且如果对象状态不会再改变,那么就足以确保任何访问都是
安全的。
@@ 如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为
“ 事实不可变对象 ” 。 在这些对象发布后,程序只需将它们视为不可变对象即可。通过使用事实
不可变对象,不仅可以简化开发过程,而且还能由于减少了同步而提高性能。
@@ 在没有额外的同步的情况下,任何线程都可以安全地使用被发布的事实不可变对象。
### 可变对象
@@ 对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用
同步来确保后续修改操作的可见性。
要安全地共享可变对象,这些对象就必须被安全地发布,并在必须线程安全的或者由某个
锁保护起来。
@@ 对象的发布需求取决于它的可变性:
------------- 不可变对象可以通过任意机制来发布
------------ 事实不可变对象必须通过安全方式来发布
------------ 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
### 安全地共享对象
@@ 当发布一个对象时,必须明确地说明对象的访问方式。
@@ 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
-------- 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由
这个线程修改。
-------- 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何
线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
------- 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有
接口来进行访问而不需要进一步的同步。
--------- 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全
中的对象,以及已经发布的并且由某个特定的锁保护的对象。