[Java并发编程实战] 对象的共享

  1. 可见性
    重排序
    http://www.infoq.com/cn/articles/java-memory-model-2
    http://ifeve.com/jvm-reordering/
    http://tech.meituan.com/java-memory-reordering.html

    1.1 加锁与可见性
    加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

    1.2 Volatile 变量
    Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保变量的更新操作通知到其他线程。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。

    volatile 变量通常用做某个操作完成、发生中断或者状态的标志。

    volatile boolean asleep;
    ...
        while (!asleep) {
            countSomeSheep();
        }
    

    加锁机制即可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性,volatile 的语义不足以确保递增操作(count++)的原子性。

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

    • 对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量的值。
    • 该变量不会与其他状态变量一起纳入不变性条件中。
    • 在访问变量时不需要加锁。
  2. 发布与逸出
    “发布(Publish)”一个对象:使对象能够在当前作用域之外的代码中使用。

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

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

    public static Set<Secrt> knownSecrets;
    
    public void initialize() {
        knonwnSecrets = new HashSet<Secret>();
    }
    

    当发布某个对象时,可能会间接地发布其他对象。如果将一个 Secret 对象添加到集合 knownSecret 中,那么同样会发布这个对象,任何代码都可以遍历这个集合。

    同样,如果从非私有方法返回一个引用,那么同样会发布返回的对象。

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

    这样就会有问题,任何调用者都能修改这个数组的内容,数组 states 已经逸出了它所在的作用域,因为这个本应是私有的变量已经被发布了。

    当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些对象也都会被发布。

    当把一个对象传递给某个外部方法时,就相当于发布了这个对象。

    最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。

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

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

    http://stackoverflow.com/questions/3705425/java-reference-escape

    安全的对象构造过程

    在 ThisEscape 中,this 引用在构造函数中逸出。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确构造。

    在构造函数中调用一个可改写的实例方法时,同样会导致 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;
        }
    }
    
  3. 线程封闭

    线程封闭(Thread Confinement)当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

    3.1 Ad-hoc 线程封闭

    Ad-hoc 线程是指,维护线程封闭性的职责完全由程序实现来承担。

    3.2 栈封闭

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

    public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
    
        // animals 被封闭在方法中,不要使它们逸出!
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals ) {
            if (candidate == null || !candidate.isPotentialMate(a)) {
                candidate = a;
            } else {
                ark.load(new AnimalPair(candidate, a));
                ++ numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }
    

    在维持对象引用的栈封闭性时,要确保被引用的对象不会逸出。

    3.3 ThreadLocal 类

    维持线程封闭性的一种更规范的方法是使用 ThreadLocal,这个类能使线程中某个值与保存值的对象关联起来。

    ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

  4. 不变性

    满足同步需要的另一个方法是使用不可变对象。 如果某个对象在被创建后其状态不能被修改,那么这个对象就称为不可变对象。

    我们了解到了许多与原子性和可见性相关的问题,例如得到失效数据,丢失更新操作或者观察到某个对象处于不一致的状态,都与多线程视图同时访问一个可变的状态相关。

    不可变对象一定是线程安全的。

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

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

    4.1 Final 域

    final 域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。

  5. 安全发布的常用模式

    要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。

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

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

    public static Holder holder = new Holder(42);
    

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值