《Java Concurrency in Practice》 学习笔记--第三章:共享变量

3 共享对象

第二章主要讨论了多线程对共享变量的访问,通过琐保证互斥访问。本章主要讨论如何在多线程间共享对象,保证其被安全访问。第二章围绕原子性,本章则围绕可见性对线程安全问题进行分析。它们共同构成构建线程安全类的基础。

3.1  可见性

变量的可见性是指一个线程对它的修改是否对其他线程可见。

程序3-1所示的NoVisibility展示了多线程环境下共享变量的可见性问题。

public class NoVisibility {
    private static boolean ready;
    private static int number;
    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }
    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
程序 3-1  存在可见性问题的NoVisibility

  ReaderThread 可能输出0,也可能无限循环下去。这是因为 ReaderThread可能看不到主线程对共享变量number和ready的修改,也可能先看到了对ready的修改,再看到对number的修改。前者缘于读写线程分别在各自的寄存器中缓存了共享变量的值,后者缘于CPU进行了指令重排。

可见,一个线程可能看不到其它线程对共享变量的修改。解决该问题的方法是对所有共享变量的访问操作进行同步

3.1.1 Stale data

可见性问题会产生Stale(不新鲜的) data。例如在 NoVisibility中, 主线程修改number之后,ReaderThread仍可能读到修改之前的数据。更诡异的是,一个线程可能从被另一个线程后写入的变量中读到新鲜的数据,而从被其先写入的变量中读到过时的数据。

3.1.2 非原子性的64位操作

JVM保证对变量的机器级存取是原子的,但64位的long和double类型的变量除外。对它们的存取实际上由两次32位的操作构成。在多个线程对long和double类型的共享变量进行写操作后,读线程可能读到一个前32位来自线程A而后32位来自线程B的这样一个64位的随机数。因此,被多个线程共享的long和double类型的变量应被同步起来或被声明为volatile类型。

3.1.3 琐与可见性

琐可以保证共享变量的可见性。A线程进入一个synchronized 块,t1时刻退出,t2时刻(t1在t2之前)B进入被同一把琐保护的一个synchronized 块,则t1时刻前对A可见的变量在t2时刻后对B可见。换句话说,A在synchronized 块之前和之中做的任何事情在B获得同一把锁后对其可见。其过程如图3-1所示。

图3-1 琐保证可见性示例

B获得琐M后,A对x,y的修改都对B可见。

琐既可以实现对共享变量的互斥访问,又可以保证共享变量可见性。前提是所有线程对该共享变量的访问都使用同一把琐。

3.1.4 volatile 变量

volatile变量不会被寄存器缓存,对其操作不参与指令重排,所有线程总是能读到它的最新值。但对volatile变量的访问不会被加锁,不会造成线程被阻塞。因此,volatile变量仅保证可见性,不保证互斥访问,是一种较之于synchronized更轻量级的同步机制。

volatile不仅仅保护被其修饰的变量。线程A在t0时刻对一个volatile变量执行写操作,线程B在t1时刻(t1在t0之后)对该变量进行读操作,则所有在t0时刻前对A可见的变量在t1时刻后对B可见。线程A的写操作相当于退出一个synchronized块,线程B的读操作相当于进入由同一把琐保护的synchronized块。但鉴于难于分析,不应过分利用这种机制来保证共享变量的可见性。

volatile应被用来简化实现和verifying your synchronization policy(第二点目前不是很理解)。不应过分依赖volatile来保证共享变量的可见性,尤其是当使用volatile之后,仍需要小心的分析其可见性的时候。

volatile恰当的使用场合包括:保护被它修饰的变量本身或该变量引用的对象的可见性;标志重要的生命周期事件是否发生(如初始化,关闭等)。如下面这段程序:利用volatile变量标志是否应当退出循环。

volatile boolean asleep;

...

    while (!asleep)

        countSomeSheep();

可见性小结:

琐同时保证原子性和可见性。Volatile变量只保证可见性。

使用volatile的条件

    1. 对变量的写不依赖于当前值(避免read-modify-write的复合操作),或者保证只有一个写线程
          2. 变量不能与其他变量有不定式关系(综合前两点,如果变量涉及复合操作,都应该使用琐,而不是volatile)

    3. 由于其他原因,琐的确是不必需的

3.2 Publish and Escape

Publish一个对象是指通过某种操作,使其可被当前作用域之外的代码访问(一般指定义该对象的类之外的代码)。如果对象在不适当的场合被publish,则称该对象escape。Publish是一个动作。Share是一种状态。

Publish对象的几种常见方式:

    1. 使用public static域保存对象的引用,使其可被所有类和线程访问

    2. 在nonprivate方法中返回对象的引用

    3. 参数传递。一个类的alien方法包括其它类的方法和自身可改写的方法(非private和final)。传递给alien方法的对象被publish。例如程序3-2所示的ThisEscape.

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            });
    }
}
程序3-2 ThisEscape publish内部类实例

ThisEscape在构造函数中创建了匿名内部类实例,将其publish给source。之后任何可以访问这些实例的代码都可以通过调用其 onEvent方法访问 ThisEscape的成员。

3.2.1安全的构造策略

不要让this引用在构造函数中escape。否则,持有该this引用的对象可能访问到一个未创建完成的对象。

容易出错的情形包括在构造函数中启动线程以及在构造函数中调用可改写的方法。

在构造函数中被创建的线程往往持有创建者的this引用。它可能是作为参数显示的传入线程的构造函数,也可能因为Thread或Runnable对象是线程创建者的内部类实例,从而被隐式的传递出去。其实在构造函数中创建线程并不错,关键是不应立即启动它,因为往往在启动后,线程才有机会使用创建者的this引用。更好的方法是提供一个自定义的init函数或使用静态工厂来启动该线程。使用静态工厂修复 ThisEscape的代码如程序3-3所示。

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);	// publish 内部对象
        return safe;
    }
}
程序3-3 使用静态工厂的ThisEscape

若在构造函数中调用可改写的方法,子类可能改写该方法,访问新添加的成员变量。子类构造时,先调用父类的构造函数,根据多态,父类将调用被子类改写后的方法,访问子类新添加的数据成员,由于父类尚未构建完成,子类新添加数据成员也尚未创建(在Java中是尚未初始化,参考《Java编程思想》8.3.3 构造函数与多态),从而导致错误。

3.3 Thread Confinement

实现线程安全的方法之一是不在线程间共享变量,将变量的使用范围限制在单个线程之内,即实现Thread Confinement。

Java没有提供直接实现Thread  Confinement的机制。Thread  Confinement要依靠程序的具体实现来保证。

下面是实现Thread Confinement的几种方法。

1 Ad-hoc方法

    不利用Java的任何机制,完全依靠程序实现的方法。比较脆弱。

2. Stack Confinement

    函数的局部变量被存放在其所在进程的栈中,只能被该进程访问。同样,只能通过本地局部变量来访问的对象不会被共享。由于无法获得原始类型变量的引用,原始类型的局部变量总是stack confined。引用类型的局部变量在不被publish的情况下才是stack confined。

3.ThreadLocal

    ThreadLocal为每个线程保存一个变量的副本,每个线程读写与之相关联的副本。

程序3-4是一个使用ThreaLocal保存数据库连接的例子。

private static ThreadLocal<Connection> connectionHolder
    = new ThreadLocal<Connection>() {
        public Connection initialValue() {
            return DriverManager.getConnection(DB_URL);
        }
    };

public static Connection getConnection() {
    return connectionHolder.get();
}
程序3-4 ThreadLocal实例

访问数据库的线程希望保持数据库连接,避免多次创建连接的开销。但Connection类非线程安全。在多线程环境下使用Connection需要同步。通过使用ThreaLocal,每个线程都将持有一个仅限自己使用的Connection,避免了同步的开销。

TheadLocal可以用来存储多线程间不共享的单件和全局变量。

ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread(e.g., a user ID or Transaction ID)。(JDK1.6 ThreaLocal类的说明)。可见,ThreadLocal往往用来保存线程的信息,而不是对象的信息。

当一个线程第一次调用ThreadLocal.get时, 程序将调用initialValue为该线程创建一个初始值。

勿滥用ThreadLocal创建非必须的全局变量,因为这会降低程序的复用性,引入耦合

3.4 Immutability

实现线程安全的另一种方法是使用不可变对象。不可变对象总是线程安全的。

一个对象是不可变的,如果(充分非必要条件)

    1.它的状态在创建后无法修改

    2.所有的域都是final类型

    3.被合理构造(this引用没有在构造过程中escape)

程序3-5展示了一个不可变的类ThreeStooges。

@Immutable
public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}
程序3-5 不可变类ThreeStooges 
不可变对象可以利用可变对象存储其内部状态。例如 ThreeStooges利用可变的集合对象存储内部数据。但在构造完成之后,外部对象无法再修改 stooges的状态。

利用不可变对象存储的状态不一定不可变,如果指向不可变对象的引用不是final类型,仍可以通过对象的替换来更新状态。

尽可能的将变量声明为final,除非它们确实需要改变。使用final有利于简化对象的状态分析。

程序3-6是一个利用不变性实现线程安全的例子。例子是第二章中提到的具有缓存功能的Factorizer。

@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 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);
    }
}
程序3-6 使用不可变对象的Factorizer

OneValueCache是不可变对象。一个线程无需考虑其它线程修改它的状态。cache的更新以重新构造OneValueCache进行替换的方式实现。因此,缓存中lastNumber和lastFactors总是处于一致的状态。volatile 保证了cache的可见性。

考虑将相关联的可变状态封装在一个不可变的对象中。状态的更新通过替换新对象的方式实现。可见性通过volatile保证。(volatile + 不可变对象)这种方式有一定局限性。当一个进程准备更新状态时,在构建新对象过程中,执行替换之前,其它进程会读到旧的状态。

3.5 Safe Publication

Safe Publication是指publish一个对象,并保证其对其它线程可见,强调可见性。程序3-7是一个Unsafe publication的例子。对象通过public域被publish。

// Unsafe publication
public Holder holder;

public void initialize() {
    holder = new Holder(42);
}
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.");
    }
}
程序3-7 不安全的publication
由于没有任何同步机制,Holder具有明显的可见性问题。 在多线程环境下,一个holder对象调用assertSanity()可能抛出异常。

不可变对象可被任何线程无同步地安全地访问。Publish不可变对象无需同步。

Safe Publication Idioms

    1.一个被合理构造的对象可以通过以下几种方式进行Safe Publication

    2.通过静态初始化器初始化其引用

    3.使用volatile或AtomicReference变量保存其引用

    4.使用被合理创建的对象的final变量保存其引用 

    5.使用被琐合理保护的变量保存其引用

第一中方法由JVM保证。例如public static Holder holder = new Holder(42);


线程安全的集合类提供Safe Publication保证:If thread A places object X in a thread-safe collection and thread B subsequently retrieves it, B is guaranteed to see the state of X as A left it, even though the application code that hands X off in this manner has no explicit synchronization. 

  • Placing a key or value in a Hashtable, synchronizedMap, or Concurrent-Map safely publishes it to any thread that retrieves it from the Map (whether directly or via an iterator);(Hashtable, synchronizedMap使用了琐,Concurrent-Map比较高效,但也比较复杂,用了多种手段)
  • Placing an element in a Vector, CopyOnWriteArrayList, CopyOnWrite-ArraySet, synchronizedList, or synchronizedSet safely publishes it to any thread that retrieves it from the collection;( CopyOnWriteArrayList, CopyOnWrite-ArraySet 用了多种手段,Vector, synchronizedList, or synchronizedSet使用锁)
  • Placing an element on a BlockingQueue or a ConcurrentLinkedQueue safely publishes it to any thread that retrieves it from the queue.

还有一种等效的不可变对象(原文是Effectively Immutable Objects,“高效的不可变对象“,但”等效的“更容易理解)。这类对象本身可变,但在发布后不会被程序修改。例如Date对象,如果我们每次更新Date对象,总是通过创建新对象,然后替换的方式(一般都是这样),那Date对象就是等效的不变对象。

等效不可变对象在Safely Published之后,可以被多线程无同步地安全地访问。Safely Published是为了保证可见性,无需同步是因为被publish后不可变。

Safe Publication只能保证对象publish后的可见性。如果对象可变,还应使用琐保证互斥访问。


安全的共享对象的常用策

1.Thread-confined. 将对象的使用限制在单线程之内。

2.Shared read-only. 共享不可变和等效不可变对象。

3.Shared thread-safe. 共享线程安全的对象。

4.Guarded. 使用琐保护共享变量。用于在线程安全类中封装其它对象。或使用被publish的对象,该对象被已知的琐保护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值