java.util.concurrent
3.1 可见性
可见性是一种复杂的属性,因为可见性中的错误总是违背我们的直觉。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
在下面的清单中 NoVisibility 说明了当多个线程在没有同步的情况下共享数据出现的错误。主线程启动读线程,然后将 number 设为 42,并将 ready 设为 true。读线程一直循环直到发现 ready 的值变为 true,然后输出 number 的值。虽然看起来会输出 42,但事实上可能输出 0,或者根本无法终止。这是因为代码中没有使用足够的同步机制,因此无法保证主线程写入的ready 值和 nunber 值对于读线程来说是可见的。
public class NoVisibility { 【皱眉脸-不要这样做】
private static boolean ready;
private static int number;
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
private static class ReaderThread extends Thread {
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
}
重排序(Reordering)”。(注释:这看上去似乎是一种失败的设计,但却是使 JVM 充分地利用现代多核处理器的强大性能。)
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
3.1.1 失效数据
失效数据。当读线程查看 ready 变量时,可能会得到一个已经失效的值。除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。更糟糕的是,失效值可能不会同时出现:一个程序可能获得某个变量的最新值,而获得另一个变量的失效值。
意料之外的异常、被破坏的数据结构、不精确的计算以及无限循环等。
在如下程序清单 Mutableinteger 不是线程安全的,因为 get 和 set 都是没有同步的情况下访问 value 的。如果某个线程调用了 set,那么另一个在调用的get 线程可能会看到更新后的值,也可能看不到。
public class MutableInteger {
private int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
在程序清代 SynchronizedInteger 中,通过对 get 和 set 方法进行同步,可以使MutableInteger 成为一个线程安全的类。仅对 set 方法进行同步时不够的,调用 get 线程仍然会看到失效值。
public class SynchronizedInteger {
private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
3.1.2 非原子的64位操作
忽略。。。
3.1.3 加锁与可见性
内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。对于同一个锁,后面进入锁的线程可以看到之前线程在锁中的所有操作结果(加锁可以保证可见性)。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步
3.1.4 Volatile变量
对于volatile 关键字的详细介绍,建议大家去仔细观看 volatile关键字解析 ,所以在这不做介绍。
3.2 发布与逸出
发布(Publish)”一个对象的一起是指,是对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。在许多情况中,我们要确保对象及其内部状态不被发布。而在某些情况下,我们又需要发布某个对象,但如果在发布时要确保线程安全性,则可能需要同步。当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。
发布对象最简单的方法就是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象,如下。发布一个对象
public class KnownSecrets {
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}
}
程序清单:是内部的可变状态逸出:
public class UnsafeStates {
private String[] states = new String[] {"AK","AL"...};
public String[] getStates() {
return states;
}
}
如何按照上述方式来发布 states,就会出现问题,因为任何调用者都能修改这个数组的内容。在这个实例中,数组 states 已经逸出了它所在的作用域,因为这个本应是私有的变量已经被发布了。
当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。
3.3 线程封闭
线程封闭(Thread Confinement),它是实现线程安全性的最简单方式之一。
线程封闭技术的常见应用时 JDBC 的 Connection 对象。线程从连接池中获得一个 Connection 对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求都是由单个线程采用同步的方式来处理,并且在 Connection 对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在处理请求时隐含地将 Connection 对象封闭在线程中。
3.3.1 Ad-hoc线程封闭
略...
3.3.2 栈封闭
栈封闭式线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与核心类库中的 ThreadLocal 混淆)。
方法都无法获得基本类型的引用,因此Java 语言的这种语义就确保了基本来兴的局部变量始终封闭在线程内。
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Aniaml> animals;
int numPairs = 0; //基本类型的局部变量
Aniaml candidate = null;
// animals 被封闭在方法中,不要使它们逸出
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
numPairs++;
}
return numPairs;
}
3.3.3 ThreadLocal 类
当前执行线程在调用 set 时设置的最新值。
ThreadLocal 对象通常用于放置对可变的单实例变量(Singleton)或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个 Connection 对象。
private static ThreadLocal<Connection> connectionThreadLocal = new ThreadLocal<>() {
@Override
protected Object initialValue() {
return DriverManager.getConnection(URL);
}
}
public static Connection getConnection() {
return connectionThreadLocal.get();
}
ThreadLocal<T> 视为包含了 Map<Thread, T> 对象,其中保存了特定于该线程的值,但 ThreadLocal 的实现并非如此。这些特定于线程的值保存在 Thread 对象,当线程终止后,这些值会作为垃圾回收。
3.4 不变性
满足同步需求的另一种方法时使用不可变对象。到目前为止,我们介绍了许多与原子性和可见性相关的问题,例如得到失效数据,丢失更新操作或者观察到某个对象处于不一致的状态等等,都与多线程试图同时访问同一个可变的状态相关。如果对象的状态不会改变,那么这些问题与复杂性也就自然消失了。
不可变对象一定是线程安全的。
虽然在Java 语言规范和 Java 内存模型中都没有给出不可变性的正式定义,但不可变性并不等于将对象中所有的域都声明为 final 类型,即使对象中所有的域都是 final 类型的,这个对象也仍然是可变的,因为在 final 类型的域中可以保存对可变对象的引用。
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态不可能修改。
- 对象的所有域都是 final 类型。
- 对象时正确创建的(在对象的创建期间, this 引用没有逸出)。
看个例子:在可变对象基础上构建的不可变类
public class ThreeStooges {
private final Set<String> stooges = new HashSet<>();
public ThreeStooges() {
stooges.add("one");
stooges.add("two");
stooges.add("three");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
3.4.1 Final 域
在 Java 内存模型中,final 域还有着特殊的语义。final 域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步。
正如“除非需要更高的可见性,否则应将所有的域都声明为私有域”是一个良好的编程习惯,“除非需要某个域是可变的,否则应将其声明为 final 域”也是一个良好的编程习惯。
3.4.2 示例:使用 volatile 类型来发布不可变对象
对于volatile 关键字的详细介绍,建议大家去仔细观看 volatile关键字解析 ,所以在这不做过多介绍。贴一个代码:
public class VolatileCachedFactorizer implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest request, ServletResponse response) {
BigInteger i = extractFromRequest(request);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(response, factors);
}
}
3.5 安全发布
到目前为止,我么重点讨论的是如何确保对象不被发布,例如让对象封闭在线程或另一个对象的内部。当然,在某些情况下我们希望多个线程间共享对象,此时必须确保安全地进行共享。
如下:在没有足够同步的情况下发布对象(不要这样做)
//不安全的发布
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
由于可见性问题,其他线程看到的 Holder 对象将处于不一致的状态,即便在该对象的构建函数中已经正确地构建了不便性条件。这种不正确的发布导致其他线程看到尚未创建完成的对象。
3.5.1 不正确的发布:正确的对象被破坏
你不能指望一个尚未被完全创建的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没有修改过它。
如下:由于未被正确发布,因此这个类可能出现故障
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if(n != n) //这句没看懂,就算同步时会出现 n 很可能成为失效值,但是难道 (n != n)不是原子操作?求解。
throw new AssertionError("this statement is false");
}
}
3.5.2 不可变对象与初始化安全性
初始化安全性保障。
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
3.5.3 安全发布的常用模式
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到 volatile 类型的域或者 AtomicReferance 对象中
- 将对象的引用保存到某个正确构造对象的 final 类型域中。
- 将对象的引用保存到一个由锁保护的域中。
线程安全库中的容器类提供了一下的安全发布保证:
- 通过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap
- 通过将某个元素放入 Vector、CopyiOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet
- 通过将某个元素放入 BlockingQueue 或者 ConcurrentLinkedQueue
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);
3.5.4 事实不可变对象
如果对象在发布后不会被修改,那么 程序只需将它们视为不可变对象即可。
在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
例如,Date 本身是可变的,但如果将它作为不可变对象来使用,那么在多个线程之间共享 Date 对象时,就可以省去对锁的使用。假设需要维护一个 Map 对象,其中保存了每位用户的最近登录时间:
public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());
如果Date对象的值在被放入Map 后就不会改变,那么 synchronizedMap 中的同步机制就足以使 Date 值被安全地发布,并且在访问这些 Date 值时不需要额外的同步。
3.5.5 可变对象
对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。
对象的发布需要取决于它的可变性:
- 不可变对象可以通过任何机制来发布
- 事实不可变对象必须通过安全方式来发布。
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
3.4.5 安全地共享对象
当发布一个对象时,必须明确地说明对象的访问方式。
在并发程序中使用和共享对象时,可以使用一些实用的策略包括:
线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享:线程安全的对象在其内部实现同步,因此对个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。