第3章 对象的共享

volatile 是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。
当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

使用:

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)

 

volatile变量的一种典型用法:检查某个专题标记以判断是否退出循环,eg:

volatile boolean asleep;

...

while(!asleep)

    countSomeSheep();

...

 
volatile变量通常用作某个操作完成,发生终端或者状态的标志。尽管volatile变量也可以用于表示其他的状态信息,但是volatile语义不足以确保递增操作(count++)的原子性,除非你能确保只又一个线程对变量执行写操作。

当且仅当满足以下所有条件时,才应该使用 volatile 变量:

1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

2. 该变量不会与其他状态变量一起纳入不变性条件中。

3. 在访问变量时不需要加锁

 
 
 
线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据就不需要同步。这种技术被称为线程封闭(Thread Confinement)。

它是实现线程安全的最简单方式之一。

 
线程封闭技术常见的例子就是JDBC的Connection对象,JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中,线程从连接池中获得一个Connection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求(例如Servlet请求或EJB调用多等)都是由单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程。因此,这种连接管理模式再处理请求时隐含的将Connection对象封闭在线程中。
Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。

Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序来承担。Ad-hoc线程封闭是非常脆弱的,因此在程序中尽量少用,在可能的情况下,应该使用更强的线程封闭技术(例如,栈封闭 或 ThreadLocal类)。

栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易于维护,也更加健壮。
 
ThreadLocal类
维持线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(Single)或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<connection>(){
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };
public static Connection getConnection(){
    return connectionHolder.get();
}
 
ThreadLocal变量类似于全局变量,它能降低代码的可重用行,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
3.4 不变性
满足同步的另一种方法是使用不可变对象(Immutable Object)。不可变对象一定是线程安全的。
虽然Java语言规范和Java内存模型中都没有给出不可变性的正式定义,但不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。

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

1.对象创建以后其状态就不能修改

2.对象的所有域都是final类型

3.对象是正确创建的(在对象的创建期间,this引用没有逸出)

 
使用volatile类型来发布不可变对象
@Immutable
class OneValueCache{
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i,BigInteger[] factors){
        lastNumber = i;
        lastFactors = factors;
    }

    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber==null || !lastNumbers.equal(i)) return null;
        else return Arrays.copyOf(lastFactors,lastFactors.length);
    }
}
 对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部存在一个不可变的对象中来消除。如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当现场获得了对该对象的引用后,就不必担心另一个线程会修改该对象的状态。如果要更新这些变量,那么可以创建一个新的容易对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet{
    private volatile OneValueCache cache = new OneValueCache(null,null);
    public void service(ServletRequest req,ServletResponse resp){
          BigInteger i = extractFromRequest(req);
          BigInteger[] factors = cache.getFactors(i);
          if(factors==null){
                 factors = factor(i);
                 cache = new OneValueCache(i,factors);
          }
          encodeIntoResponse(resp,factors);
     }
}
与cache 相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用确保可见性,使得在没有使用锁的情况下仍然是线程安全的。
3.5.2 不可变对象与初始化安全性
Java内存模型为不可变对象的共享提供了一种特殊的初始化安全保证。我们已经知道,即使某个对象的引用对其他线程是可见的,也并不意味着对象状态对于该对象的线程来说一定是可见的。为了确保对象状态能呈现出一只的视图,就必须使用同步。
另一方面,即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象,为了维持这种初始化安全性的保证,必须满足不可变性的所有需求:状态不可修改,所有域都是final类型,以及正确的构造过程。
这种保证还将延伸到被正确创建对象中所有final类型的域。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
3.5.3 安全发布的常用模式
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:
1.在静态初始化函数中初始化一个对象引用
2.将对象的引用保存到volatile类型的域或者AtomicReference对象中
3.将对象的引用保存到某个正确构造对象的final类型域中
4.将对象的引用保存到一个由锁保护的域中(包含 线程安全容器内部的同步)

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

1.通过将一个键或者值放入HashTable,synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)

2.通过将某个元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList或synchronizedSet中,可以将该元素安全地帆布到任何从这些容器中访问该元素的线程

3.通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

类库中的其他数据传递机制(例如Future和Exchanger)同样能实现安全发布。

 

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

public static Holder holder = new Holder(42);

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

 

3.5.4 事实不可变对象

所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的,并且如果对象状态不会再改变,那么就足以确保任何访问都是安全的。

如果对象从技术上来看是可变的,但其状态再发布后不会再改变,那么把这种对象称为"事实不可变对象(Effectively Immutable Object)"。

当满足以下条件时,对象才是不可变的:
1.对象创建以后其状态就不能修改
2.对象的所有域都是final类型
3.对象是正确创建的(在对象的创建期间,this引用没有逸出)

事实不可变对象不需要满足不可变性的严格定义。在这些对象发布后,程序只需将它们视为不可变对象即可。在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

eg: Date本身是可变的,但如果将它作为不可变对象来使用吗,那么在多个线程之间共享Date对象时,就可以省去对锁的使用。
public Map<String,Date> lastLogin = Collections.synchronizedMap(new HashMap<String,Date>());


3.5.5 可变对象
如果对象在构造后可以修改,那么安全发布只能确保"发布当时"状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全的共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。

对象的发布需求取决于它的可变性:
1.不可变对象可以通过任意机制来发布
2.事实不可变对象必须通过安全方式来发布
3.可变对象必须通过安全的方式来发布,并且必须是线程安全的或者由某个锁保护起来

3.5.6 安全地共享对象

当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都由于没有理解共享对象的这些"既定规则"而导致的。当发布一个对象时,必须明确地说明对象的访问方式。

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

猜你喜欢

转载自mikzhang.iteye.com/blog/2268447