先看一個單例類,後文中都會用到:java
public class SimpleWorkingHardSingleton { private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 數量 private int count; private SimpleWorkingHardSingleton() { count = 0; } public static SimpleWorkingHardSingleton getInstance() { return simpleSingleton; } public int getCount() { return count; } public void addCount(int increment) { this.count += increment; System.out.println(this.count); } }
上文中,咱們已經知道這個類的getCount方法對count的操做是線程不安全的,咱們能夠用一些原子變量來實現原子性:編程
public class SimpleWorkingHardSingleton { private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 數量 private AtomicLong atomicCount = new AtomicLong(0); private SimpleWorkingHardSingleton() { count = 0; } public static SimpleWorkingHardSingleton getInstance() { return simpleSingleton; } public AtomicLong getAtomicCount() { return atomicCount; } public void addAtomicCount(long increment) { this.atomicCount.getAndAdd(increment); } }
能夠看到,在這個類中,咱們把count使用AtomicLong原子類。java的jdk包實現了一系列的原子類,這些原子類型的操做都是原子的。那麼count的增長就不會分爲3步(獲取,增長,賦值)了,這個原子的操做是原子類內部實現的,咱們在使用過程當中只需知道這個操做過程是原子的、不可分割的便可。在使用原子類型的狀況下:count變量是會達到預期的效果的。緩存
這裏所說的原子變量的失效狀況是指當類中使用了多個原子變量,若是一個操做要改變多個原子變量,那麼仍是會出現同步問題:安全
public class SimpleWorkingHardSingleton { private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 數量 private AtomicLong atomicCount = new AtomicLong(0); private AtomicLong atomicCountCopy = new AtomicLong(0); private SimpleWorkingHardSingleton() { count = 0; } public static SimpleWorkingHardSingleton getInstance() { return simpleSingleton; } public AtomicLong getAtomicCount() { return atomicCount; } public AtomicLong getAtomicCountCopy() { return atomicCountCopy; } public void addAtomicCount(long increment) { this.atomicCount.getAndAdd(increment); this.atomicCountCopy.getAndAdd(increment); } }
這種狀況下,atomicCount和atomicCountCopy各自的增長是原子的,可是兩個變量都增長這個過程是兩步,不是原子的。如果a、b兩根線程在運行addAtomicCount方法,a線程執行完atomicCount的增長,此時a線程掛起,b線程執行,而且執行了atomicCount和atomicCountCopy的增長,那麼此時atomicCountCopy就要比atomicCount小1了,由於a線程還有一半的任務沒有執行呢。多線程
java提供了一種內置的鎖機制同步代碼塊(synchronized block),它包括兩部分:鎖對象和由鎖對象保護的代碼塊。併發
synchronized (lock) { // 操做或訪問由lock保護的代碼塊 }
public class SimpleWorkingHardSingleton { private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton(); // 數量 private int count; private int countCopy; private SimpleWorkingHardSingleton() { count = 0; } public static SimpleWorkingHardSingleton getInstance() { return simpleSingleton; } public int getCount() { return count; } public int getCountCopy() { return countCopy; } public synchronized void addCount(int increment) { /* try { Thread.sleep(3000); } catch (InterruptedException e) { System.err.println(e); } */ this.count += increment; this.countCopy += increment; System.out.println(this.count); } }
上文代碼中synchronized對整個方法進行了修飾,那麼保護的代碼就是方法中的所有代碼;這樣在多線程環境中,會有序遞增地輸出count。可是這樣有一個潛在問題就是性能問題;
synchronized對整個方法進行了修飾,就會致使這個方法每次只有一個線程能夠運行,這就會致使性能問題;假如這個方法中有一個耗時3s的io操做,咱們用Thread.sleep(3000);來模擬。然而synchronized保護的代碼塊本不該該包含這3s的操做,所以代碼應該寫成:ide
public void addCount(int increment) { try { Thread.sleep(3000); } catch (InterruptedException e) { System.err.println(e); } synchronized (this) { this.count += increment; System.out.println(this.count); } }
上文中兩個變量不一樣步的狀況,就能夠用synchronized同步代碼塊來解決;並且使用synchronized要注意,先保證正確性,便可能產生併發問題的共享變量都要放在同步代碼塊當中;而後再追求性能,即對儘量短的代碼進行保護,也不能太過細化由於鎖的使用和釋放都是須要代價的。性能
一個稍微複雜的場景(多看例子多模仿系列)this
/** * 實現帶緩存功能的因子分解 */ public class CachedFactorizer { private static CachedFactorizer cachedFactorizer = new CachedFactorizer(); // 上一個處理的數字 private long lastNumber; // 上一個數字分解的結果 private long[] lastFactors; // 處理數字的次數 private long hits; // 緩存命中的次數 private long cacheHits; private CachedFactorizer() { } public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double)cacheHits / (double)hits; } public static CachedFactorizer getInstance() { return cachedFactorizer; } public long[] factor(int target) { // 僞代碼,僞裝實現了因子分解 return new long[] {}; } public void doFactor(int target) { Thread.sleep(300); synchronized (this) { hits++; if (target == lastNumber) { cacheHits++; } else { lastNumber = target; lastFactors = factor(target); } } } }
當一個線程請求另外一個線程持有的鎖的時候,那麼請求的線程會阻塞;重入的概念是:當線程去獲取本身所擁有的鎖,那麼會請求成功;重入的原理是:爲每一個鎖關聯一個計數器和持有者線程,當計數器爲0時候,這個鎖被認爲是沒有被任何線程持有;當有線程持有鎖,計數器自增,而且記下鎖的持有線程,當同一線程繼續獲取鎖時候,計數器繼續自增;當線程退出代碼塊時候,相應地計數器減1,直到計數器爲0,鎖被釋放;此時這個鎖才能夠被其餘線程得到。atom
public class Parent { public synchronized void do() { } } public class Child extends Parent { @Override public synchronized void do() { blabla super.do(); } }
若是沒有重入機制,那麼Child對象在執行do方法時候會發生死鎖,由於它拿不到本身持有的鎖