鎖
多個線程或多個進程在同時改變某個共享變量時,需要對變量或代碼塊做同步(鎖),使其在修改這種變量時能夠線性執行
怎麼加鎖
CAS:(原子操作)將預期值與內存實際值比較,當相等時,把內存實際值修改爲期望值
CAS 的 ABA問題,內存值 0 ,+1,再-1 ,再進行CAS操作比較的時候,是符合的,單實際上該內存值已經改變過了
原子操作:不會被線程調度打斷的操作
Redis的操作之所以是原子性的,是因爲Redis是單線程的。
重量級鎖:JVM在JAVA1.6以前使用操作系統自身的互斥鎖(重量級鎖),這種鎖需要操作系統內核態與用戶態來回切換比較耗時間。
自旋鎖:CAS比較後不符合,循環比較
自適應自旋鎖:自旋時間短的,更容易自旋,時間長的,更少自旋甚至跳過
輕量級鎖:不申請互斥量,只有在多個線程發生鎖競爭後才改爲重量級鎖,在那之前,僅僅將Mark Word中的鎖狀態字節CAS更新爲鎖記錄的地址(指針),如果更新成功,則輕量級鎖獲取成功
偏向鎖:不進行CAS更新指向…,只有在多個線程發生鎖競爭後才改爲輕量級鎖,僅僅將Mark Word中的偏向鎖標誌置1,如果更新成功,則偏向鎖獲取成功。
總結上面幾種鎖的區別,鎖越輕量,消耗越小,操作越輕微,當發生線程競爭後不斷向更重量的鎖去變化。
java對象頭裏一般包含,HashCode、GC分代年齡、鎖狀態標誌、是否偏向鎖標記位、偏向線程ID
上文的Mark Word對應這裏的 鎖狀態、鎖標誌、是否偏向鎖標記位,共佔一個字節,32位系統是32位,64位系統是64位。
由此可知,一般鎖的實現是採用一種具有bool性質的數據做標誌,比如某個數據的有無,比如某段內存是否指向某段地址,比如對象中的鎖標誌位置1置0,比如因爲redis裏的key是唯一的,一個 key的有和無 ,比如因爲數據庫表裏的主鍵是唯一的,一條記錄的有和無,所以只要能實現標誌加和 解鎖兩種狀態的方式都可以實現加鎖。
多線程鎖
死鎖
死鎖分爲兩種,
-
一個線程獲取到鎖,沒釋放的時候再次獲取該鎖,造成死鎖
可以通過可重入鎖避免
可重入鎖:線程獲取一個資源的鎖,可以再次獲取該資源的鎖,建立一個標誌數,每獲取一次,標誌數加一,每釋放一次,標誌數減一,標誌數爲0,解鎖 -
線程a獲取資源1的鎖同時等待資源2,線程b獲取資源2的鎖同時等待資源1,這樣線程a和b進入死鎖
可以通過修改資源訪問順序的方式避免死鎖,把線程a和b都修改爲先獲取資源1再獲取資源2
基本所有的死鎖問題都可以通過加一個鎖失效時間來解決
活鎖
是一系列線程在輪詢地等待某個不可能爲真的條件爲真。活鎖的時候進程是不會blocked,這會導致耗盡CPU資源
如線程a從隊列中取出任務1來執行,如果任務執行失敗,那麼將任務重新加入隊列,繼續執行。假設任務總是執行失敗,那麼線程一直在繁忙卻沒有任何結果
如線程a請求資源1,如失敗請求資源2,線程b用同樣的邏輯請求資源1,2,a和b不斷的同時失敗,同時改變邏輯,再同時失敗…
可以通過加隨機等待時間,如果檢測到衝突,那麼就暫停隨機的一定時間進行重試解決
處於活鎖的實體是在不斷的改變狀態, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。
悲觀鎖
代碼裏每次數據操作都加鎖,同時只能有一個線程操作數據
樂觀鎖
樂觀鎖與CAS類似,代碼不加鎖,默認不會出現資源競爭問題
- 爲表中加一個 version 字段;
- 當讀取數據時,連同這個 version 字段一起讀出;
- 數據每更新一次就將此值加一;(此種方式可以避免ABA問題)
- 當提交更新時,判斷數據庫表中對應記錄的當前版本號是否與之前取出來的版本號一致,如果一致則可以直接更新,如果不一致則表示是過期數據需要重試或者做其它操作
分佈式鎖
CAP :一致性(Consistency):各個分佈式系統數據是否一致、可用性(Availability):各個分佈式系統功能是否齊全。分區容錯性(Partition tolerance):分佈式某個系統掛掉了,系統是否還能繼續用,最多隻能同時滿足兩項。
爲了保證數據的最終一致性,需要用分佈式事務、分佈式鎖等
分佈式事務
單進程事務一般使用註解就可以,分佈式系統基本都是保證最終一致性,可以採用
- 補償機制
程序員編寫補償代碼,在分佈式系統事務操作進行異常捕捉,然後根據是否異常,確定是否進行補償
它分爲三個階段:
Try 嘗試
Confirm 提交,只要Try成功,Confirm一定成功。
Cancel 回滾或業務取消 - 本地消息表
生產者A新建一個消息表,當要進行事務操作時,存一個帶狀態的消息到消息表,利用MQ發給消費者,消費者B事務成功,則修改消息表的狀態爲成功,A定時檢查消息表,把失敗的事務重新發送,直到狀態被B改爲成功,對於一直不成功的設置一個次數限制,超過限制了A的事務操作進行回滾。
分佈式鎖
分佈式鎖研究的是多個進程之間的鎖,一般在多線程中,可以採取內存中的對象上的鎖標誌加鎖解鎖和標記要鎖的數據的地址(指針),但是多線程,一般互相的內存是不互通的,所以要採取使用第三方的工具
一般可以採取
- 樂觀鎖(版本號)的方式
- 利用 Redis Key唯一性(如下單系統用每個訂單的編號做key,下單成功存入Redis,如果該key存在,則說明鎖着,其餘進程無法操作該訂單)
- 數據表的主鍵唯一性等(同上)
方式實現分佈式鎖