1.什麼是鎖?html
多線程的鎖、數據庫的鎖、分佈式的鎖,三種鎖機制。java
在單進程的系統中,當存在多個線程能夠同時改變某個變量(可變共享變量)時,就須要對變量或代碼塊作同步,使其在修改這種變量時可以線性執行消除併發修改變量。redis
而同步的本質是經過鎖來實現的。爲了實現多個線程在一個時刻同一個代碼塊只能有一個線程可執行,那麼須要在某個地方作個標記,這個標記必須每一個線程都能看到,當標記不存在時能夠設置該標記,其他後續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記後再去嘗試設置標記。這個標記能夠理解爲鎖。spring
除了利用內存數據作鎖其實任何互斥的都能作鎖(只考慮互斥狀況),如流水錶中流水號與時間結合作冪等校驗能夠看做是一個不會釋放的鎖,或者使用某個文件是否存在做爲鎖等。只須要知足在對標記進行修改能保證原子性和內存可見性便可。數據庫
總結:一是在多線程中,維護線程的安全和維護線程的可見性;二是在在數據庫中,保存數據的一致性。設計模式
2.什麼是分佈式鎖?緩存
當在分佈式模型下,數據只有一份(或有限制),此時須要利用鎖的技術控制某一時刻修改數據的進程數。安全
3.多線程鎖服務器
在單機環境中,爲了保持在使用多線程的時候,確保線程的安全性,咱們通常會使用Java API 中java.util.concurrent.* 包下面的API進行代碼的編寫。多線程
若是在不一樣的場景使用不一樣的API呢?對於Java來講,是面向對象的,無非就是在class(類),變量,method(方法)三個地方(一是類上(class);二是方法上;三是全局變量。)
對於Class來講,最著名的是Spring依賴注入Bean實例,使用的是單例設計模式,確保了在多線程建立class的時候,線程是安全的。
而對於method來講,有兩個關鍵字,一是synchronized,另外一個是lock。對於synchronized來講,通常是加在函數名稱以前加上synchronized,或者在方法體上加上。
synchronized(Object o){ //do something}
而對於lock來講,lock是重入鎖,只能在方法體內部加上,格式以下:
Lock reentrantLock = new ReentrantLock(); reentrantLock.lock(); //處理事務reentrantLock.unlock();
其實對於上面的三條語句(代碼)來講,並非最好的。咱們還能夠設置鎖(是否公平,鎖的時間)相關屬性,同時把reentrantLock.unlock();在finally裏面,也就是說,在捕獲異常的時候,無論執行如何,最終都會釋放這個鎖的,代碼以下:
Lock reentrantLock = new ReentrantLock(); try { //設置時間 reentrantLock.tryLock(2, TimeUnit.SECONDS); //do something }catch (Exception e){ e.printStackTrace(); }finally { reentrantLock.unlock();}
補充知識點:
什麼是重入鎖?
重入鎖
(1)重進入:
1.定義:重進入是指任意線程在獲取到鎖以後,再次獲取該鎖而不會被該鎖所阻塞。關聯一個線程持有者+計數器,重入意味着鎖操做的顆粒度爲「線程」。
2.須要解決兩個問題:
線程再次獲取鎖:鎖須要識別獲取鎖的現場是否爲當前佔據鎖的線程,若是是,則再次成功獲取;
鎖的最終釋放:線程重複n次獲取鎖,隨後在第n次釋放該鎖後,其餘線程可以獲取該鎖。要求對鎖對於獲取進行次數的自增,計數器對當前鎖被重複獲取的次數進行統計,當鎖被釋放的時候,計數器自減,當計數器值爲0時,表示鎖成功釋放。
3.重入鎖實現重入性:每一個鎖關聯一個線程持有者和計數器,當計數器爲0時表示該鎖沒有被任何線程持有,那麼任何線程均可能得到該鎖而調用相應的方法;當某一線程請求成功後,JVM會記下鎖的持有線程,而且將計數器置爲1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程若是再次請求這個鎖,就能夠再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,若是計數器爲0,則釋放該鎖
最後對於全局變量來講,須要使用關鍵字volatile。相信看過spring源碼的都知道,緩存。不知道有沒有發現,使用了volatile關鍵字進行修飾。
總結:
主要是synchronized和lock的區別,兩個均可以用在方法體上。兩個都是重入鎖,synchronized可重入性,指的是同一線程在調用本身類中其餘synchronized方法/塊或調用父類的synchronized方法/塊都不會阻礙該線程的執行,就是說同一線程對同一個對象鎖是可重入的,並且同一個線程能夠獲取同一把鎖屢次,也就是能夠屢次重入。而lock最大的區別是:可中斷響應、鎖申請等待限時、公平鎖等功能。另外能夠結合Condition來使用。也就是說lock功能更強大。從Jdk 1.5 開始以後,因爲對synchronized進行了優化,synchronized和lock的效率是差很少的。
注意:上面的鎖,在單機服務器是很好使用的,假如獲取訂單號(惟一),是能夠確保的。可是若是把該項目部署到多臺服務器上去,該鎖只能在該服務器上鎖住,也就是對於分佈式是沒有的,獲取訂單號是確保不了惟一的。
4.分佈式鎖
在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。因此針對分佈式鎖的實現目前有多種方案,一是基於緩存,而緩存咱們以Redis爲例;第二個是基於ZK。相對於數據庫來講,上面兩個效率高,速度快一點。
1.基於Redis分佈式鎖
網上大部分資料,通常都是以setnx() 和 expire() 兩個命令爲基礎的,進行分佈式鎖控制。其實這個方式是存在問題的,由於這兩個命令不是原子操做,可能存在的狀況以下:
setnx()命令設置key和value成功,可是執行到expire()命令的時候,發生了服務器進程之間忽然掛掉了,多是由於機器掉電或者是被人爲殺掉的,就會致使 expire() 得不到執行,進而形成死鎖。
正確的打開方式:
第一種命令的方式:
須要在Redis 2.6.12 版本以後,仍是利用set()命令,該命令有設置時間的參數,也就是說該命令直接結合了expire()命令。
set aaron:test:lock true ex 10 nx
參數解析:
EX second :設置鍵的過時時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value 。
PX millisecond :設置鍵的過時時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value 。
NX :只在鍵不存在時,纔對鍵進行設置操做。 SET key value NX 效果等同於 SETNX key value 。
XX :只在鍵已經存在時,纔對鍵進行設置操做。
釋放鎖很簡單,直接刪除 key:
del aaron:test:lock
第二種Java代碼方式:
獲取鎖:
調用:
Long time = System.currentTimeMillis() + RedisConstant.LOCK_TIME_OUT_TEN;if (!redisLockUtil.tryLock(key, time)) {throw new Exception(...);}
具體工具類:
public boolean tryLock(String key, long value) {while (value > System.currentTimeMillis()) {// 第一步:若是設置成功,表明加鎖成功if (stringRedisTemplate.opsForValue().setIfAbsent(key, String.valueOf(value))) {return true;}// 第二步:獲取上一個線程A的鎖的值 BC的值爲valueString currentValue = stringRedisTemplate.opsForValue().get(key);// 第三步:若是鎖過時if (!StringUtils.isEmpty(currentValue) && Long.valueOf(currentValue) < System.currentTimeMillis()) {// 第四步:獲取上一個鎖的值並設置新的值, BC有前後關係。String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, String.valueOf(value));// 第五步:判斷if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {return true;}}try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}}return false;}
釋放鎖:
public Boolean unlock(String key, long value) {// 執行lua腳本,確保原子性String script = "if redis.call('get', KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) return true else return false end";DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);return stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(value));}
總結:
1.超時問題。Redis的分佈式鎖不能解決超時問題,若是在加鎖和釋放鎖之間的邏輯執行的太長,以致於超出了鎖的超時限制,就會出現問題。由於這時候第一個線程持有的鎖過時了,臨界區的邏輯尚未執行完,這個時候第二個線程就提早從新持有了這把鎖,致使臨界區代碼不能獲得嚴格的串行執行。爲了不這個問題,Redis 分佈式鎖不要用於較長時間的任務。若是真的偶爾出現了,數據出現的小波錯亂可能須要人工介入解決。
個人建議最好是測試一下該須要執行的程序最大運行時間,而後把這個運行時間設置到超時時間裏面去。
2.可重入性
上面的Java代碼是不支持可重入性的,Redis 分佈式鎖若是要支持可重入,須要對客戶端的 set 方法進行包裝,使用線程的 Threadlocal 變量存儲當前持有鎖的計數。不過這個實現起來有點麻煩,不推薦使用。
2.基於ZK分佈式鎖
基於zookeeper臨時有序節點能夠實現的分佈式鎖,大體思想即爲:每一個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個惟一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只須要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除便可。同時,其能夠避免服務宕機致使的鎖沒法釋放,而產生的死鎖問題。
看下Zookeeper能不能解決前面提到的問題:
鎖沒法釋放?
使用Zookeeper能夠有效的解決鎖沒法釋放的問題,由於在建立鎖的時候,客戶端會在ZK中建立一個臨時節點,一旦客戶端獲取到鎖以後忽然掛掉(Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。
非阻塞鎖?
使用Zookeeper能夠實現阻塞的鎖,客戶端能夠經過在ZK中建立順序節點,而且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。
不可重入?
使用Zookeeper也能夠有效的解決不可重入的問題,客戶端在建立節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。
單點問題?
使用Zookeeper能夠有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就能夠對外提供服務。
具體實現,參看以前zk相關的文章:
zookeeper知識點講解(三)--API使用、分佈式鎖和簡單的服務註冊與發現;( https://mp.weixin.qq.com/s/W1wM5UNUFRAJZA02RLvIvw )。
參考資料:
MySQL中的鏈接、實例、會話、數據庫、線程之間的關係,雅思敏.
2.Redis 命令參考(http://doc.redisfans.com/index.html).
3.zookeeper知識點講解(三)--API使用、分佈式鎖和簡單的服務註冊與發現.