在多線程的軟件世界裏,對共享資源的爭搶過程(Data Race)就是併發,而對共享資源數據進行訪問保護的最直接辦法就是引入鎖。html
POSIX threads(簡稱Pthreads)是在多核平臺上進行並行編程的一套經常使用的API。線程同步(Thread Synchronization)是並行編程中很是重要的通信手段,其中最典型的應用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(Critical Section)進行保護(另外一種經常使用的同步機制是barrier)。java
無鎖編程也是一種辦法,但它不在本文的討論範圍,併發多線程轉爲單線程(Disruptor),函數式編程,鎖粒度控制(ConcurrentHashMap桶),信號量(Semaphore)等手段均可以實現無鎖或鎖優化。redis
技術上來講,鎖也能夠理解成將大量併發請求串行化,但請注意串行化不能簡單等同爲** 排隊 ,由於這裏和現實世界沒什麼不一樣,排隊意味着你們是公平Fair的領到資源,先到先得,然而不少狀況下爲了性能考量多線程之間仍是會不公平Unfair**的去搶。Java中ReentrantLock可重入鎖,提供了公平鎖和非公平鎖兩種實現。sql
再注意一點,串行也不是意味着只有一個排隊的隊伍,每次只能進一個。固然能夠好多個隊伍,每次進入多個。好比餐館一共10個餐桌,服務員可能一次放行最多10我的進去,有人出來再放行同數量的人進去。Java中Semaphore信號量,至關於同時管理一批鎖。數據庫
自旋鎖是一種非阻塞鎖,也就是說,若是某線程須要獲取自旋鎖,但該鎖已經被其餘線程佔用時,該線程不會被掛起,而是在不斷的消耗CPU的時間,不停的試圖獲取自旋鎖。apache
互斥鎖是阻塞鎖,當某線程沒法獲取互斥鎖時,該線程會被直接掛起,再也不消耗CPU時間,當其餘線程釋放互斥鎖後,操做系統會喚醒那個被掛起的線程。編程
可重入鎖是一種特殊的互斥鎖,它能夠被同一個線程屢次獲取,而不會產生死鎖。緩存
java環境下能夠經過synchronized和lock開實現本地鎖。多線程
//synchronized
public synchronized void demoMethod(){}
public void demoMethod(){
synchronized (this)
{
//other thread safe code
}
}
private final Object lock = new Object();
public void demoMethod(){
synchronized (lock)
{
//other thread safe code
}
}
public synchronized static void demoMethod(){}
//lock
private final Lock queueLock = new ReentrantLock();
public void printJob(Object document) {
queueLock.lock();
try
{
Long duration = (long) (Math.random() * 10000);
System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date());
Thread.sleep(duration);
} catch (InterruptedException e)
{
e.printStackTrace();
} finally
{
System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
queueLock.unlock();
}
}
複製代碼
鎖非靜態是鎖了對象的實例;鎖靜態是鎖了對象的類型。併發
synchronized void testRead(){
this.testWrite();
}
synchronized void testWrite(){}
複製代碼
名稱 | 優勢 | 缺點 |
---|---|---|
synchronized | 實現簡單,語義清晰,便於JVM堆棧跟蹤,加鎖解鎖過程由JVM自動控制,提供了多種優化方案,使用更普遍 | 悲觀的排他鎖,不能進行高級功能 |
lock | 可定時的、可輪詢的與可中斷的鎖獲取操做,提供了讀寫鎖、公平鎖和非公平鎖 | 需手動釋放鎖unlock,不適合JVM進行堆棧跟蹤 |
使用分佈式鎖的目的有兩個,一個是避免屢次執行冪等操做提高效率;一個是避免多個節點同時執行非冪等操做致使數據不一致。 接下來咱們來看如何實現分佈式鎖,在java環境下有三種也即經過數據庫,經過redis及經過Zk來實現。
經過主鍵及其餘約束使用拋異常來實現分佈式鎖不在本文討論範圍。一下爲基於數據庫排他鎖來實現分佈式鎖
/** * 超時獲取鎖 * @param lockID * @param timeOuts * @return * @throws InterruptedException */
public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {
String sql = "SELECT id from test_lock where id = ? for UPDATE ";
long futureTime = System.currentTimeMillis() + timeOuts;
long ranmain = timeOuts;
long timerange = 500;
connection.setAutoCommit(false);
while (true) {
CountDownLatch latch = new CountDownLatch(1);
try {
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, lockID);
statement.setInt(2, 1);
statement.setLong(1, System.currentTimeMillis());
boolean ifsucess = statement.execute();//若是成功,那麼就是獲取到了鎖
if (ifsucess)
return true;
} catch (SQLException e) {
e.printStackTrace();
}
latch.await(timerange, TimeUnit.MILLISECONDS);
ranmain = futureTime - System.currentTimeMillis();
if (ranmain <= 0)
break;
if (ranmain < timerange) {
timerange = ranmain;
}
continue;
}
return false;
}
/** * 釋放鎖 * @param lockID * @return * @throws SQLException */
public void unlockforUpdtate(String lockID) throws SQLException {
connection.commit();
}
複製代碼
加鎖
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/** * 嘗試獲取分佈式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @param expireTime 超期時間 * @return 是否獲取成功 */
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
複製代碼
第一個爲key,咱們使用key來當鎖,由於key是惟一的。
第二個爲value,咱們傳的是requestId,不少童鞋可能不明白,有key做爲鎖不就夠了嗎,爲何還要用到value?緣由就是咱們在上面講到可靠性時,分佈式鎖要知足第四個條件解鈴還須繫鈴人,經過給value賦值爲requestId,咱們就知道這把鎖是哪一個請求加的了,在解鎖的時候就能夠有依據。requestId可使用UUID.randomUUID().toString()方法生成。
第三個爲nxxx,這個參數咱們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,咱們進行set操做;若key已經存在,則不作任何操做;
第四個爲expx,這個參數咱們傳的是PX,意思是咱們要給這個key加一個過時的設置,具體時間由第五個參數決定。
第五個爲time,與第四個參數相呼應,表明key的過時時間。
解鎖
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/** * 釋放分佈式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @return 是否釋放成功 */
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
複製代碼
第一行代碼,咱們寫了一個簡單的Lua腳本代碼
第二行代碼,咱們將Lua代碼傳到jedis.eval()方法裏,並使參數KEYS[1]賦值爲lockKey,ARGV[1]賦值爲requestId。eval()方法是將Lua代碼交給Redis服務端執行。
基於Redlock實現分佈式鎖的爭論見
使用curator來實現分佈式鎖。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}
複製代碼
方式 | 優勢 | 缺點 |
---|---|---|
基於DB | 直接藉助數據庫,容易理解 | 會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜 操做數據庫須要必定的開銷,性能問題須要考慮 使用數據庫的行級鎖並不必定靠譜,尤爲是當咱們的鎖表並不大的時候 |
基於緩存 | 性能好,實現起來較爲方便 | 經過超時時間來控制鎖的失效時間並非十分的合理 |
基於ZK | 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單 | 性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解 |
zookeeper可靠性比redis強太多,只是效率低了點,若是併發量不是特別大,追求可靠性,首選zookeeper。爲了效率,則首選redis實現。