前言:
上文講了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等)