1. 程式人生 > >Java併發程式設計規則:不可變物件永遠是執行緒安全的

Java併發程式設計規則:不可變物件永遠是執行緒安全的

建立後狀態不能被修改的物件叫作不可變物件。不可變物件天生就是執行緒安全的。它們的常量(變數)是在建構函式中建立的,既然它們的狀態無法被修改,那麼這些常量永遠不會被改變——不可變物件永遠是執行緒安全的。

不可變性的理解:

無論是Java語言規範還是Java儲存模型都沒有對不可變性做出正式的定義。不可變性並不是將域簡單地等於將物件的所有變數都宣告為final型別,所有域都是final型別的物件仍然可以是可變的,因為final域可以獲得一個可變物件的引用。

一個不可變的物件必須滿足的條件:它的狀態在建立後不能再修改,所有域都是final型別,並且它被正確建立(建立期間沒有發生this引用的逸出)。來看個不可變類示例:

package net.jcip.examples;

import java.util.*;

import net.jcip.annotations.*;

/**
 * ThreeStooges
 * <p/>
 * Immutable class built out of mutable underlying objects,
 * demonstration of candidate for lock elision
 *
 * @author Brian Goetz and Tim Peierls
 */
@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);
    }

    public String getStoogeNames() {
        List<String> stooges = new Vector<String>();
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
        return stooges.toString();
    }
}

注意:“物件是不可變的”與“物件的引用是不可變的”之間並不相等。

可用final修飾可變變數:

即使物件是可變的,將一些屬性變數宣告為final型別仍然有助於簡化對其狀態的判斷。因為限制了物件的可見性,也就約束了其可能的狀態集,即使有一兩個可變的狀態變數,這樣一個“幾乎不可變”的物件仍然比有很多可變變數的物件要簡單。將變數宣告為final型別,還向維護人員明確指出這些域是不可能變的。

package net.jcip.examples;

import java.math.BigInteger;
import java.util.*;

import net.jcip.annotations.*;

/**
 * OneValueCache
 * <p/>
 * Immutable holder for caching a number and its factors
 *
 * @author Brian Goetz and Tim Peierls
 */
@Immutable
public 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);
    }
}

用volatile釋出不可變物件:

使用volatile變數(僅可見性,而不具備同步性)不能保證執行緒安全性,但有時不可變物件也可以提供一種弱形式的原子性。

package net.jcip.examples;

import java.math.BigInteger;
import javax.servlet.*;

import net.jcip.annotations.*;

/**
 * VolatileCachedFactorizer
 * <p/>
 * Caching the last result using a volatile reference to an immutable holder object
 *
 * @author Brian Goetz and Tim Peierls
 */
@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet 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);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}


簡單地釋出安全物件:

--------------------------安全釋出物件的條件:----------------------------

1、通過靜態初始化物件的引用;
2、將引用儲存到volatile變數或AutomaticReference;
3、將引用儲存到final域欄位中;
4、將引用儲存到由鎖正確保護的域中;

在多執行緒中,常量(變數)最好是宣告為不可變型別,即使用final關鍵字。如下是一個不正確釋出的物件:

package net.jcip.examples;

/**
 * Holder
 * <p/>
 * Class at risk of failure if not properly published
 *
 * @author Brian Goetz and Tim Peierls
 */
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.");
    }
}

修正方式:將變數n用final修飾,使之不可變(即執行緒安全)。
package net.jcip.examples;

/**
 * Holder
 * <p/>
 * Class at risk of failure if not properly published
 *
 * @author Brian Goetz and Tim Peierls
 */
public class Holder {
    private final int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }
}
注意:不可變物件可以在沒有任何額外同步的情況下,安全地用於任意執行緒;甚至釋出它時也不需要同步。

釋出物件必要條件依賴於物件的可變性:

1、不可變物件可以依賴任何機制釋出;

2、高效不可變物件必須安全地釋出;

3、可變物件必須要安全釋出,同時必須要執行緒安全或被鎖保護。

Java中支援執行緒安全保證的類:

1、置入HashTable、synchronizedMap、ConcurrentMap中的主鍵以及值,會安全地釋出到可以從Map獲取他們的任意執行緒中,無論是直接還是通過迭代器(Iterator)獲得。

2、置入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中的元素,會安全地釋出到可以從容器中獲取它的任意執行緒中。

3、置入BlockingQueue或ConcurrentLinkedQueue的元素,會安全地釋出到可以從容器中獲取它的任意執行緒中。

通常,以最簡單最安全的方式釋出一個類就是使用靜態初始化:

 public static Holder holder=new Holder(99);

任何執行緒都可以在沒有額外的同步下安全地使用一個安全釋出的高效不可變物件。
 public Map<String,Date> lastLogin=Collections.synchronizedMap(new HashMap<String, Date>());
Date是可變型別,通過Collections.synchronizedMap安全的資料結構使得使用它的結果不可變,從而不需要額外的同步。

安全地共享物件:

在併發程式中,使用和共享物件的一些最有效的策略如下:

-------------執行緒限制:--------------

一個執行緒限制的物件,通過限制線上程中,而被執行緒獨佔,且只能被佔有它的執行緒修改。

-------------共享只讀(read-only):-------------

一個共享的只讀物件,在沒有額外同步的情況下,可以被多個執行緒併發地訪問,但任何執行緒都不能修改它。共享只讀物件包括可變物件與高效不可變物件。

-------------共享執行緒安全(thread-safe):-------------

一個執行緒安全的物件在內部進行同步,所以其它執行緒無額外同步,就可以通過公共介面隨意地訪問它。

-------------被守護(Guarded):-------------

一個被守護的物件只能通過特定的鎖來訪問。被守護的物件包括哪些被執行緒安全物件封裝的物件,和已知特定的鎖保護起來的已釋出物件。