【J2SE】java併發編程實戰 讀書筆記( 1、2、三章)

線程的優缺點

線程是系統調度的基本單位。線程若是使用得當,能夠有效地下降程序的開發和維護等成本,同時提高複雜應用程序的性能。多線程程序能夠經過提升處理器資源的利用率來提高系統的吞吐率。與此同時,在線程的使用開發過程當中,也存在着諸多須要考慮的風險。java

  1. 安全性:有合理的同步下,多線程的併發隨機執行使線程安全性變得複雜,如++i
  2. 活躍性:在多線程中,常由於缺乏資源而處於阻塞狀態,當某個操做不幸形成無限循環,沒法繼續執行下去的時候,就會發生活躍性問題。
  3. 性能:線程總會帶來程序的運行時開銷,多線程中,當頻繁地出現上下文切換操做時,將會帶來極大的開銷。

線程安全性

線程安全的問題着重於解決如何對狀態訪問操做進行管理,特別是對共享和可變的狀態。共享意味着可多個線程同時訪問;可變即在變量在其生命週期內能夠被改變;狀態就是由某個類中的成員變量(Field)。緩存

一個無狀態的對象必定是線程安全的。由於它沒有可被改變的東西。
public class LoginServlet implements Servlet {
    public void service(ServletRequest req, ServletResponse resp) {
        System.out.println("無狀態Servlet,安全的類,沒有字段可操做");
    }
}

原子性

正如咱們熟知的 ++i操做,它包含了三個獨立的「讀取-修改-寫入」操做序列,顯然是一個複合操做。爲此java提供了原子變量來解決 ++i這類問題。當狀態只是一個的時候,徹底能夠勝任全部的狀況,但當一個對象擁有兩個及以上的狀態時,仍然存在着須要思考的複合操做,儘管狀態都使用原子變量。以下:安全

public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = 
        new AtomicReference<BigInteger>();
    private final AtomicReference<BigInteger[]> lastFactors = 
        new AtomicReference<BigInteger[]>();

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get())) {
            encodeIntoResponse(resp, lastFactors.get());
        } else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
} // lastNumber lastFactors 雖然都是原子的,可是 if-else 是複合操做,屬「先驗條件」

既然是複合操做,最直接,簡單的方式就是使用synchronized將這個方法同步起來。這種方式能到達預期效果,但效率十分低下。多線程

既然提到synchronized加鎖同步,那麼就必須知道 鎖的特色:併發

  1. 鎖是能夠重入的。即子類的同步方法能夠調用本類或父類的同步方法。
  2. 同一時刻,只有一個線程可以訪問對象中的同步方法。
  3. 靜態方法的鎖是 類;普通方法的鎖是 對象自己。

回顧上面的代碼,一個方法體中,只要涉及了多個狀態的時候,就必定須要同步整個方法嗎?答案是否認的,同步只是爲了讓多步操做爲原子性,即對複合操做同步便可,所以須要明確的即是哪些操做是複合操做。以下:函數

public class CachedFactorizer implements Servlet {
    private BigInteger lastNumber;
    private BigInteger[] lastFactors;
    private long hits;
    private long cacheHits;
    
    public synchronized long getHits() {
        return hits;
    }
    
    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }
    
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        
        if (factors == null) {
            factors = factor(i);
            synchronized (this) {
                lastNumber = 1;
                lastFactors = factors.clone();
            }
        }
        
        encodeIntoResponse(reqsp, factors);
    }
}// 兩個synchronized分別同步獨立的複合操做。

對象共享

重排序:當一個線程修改對象狀態後,其餘線程沒有看見修改後的狀態,這種現象稱爲「重排序」。性能

java內存模型容許編譯器對操做順序進行重排序,並將數據緩存在寄存器中。當缺少同步的狀況下,每個線程在獨立的緩存中使用緩存的數據,並不知道主存中的數據已被更改。這就涉及到內存可見性的問題。this

可見性

內存可見性:同步的另外一個重要的方面。咱們不只但願防止多個線程同時操做對象狀態,並且還但願確保某一個線程修改了狀態後,能被其餘線程看見變化。線程

volatile:使用 synchronized能夠實現內存可見,但java提供了一種稍弱的更輕量級得同步機制volatile變量。在訪問volatile變量時不會執行加鎖操做,所以不會產生線程阻塞。即使如此仍是不能過分使用volatile,當且僅當能簡化代碼的實現以及對同步策略的驗證時,才考慮使用它。code

發佈與逸出

發佈指:使對象可以在當前做用於以外的代碼中使用。即對象引用能被其餘對象持有。發佈的對象內部狀態可能會破壞封裝性,使程序難以維持不變性條件。

逸出指:當某個不該該發佈的對象被髮布時,這種狀況被稱爲逸出。

// 正確發佈:對象引用放置公有靜態域中,全部類和線程均可見
class CarFactory {
    public static Set<Car> cars;
    
    private CarFactory() {
        cars = new HashSet<Car>();
    }    // 私有,外部沒法獲取 CarFactory的引用
    
    public static Car void newInstance() {    
        Car car = new Car("碰碰車");
        cars.put(car);
        return car;
    }    // 使用方法來獲取 car
}
// 逸出
class Person {
    private String[] foods = new String[] {"土豆"};
    
    public Person(Event event) {
        person.registListener {
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            }
        }
    }// 隱式逸出了this,外界獲得了Person的引用 而且 EventListener也獲取了Person的引用。
    
    public String[] getFoods() {
        return foods;
    }// 對發佈的私有 foods,外界仍是能夠修改foods內部值
}

線程封閉

將可變的數據僅放置在單線程中操做的技術,稱之爲發線程封閉。

棧封閉:只能經過局部變量才能訪問對象。局部變量的固有屬性之一就是封裝在執行線程中,它們位於執行線程的棧中,其餘線程沒法訪問這個棧,即只在一個方法內建立和使用對象。

public int test(Person p) {
    int num = 0;
    PersonHolder holder = new PersonHolder();
    
    Person newPerson = deepCopy(p);
    Person woman = holder.getLove(newPerson);
    newPerson.setWomen(person);
    num++;
    
    return num; // 基本類型沒有引用,對象建立和修改都沒有逸出本方法
}

ThreadLocal類:ThreadLocal可以使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了 getset等訪問接口的方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,因get老是返回由當前執行線程在調用set時設置的最新值。

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

public static Connection getConnection() {
    return connectionHolder.get();
}
當某個頻繁執行的操做須要一個臨時對象,例如一個緩衝區,而同時又但願避免在每次執行時都從新分配該臨時對象,就可使用ThreadLocal。

不變性

線程安全性是不可變對象的固有屬性之一。不可變對象必定是線程安全的,它們的不變性條件是由構造函數建立的,只要它們的狀態不可變。

//    在可變對象基礎上構建不可變類
public final class ThreadStooges {
    private final Set<String> stooges = new HashSet<String>();
    
    public ThreadStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
    }
    
    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}// 沒有提供可修改狀態的方式,儘管使用了Set可變集合,但被private final修飾着

對象不可變的條件

  1. 對象建立之後其狀態就不能修改。
  2. 對象的全部域都是final類型。
  3. 對象是正確建立的(在對象的建立期間,this引用沒有逸出)

安全發佈

任何線程均可以在不須要額外同步的狀況下安全地訪問不可變對象,即便在發佈這些對象時沒有使用同步。
// 安全的 Holder類
class Holder {
    private int n;
    public Holder(int n) {
        this.n = n;
    }
}

public class SessionHolder {
    // 錯誤的發佈,致使 Holder不安全
    public Holder holder;
    
    public void init() {
        holder = new Holder(10);
    }
}// 當初始化 holder的時候,holder.n會被先默認初始化爲 0,而後構造函數才初始化爲 10;在併發狀況下,可能會有線程在默認初始化 與 構造初始化中,獲取到 n 值爲 0, 而不是 10;

要安全的發佈一個對象,對象的引用以及對象的狀態必須同時對其餘線程可見。一個正確構造的對象能夠經過如下方式安全發佈:

  • 在靜態初始化函數中初始化一個對象引用。
  • 將對象的引用保存到 volatitle 類型的域或者 AtomicReferance 對象中。
  • 將對象的引用保存到某個正確構造對象的 final 類型域中。
  • 將對象的引用保存到一個由鎖保護的域中。

在線程併發容器中的安全發佈:

  • 經過將一個鍵或者值放入 Hashtable、synchronizedMap 或者 ConsurrentMap中,能夠安全地將它發佈給任何從這些容器中訪問它的線程(不管是直接訪問仍是經過迭代器訪問)。
  • 經過將某個元素放入 Vector、 CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet中,能夠將元素安全地發佈到任何從這些容器中訪問該元素的線程。
  • 經過將某個元素放入 BlockingQueue或者ConcurrentLinkedQueue中,能夠將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。

一般,要發佈一個靜態構造的對象,最簡單、安全的方式就是使用靜態的初始化器。如public static Holder holder = new Holder(10)。若是對象在發佈後狀態不會被修改(則稱爲事實不可變對象),那麼在沒有額外的同步狀況下,任何線程均可以安全地使用被安全發佈的不可變對象。

對象的發佈需求取決於它的可變性:

  • 不可變對象能夠經過任意機制來發布。
  • 事實不可變對象必須經過安全方式來發布。
  • 可變對象必須經過安全方式來發布,而且必須是線程安全的或者有某個鎖保護起來。

在併發程序中使用和共享對象時可採用的策略:

  • 線程封閉。將對象封閉在線程中,如在方法中建立和修改局部對象。
  • 只讀共享。
  • 線程安全共享。對象內部實現同步,使用公有接口來訪問。
  • 保護對象。使用特定的鎖來保護對象。
相關文章
相關標籤/搜索