什么是鎖
在多線程的軟件世界里,對共享資源的爭搶過程(Data Race)就是并發(fā)朵逝,而對共享資源數(shù)據(jù)進行訪問保護的最直接辦法就是引入鎖拴曲。
POSIX threads(簡稱Pthreads)是在多核平臺上進行并行編程的一套常用的API占哟。線程同步(Thread Synchronization)是并行編程中非常重要的通訊手段坑鱼,其中最典型的應(yīng)用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(qū)(Critical Section)進行保護(另一種常用的同步機制是barrier)。
無鎖編程也是一種辦法动漾,但它不在本文的討論范圍丁屎,并發(fā)多線程轉(zhuǎn)為單線程(Disruptor),函數(shù)式編程旱眯,鎖粒度控制(ConcurrentHashMap桶)晨川,信號量(Semaphore)等手段都可以實現(xiàn)無鎖或鎖優(yōu)化。
技術(shù)上來說删豺,鎖也可以理解成將大量并發(fā)請求串行化共虑,但請注意串行化不能簡單等同為** 排隊 ,因為這里和現(xiàn)實世界沒什么不同呀页,排隊意味著大家是公平Fair的領(lǐng)到資源妈拌,先到先得,然而很多情況下為了性能考量多線程之間還是會不公平Unfair**的去搶蓬蝶。Java中ReentrantLock可重入鎖尘分,提供了公平鎖和非公平鎖兩種實現(xiàn)。
再注意一點丸氛,串行也不是意味著只有一個排隊的隊伍培愁,每次只能進一個。當然可以好多個隊伍缓窜,每次進入多個竭钝。比如餐館一共10個餐桌,服務(wù)員可能一次放行最多10個人進去雹洗,有人出來再放行同數(shù)量的人進去香罐。Java中Semaphore信號量,相當于同時管理一批鎖时肿。
鎖的類型
自旋鎖(Spin Lock)
自旋鎖是一種非阻塞鎖庇茫,也就是說,如果某線程需要獲取自旋鎖螃成,但該鎖已經(jīng)被其他線程占用時旦签,該線程不會被掛起,而是在不斷的消耗CPU的時間寸宏,不停的試圖獲取自旋鎖宁炫。
互斥鎖 (Mutex Lock)
互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時氮凝,該線程會被直接掛起羔巢,不再消耗CPU時間,當其他線程釋放互斥鎖后,操作系統(tǒng)會喚醒那個被掛起的線程竿秆。
可重入鎖 (Reentrant Lock)
可重入鎖是一種特殊的互斥鎖启摄,它可以被同一個線程多次獲取,而不會產(chǎn)生死鎖幽钢。
鎖舉例
本地鎖
java環(huán)境下可以通過synchronized和lock開實現(xiàn)本地鎖歉备。
//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();
}
}
鎖非靜態(tài)是鎖了對象的實例;鎖靜態(tài)是鎖了對象的類型匪燕。
一些特性
- 可重入蕾羊。如下可以直接進入testWrite方法不用重新申請鎖。synchronized和lock都是可重入鎖帽驯。
synchronized void testRead(){
this.testWrite();
}
synchronized void testWrite(){}
- 可中斷鎖肚豺。例如A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖如果B可以中斷則該鎖為可中斷鎖界拦。synchronized就不是可中斷鎖,而Lock是可中斷鎖梗劫。
- 公平鎖和非公平鎖享甸。以請求鎖的順序來獲取鎖是公平鎖。synchronized是非公平鎖梳侨,lock默認是非公平鎖蛉威,但是可以設(shè)置為公平鎖。
對比
名稱 | 優(yōu)點 | 缺點 |
---|---|---|
synchronized | 實現(xiàn)簡單走哺,語義清晰蚯嫌,便于JVM堆棧跟蹤,加鎖解鎖過程由JVM自動控制丙躏,提供了多種優(yōu)化方案择示,使用更廣泛 | 悲觀的排他鎖,不能進行高級功能 |
lock | 可定時的晒旅、可輪詢的與可中斷的鎖獲取操作栅盲,提供了讀寫鎖、公平鎖和非公平鎖 | 需手動釋放鎖unlock废恋,不適合JVM進行堆棧跟蹤 |
分布式鎖
使用分布式鎖的目的有兩個谈秫,一個是避免多次執(zhí)行冪等操作提升效率;一個是避免多個節(jié)點同時執(zhí)行非冪等操作導致數(shù)據(jù)不一致鱼鼓。
接下來我們來看如何實現(xiàn)分布式鎖拟烫,在java環(huán)境下有三種也即通過數(shù)據(jù)庫,通過redis及通過Zk來實現(xiàn)迄本。
通過數(shù)據(jù)庫實現(xiàn)
通過主鍵及其他約束使用拋異常來實現(xiàn)分布式鎖不在本文討論范圍硕淑。一下為基于數(shù)據(jù)庫排他鎖來實現(xiàn)分布式鎖
/**
* 超時獲取鎖
* @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();
}
通過緩存系統(tǒng)實現(xiàn)
加鎖
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纠永,我們就知道這把鎖是哪個請求加的了鬓长,在解鎖的時候就可以有依據(jù)。requestId可以使用UUID.randomUUID().toString()方法生成尝江。
第三個為nxxx涉波,這個參數(shù)我們填的是NX,意思是SET IF NOT EXIST炭序,即當key不存在時啤覆,我們進行set操作;若key已經(jīng)存在惭聂,則不做任何操作窗声;
第四個為expx,這個參數(shù)我們傳的是PX辜纲,意思是我們要給這個key加一個過期的設(shè)置笨觅,具體時間由第五個參數(shù)決定。
第五個為time耕腾,與第四個參數(shù)相呼應(yīng)见剩,代表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代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey牵舵,ARGV[1]賦值為requestId柒啤。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。
基于Redlock實現(xiàn)分布式鎖的爭論見
通過ZK實現(xiàn)
使用[curator]{https://curator.apache.org/}來實現(xiàn)分布式鎖畸颅。
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;
}
分布式鎖對比
方式 | 優(yōu)點 | 缺點 |
---|---|---|
基于DB | 直接借助數(shù)據(jù)庫担巩,容易理解 | 會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越復雜 操作數(shù)據(jù)庫需要一定的開銷没炒,性能問題需要考慮 使用數(shù)據(jù)庫的行級鎖并不一定靠譜涛癌,尤其是當我們的鎖表并不大的時候 |
基于緩存 | 性能好,實現(xiàn)起來較為方便 | 通過超時時間來控制鎖的失效時間并不是十分的合理 |
基于ZK | 有效的解決單點問題,不可重入問題拳话,非阻塞問題以及鎖無法釋放的問題先匪。實現(xiàn)起來較為簡單 | 性能上不如使用緩存實現(xiàn)分布式鎖。 需要對ZK的原理有所了解 |
結(jié)論
zookeeper可靠性比redis強太多弃衍,只是效率低了點呀非,如果并發(fā)量不是特別大,追求可靠性镜盯,首選zookeeper岸裙。為了效率,則首選redis實現(xiàn)速缆。