線程是系統調度的基本單位。
線程若是使用得當,能夠有效地下降程序的開發和維護等成本,同時提高複雜應用程序的性能。多線程程序能夠經過提升處理器資源的利用率來提高系統的吞吐率。與此同時,在線程的使用開發過程當中,也存在着諸多須要考慮的風險。java
++i
。線程安全的問題着重於解決如何對狀態訪問操做進行管理,特別是對共享和可變的狀態。共享意味着可多個線程同時訪問;可變即在變量在其生命週期內能夠被改變;狀態就是由某個類中的成員變量(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
加鎖同步,那麼就必須知道 鎖的特色:併發
回顧上面的代碼,一個方法體中,只要涉及了多個狀態的時候,就必定須要同步整個方法嗎?答案是否認的,同步只是爲了讓多步操做爲原子性,即對複合操做同步便可,所以須要明確的即是哪些操做是複合操做。以下:函數
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提供了 get
、set
等訪問接口的方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,因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修飾着
對象不可變的條件
- 對象建立之後其狀態就不能修改。
- 對象的全部域都是final類型。
- 對象是正確建立的(在對象的建立期間,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)
。若是對象在發佈後狀態不會被修改(則稱爲事實不可變對象),那麼在沒有額外的同步狀況下,任何線程均可以安全地使用被安全發佈的不可變對象。
對象的發佈需求取決於它的可變性:
- 不可變對象能夠經過任意機制來發布。
- 事實不可變對象必須經過安全方式來發布。
- 可變對象必須經過安全方式來發布,而且必須是線程安全的或者有某個鎖保護起來。
在併發程序中使用和共享對象時可採用的策略:
- 線程封閉。將對象封閉在線程中,如在方法中建立和修改局部對象。
- 只讀共享。
- 線程安全共享。對象內部實現同步,使用公有接口來訪問。
- 保護對象。使用特定的鎖來保護對象。