博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
线程的共享
阅读量:514 次
发布时间:2019-03-07

本文共 6095 字,大约阅读时间需要 20 分钟。

目录

编写正确的并发程序时,关键问题在于:在访问
共享的可变状态时需要进行正确的管理。使用同步的方式可以避免多个线程在同一时刻访问相同的数据。同步还有另一个重要的方面:内存可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态,而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能看到发生的变化。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即得知修改的值。

Java内存模型是通过在变量修改后将新值同步到主内存,在变量读取前从主内存刷新变量值规则,来实现可见性的。
对于volatile修饰的变量,可以保证可见性。(volatile变量具有可见性,能保证新值立即同步到主内存,以及每次使用前立即从主内存刷新)
除了volatile外,synchronized和final关键字也可实现可见性。synchronized同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这个规则保证。
final关键字的可见性则是通过“被final修饰的字段,在构造器中完成初始化后,如果构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值”保证。
此外,加锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。所以,加锁的含义不仅仅局限于互斥行为,还包括内存可见性。
综上,在Java中有四类场景可保证可见性:
(1) volatile 关键字
(2) synchronized 关键字
(3) final 关键字
(4) 锁(如内置锁、显式锁、可重入锁、读写锁等)

线程封闭(Thread Confinement)

如果仅在单线程内访问数据,这种技术称为线程封闭。因为数据仅被单个线程访问,所以这些数据不存在共享的可能,也就不会出现线程不安全。当某个对象封装在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

线程封闭技术的一种常见应用是Swing的事件分发线程。将线程不安全对象(如可视化组件和数据模型)封闭在事件分发线程中,从而保证线程安全。线程封闭技术的另一种常见应用是JDBC的Connection对象。JDBC并不要求Connection对象是线程安全的,但在实际应用中,都是单线程采用同步的方式来处理请求,并在Connection对象返回之前,连接池不会将其分配给其他线程。所以这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。
Java语言及核心库提供了一种机制来帮助维持线程封闭性,如局部变量ThreadLocal类

Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是脆弱的,因为没有任何一种语言特性能将对象封闭到目标线程上。而且在实际应用中,线程封闭对象的引用通常保存在公有变量中。

当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在某些情况下,单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。
由于Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用,而应使用更强的线程封闭技术。

栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

对基本类型的局部变量,无论如何都不会破坏栈封闭性。由于任何方法都无法获取对基本类型的引用,所以Java语言的这种语义确保了基本类型的局部变量始终封闭在线程内。
对对象引用的局部变量,程序员需要多做一些工作,以确保被引用的对象不会逸出。如避免将对象的应用或对象中的任何内部数据暴露出去,从而破坏线程封闭性。
如果在线程内部上下文中使用非线程安全的对象,那么该对象仍是线程安全的。

ThreadLocal类

维持线程封闭性的一种更规范的做法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get方法总是返回由当前执行线程在调用set时设置的最新值。

当某个频繁执行的操作需要一个临时对象,如一个缓存区,而同时又希望避免在每次执行时都重新分配该临时对象,就可使用ThreadLocal类。
开发人员经常“滥用”ThreadLocal。如将所有全局变量都作为ThreadLocal对象,或作为一种“隐藏”方法参数的手段。ThreadLocal变量类似于全局变量,它会降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

不变性

如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一。

不可变对象很简单。它们只有一个状态,并且该状态由构造函数来控制。
当满足以下条件时,对象才是不可变的:
(1) 对象创建以后其状态就不能修改。
(2) 对象的所有域都是final类型。
(3) 对象是正确创建的(在对象的创建期间,this引用没有逸出)。

final域

final关键字修饰的字段。

安全发布

如果我们希望在多线程间共享对象,此时必须确保安全地进行共享。(发布对象)

发布和逸出

发布(Publish)一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。在发布时,要确保线程安全性。发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件。

逸出(Escape)是指某个不应该发布的对象被发布的情况。

发布对象的常用方式总结

发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看到该对象。示例代码如下:

public static Set
knownSecrets;public void initialize() {
knownSecrets = new HashSet
();}

当发布某个对象时,可能会间接地发布其他对象。以上述代码为例,将Secret对象添加到集合knownSecrets后,那么同样后发布这个对象。因为任何代码都可以遍历这个集合,并获得对这个Secret对象的引用。同样,如果从非私有方法中返回一个引用,那么同样会发布返回的对象。示例代码如下:

Class UnsafeStates {
private String[] states = new String[]{
"AK", "AL", "..." }; public String[] getStates() {
return states; }}

在这个示例中,数组states已经逸出了它所在的作用域。因为任何调用者都可间接通过getStates方法修改它。

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。
最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。示例代码如下:

public class ThisEscape {
public ThisEscape(EventSource source){
source.registerListener() {
new EventListener() {
public void onEvent(Event e) {
doSomething(e); } } } }}

当ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身,因为这个内部类的实例包含了对ThisEscape实例的隐含引用(EventListener类功能)。

注意,上述代码会引起this引用在构造函数中逸出。因为当ThisEscape发布EventListener时,在外部封装的ThisEscape实例也隐含地发布了。
不要在构造过程中使this引用逸出。在从对象的构造函数中发布对象时,会发布一个尚未构造完成的对象。构造函数中this引用逸出的常见错误有两种:(1)在构造函数中启动线程或注册一个事件监听器。当对象在其构造函数中创建一个线程时,this引用都会被新创建的线程共享。在对象尚未构造之前,新的线程就可看到它。(2)在构造函数中调用一个可改写的实例方法(既不是私有方法,也不是终结方法)时,同样会导致this引用在构造过程中逸出。
针对诸如上述代码在构造函数中注册事件监听器带来的this引用逸出,可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。示例代码如下:

public class SafeListener {
private final EventListener listener; private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e); } }; } public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; }}

不正确的发布

对象的不正确发布,会导致对象的完整性被破坏,观察该对象的线程将看到对象处于不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没有修改过它。示例代码如下:

// 定义类public class Holder {
private int n; public Holder(int n) {
this.n = n; } public void assertSanity() {
if(n != n) {
throw new AssertionError("This statement is false."); } }}
// 不安全的发布public Holder holder;public void initialize() {
holder = new Holder(42);}

由于没有使用同步来确保Holder对象对其他线程可见,因此将Holder称为“未被正确发布”。未被正确发布的对象存在两个问题:(1)除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或之前的旧值。(2)线程看到Holder引用的值是最新的,但Holer状态的值却是失效的。这会导致情况变得不可预测。如某个线程第一次读取域时得到失效值,而再次读取这个域时,会得到一个更新值。

安全发布的常用模式

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

  • (1) 在静态初始化函数中初始化一个对象引用。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器。示例代码如下:

public static Holder holder = new Holder(42);

静态初始化器由JVM在类的初始化阶段执行。由于JVM内部存在同步机制,因此通过这种方式初始化的任何对象都可安全发布。

  • (2) 将对象的引用保存到volatile类型的域或者AtomicReference对象中。

  • (3) 将对象的引用保存到某个正确构造对象的final类型域中。

  • (4) 将对象的引用保存到一个由锁保护的域中。

线程安全库中的容器类提供以下的安全发布保证:

(a) 通过一个键或值放入Hashtable、synchronizedMap或ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。
(b) 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以安全地将它发布给任何从这些容器中访问它的线程。
© 通过将某个元素放入BlockingQueue或ConcurrentLinkedQueue中,,可以 安全地将它发布给任何从这些队列中访问它的线程。
(d) 类库中的其他数据传递机制(如Future和Exchanger)同样能实现安全发布。

事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象”(Effectively Immutable Object)。通过使用事实不可变对象,不仅可以简化开发过程,而且还能减少同步而提高性能。

可变对象

如果对象在构造后可以修改,那么这种对象叫做可变对象。 对于可变对象,安全发布时,需确保“发布当时”状态的可见性。也就是说,不仅在发布对象时需要使用同步,而且每次对象访问时,同样需要使用同步来确保后续操作的可见性。

安全地共享对象

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

(1) 线程封闭。线程封闭保证对象只能被一个线程拥有。
(2) 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。如不可变对象和事实不可变对象。
(3) 线程安全共享。线程安全的对象在内部实现同步,多个线程可以通过该对象的公有接口,而无需进一步的同步。
(4) 保护对象。被保护的对象只能通过持有特定的锁来访问。

转载地址:http://hbvcz.baihongyu.com/

你可能感兴趣的文章