Java併發(二)-實現同步

併發帶來的問題

先看一個單例類,後文中都會用到: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實現同步

java提供了一種內置的鎖機制同步代碼塊(synchronized block),它包括兩部分:鎖對象和由鎖對象保護的代碼塊。併發

  1. 若synchronized修飾了一段代碼,則負責保護一段代碼;
    synchronized (lock) { // 操做或訪問由lock保護的代碼塊 }
  2. 若修飾了一個方法,則負責保護這個方法的所有代碼,鎖是當前對象;若synchronized修飾靜態方法,那麼同步代碼塊的鎖是Class
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);
            }
        }
    }
}
  1. 其實能夠在doFactor方法前用synchronized修飾,然而這樣不符合性能問題;因此應該用synchronized修飾代碼塊便可
  2. getHits和getCacheHitRatio方法加上了synchronized修飾,用的鎖就是this,因此和doFactor裏面的鎖是同樣的;於是達到的效果是在doFactor內進行因子計算時候,getHits和getCacheHitRatio方法在阻塞狀態

java鎖機制的重入

當一個線程請求另外一個線程持有的鎖的時候,那麼請求的線程會阻塞;重入的概念是:當線程去獲取本身所擁有的鎖,那麼會請求成功;重入的原理是:爲每一個鎖關聯一個計數器和持有者線程,當計數器爲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方法時候會發生死鎖,由於它拿不到本身持有的鎖

參考內容

  1. 書籍《Java併發編程實戰》
相關文章
相關標籤/搜索