Sentinel源碼-多線程併發N種同步方式

前言:

    上文講了Sentinel在線程池使用方面的經驗。

    既然有了多線程任務執行,那麼就會存在併發問題,當出現併發問題時,解決方案無外乎加鎖。

    Sentinel中也有很多解決併發問題的方案,跟着筆者來看看吧。

 

1.synchronized使用

    這是比較常用的一種同步方式,我們來看下Sentinel對其的使用

// ZookeeperDataSource.java
// 被鎖定對象,final類型的,保證全局唯一
private static final Object lock = new Object();

private void initZookeeperListener(final String serverAddr, final List<AuthInfo> authInfos) {
    try {
        ...

        String zkKey = getZkKey(serverAddr, authInfos);
        if (zkClientMap.containsKey(zkKey)) {
            this.zkClient = zkClientMap.get(zkKey);
        } else {
            // 使用的方式在這裏
            // 鎖定lock對象
            synchronized (lock) {
                if (!zkClientMap.containsKey(zkKey)) {
                    ...
                    this.zkClient = zc;
                    this.zkClient.start();
                    Map<String, CuratorFramework> newZkClientMap = new HashMap<>(zkClientMap.size());
                    newZkClientMap.putAll(zkClientMap);
                    newZkClientMap.put(zkKey, zc);
                    zkClientMap = newZkClientMap;
                } else {
                    this.zkClient = zkClientMap.get(zkKey);
                }
            }
        }

        this.nodeCache = new NodeCache(this.zkClient, this.path);
        this.nodeCache.getListenable().addListener(this.listener, this.pool);
        this.nodeCache.start();
    } catch (Exception e) {
        RecordLog.warn("[ZookeeperDataSource] Error occurred when initializing Zookeeper data source", e);
        e.printStackTrace();
    }
}

2.ReentrantLock的使用

    synchronized加鎖方式,可以分解爲:入鎖和出鎖。

// 入鎖
synchronized(lock){
 // owner code   
    
}// 出鎖

    出鎖之後即釋放對該對象的鎖定,其他線程可以競爭該對象鎖,執行業務代碼。

    這種加鎖的方式勝在簡單,但是有一個缺陷就是:如果執行的業務邏輯特別耗時,那麼該對象鎖就一直無法釋放,其他線程就一直無法獲得該對象鎖。

 

    解決這個缺陷,可以使用ReentrantLock。下面來看一個Sentinel的使用:

// ClusterNode.java
// 創建鎖對象
private final ReentrantLock lock = new ReentrantLock();

public Node getOrCreateOriginNode(String origin) {
    StatisticNode statisticNode = originCountMap.get(origin);
    if (statisticNode == null) {
        try {
            // 加鎖
            lock.lock();
            statisticNode = originCountMap.get(origin);
            ...
        } finally {
            // 釋放鎖,注意該處,一定要在finally中釋放鎖,否則lock一直無法釋放,其他線程就無法執行
            lock.unlock();
        }
    }
    return statisticNode;
}

    剛纔這個示例好像還是沒有解決剛纔提出的Synchronized的問題。

    回頭來看下ReentrantLock的其他方法:

// 獲取鎖,可被中斷
public void lockInterruptibly() throws InterruptedException{}

// 持續不斷的去獲取鎖
public boolean tryLock() {}

// 規定時間內沒有獲取鎖則返回false
public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {}

    可以通過第三個方法來獲取鎖,這樣就實現了規定時間內返回鎖。

 

3.創建同步集合

    開發中,集合被大量使用,而集合也是多線程併發問題高發區,如何線程安全的使用集合是一個問題。

    我們來看下一個示例:

    

// DynamicSentinelProperty.java
protected Set<PropertyListener<T>> listeners
    = Collections.synchronizedSet(new HashSet<PropertyListener<T>>());

    這個是JDK提供的一個將常規集合轉換成同步集合的方法,還有以下方法,可以看到,針對常規集合,我們都可以通過Collections來轉換成同步集合。   

    我們可以簡單的分析下這種同步的實現方式:

    1)Collections.SynchronizedSet()

static class SynchronizedSet<E>
    extends SynchronizedCollection<E>
    implements Set<E> {
        private static final long serialVersionUID = 487447009682186044L;

        // 構造方法,直接使用SynchronizedCollection的構造方法
        SynchronizedSet(Set<E> s) {
            super(s);
        }
        SynchronizedSet(Set<E> s, Object mutex) {
            super(s, mutex);
        }
        ...
}
   
// SynchronizedCollection.java
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
        private static final long serialVersionUID = 3053995032091335093L;

    	// 真正實現方法的類
        final Collection<E> c;  // Backing Collection
        final Object mutex;     // Object on which to synchronize

    	// 上述構造方法就是調用這個的
        SynchronizedCollection(Collection<E> c) {
            this.c = Objects.requireNonNull(c);
            mutex = this;
        }

        SynchronizedCollection(Collection<E> c, Object mutex) {
            this.c = Objects.requireNonNull(c);
            this.mutex = Objects.requireNonNull(mutex);
        }

    	// 可以看到,同步的奧祕在這
    	// 調用方法統統添加synchronized,內部實現還由原來的集合類實現
        public int size() {
            synchronized (mutex) {return c.size();}
        }
        public boolean isEmpty() {
            synchronized (mutex) {return c.isEmpty();}
        }
        public boolean contains(Object o) {
            synchronized (mutex) {return c.contains(o);}
        }
        
...

    總結:通過Collections創建同步集合也是一個簡單的選項。

    缺點就是:所有的方法都被同步了,效率低

 

    2)jdk自帶同步集合類

    java.util.concurrent包下有一批Concurrent開頭的集合類,這些都是jdk自實現的一些同步集合,可以保證在多線程下安全使用。之前筆者也有寫過一些相關博客,讀者可自行閱讀。

    下面來看下Sentinel的使用:

// StatisticSlotCallbackRegistry.java
public final class StatisticSlotCallbackRegistry {

    private static final Map<String, ProcessorSlotEntryCallback<DefaultNode>> entryCallbackMap
        = new ConcurrentHashMap<String, ProcessorSlotEntryCallback<DefaultNode>>();
...

	// 具體方法使用
	// 可以看到這裏沒有使用同步策略,由ConcurrentHashMap內部來保證線程安全
    public static void clearExitCallback() {
        exitCallbackMap.clear();
    }

    public static void addEntryCallback(String key, ProcessorSlotEntryCallback<DefaultNode> callback) {
        entryCallbackMap.put(key, callback);
    }

4.原子類

    這個也是我們比較常用的一種同步方式,與同步集合類在相同的包路徑下,具體路徑是java.util.concurrent.atomic。

    這裏面主要都是一些有關Integer、Long的同步原子類,我們在做計數類的操作時,可以避免使用加鎖的方式,直接使用原子類即可。

    

// NamedThreadFactory.java
public class NamedThreadFactory implements ThreadFactory {

    // 線程計數
    private final AtomicInteger threadNumber = new AtomicInteger(1);

	@Override
    public Thread newThread(Runnable r) {
        // 非線程安全的newThread方法,計數需要保持併發安全,就直接使用AtomicInteger即可
        Thread t = new Thread(group, r, namePrefix + "-thread-" + threadNumber.getAndIncrement(), 0);
        t.setDaemon(daemon);
        return t;
    }

總結:

    1.使用Synchronized來加鎖業務代碼(最簡單,使用不當也容易造成其他線程無法獲取對象鎖)

    2.使用ReentrantLock加鎖業務代碼(多種加鎖方法,滿足用戶多需求)

    3.使用同步集合(Collections.SynchronizedSet()、ConcurrentHashMap等)

    4.使用原子類(AtomicInteger等)

 

發佈了124 篇原創文章 · 獲贊 126 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章