基於jdk lock的併發鎖
在JDK1.5以後,添加了Lock接口,它用於實現與Synchronized關鍵字相同的鎖操作,來實現多個線程控制對共享資源的訪問。但是能提供更加靈活的結構,可能具有完全不同的屬性,並且可能支持多個相關的Condition對象public
interface
Lock {
// 獲得鎖資源
void
lock();
// 嘗試獲得鎖,如果當前線程被調用了interrupted則中斷,並拋出異常,否則就獲得鎖
void
lockInterruptibly()
throws
InterruptedException;
// 判斷能否獲得鎖,如果能獲得,則獲得鎖,並返回true(此時已經獲得了鎖)
boolean
tryLock();
// 保持給定的等待時間,如果期間能拿到鎖,則獲得鎖,同樣如果期間被中斷,則拋異常
boolean
tryLock(
long
time, TimeUnit unit)
throws
InterruptedException;
// 釋放鎖
void
unlock();
// 返回與此Lock對象綁定Condition實例
Condition newCondition();
}
其中,tryLock只會嘗試一次,如果返回false,則走false的流程,不會一直讓線程一直等待
package com.snjx.common.utils;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import org.apache.log4j.Logger;
/**
*
* @ClassName: zklock
* @Description:TODO(zk 分佈式併發鎖)
* @author: snjx
* @date: 2017年10月24日 下午7:50:52
*/
public class JdkLock implements Runnable{
private Logger log = Logger.getLogger(JdkLock.class);
//併發數
private static final int num =10;
//倒計數器
private static CountDownLatch cdLatch=new CountDownLatch(num);
OrderCodeGeneratr orderCodeGeneratr=new OrderCodeGeneratr();
//jdk併發鎖
private static Lock lock= new ReentrantLock();
//創建訂單接口
public void createOrder(){
String OrderCode=null;
//加鎖
lock.lock();
//獲取訂單編號
try {
OrderCode=orderCodeGeneratr.getOrderCode();
} catch (Exception e) {
e.printStackTrace();
}
finally {
//解鎖
lock.unlock();
}
log.info(Thread.currentThread().getName()+"----------->>>> 訂單號 : "+OrderCode);
}
@Override
public void run() {
try {
//等待其他線程初始化
cdLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
//創建訂單
createOrder();
}
public static void main(String[] args) {
for (int i = 0; i <=num; i++) {
//按照線程數迭代實例化線程
new Thread(new JdkLock()).start();
//創建一個線程,倒計數器減一
cdLatch.countDown();
}
}
}
-----------------------------------------------------------------------------------------------------------------------------
應用場景
當多個機器(多個進程)會對同一條數據進行修改時,並且要求這個修改是原子性的。這裏有兩個限定:(1)多個進程之間的競爭,意味着JDK自帶的鎖失效;(2)原子性修改,意味着數據是有狀態的,修改前後有依賴。
實現方式
- 基於Redis實現,主要基於redis的setnx(set if not exist)命令;
- 基於Zookeeper實現;
- 基於version字段實現,樂觀鎖,兩個線程可以同時讀取到原有的version值,但是最終只有一個可以完成操作;
這三種方式中,我接觸過第一和第三種。基於redis的分佈式鎖功能更加強大,可以實現阻塞和非阻塞鎖。
基於zookeeper的分佈式鎖
利用臨時順序節點控制時序實現
算法思路:對於加鎖操作,可以讓所有客戶端都去/lock目錄下創建臨時順序節點,如果創建的客戶端發現自身創建節點序列號是/lock/目錄下最小的節點,則獲得鎖。否則,監視比自己創建節點的序列號小的節點(比自己創建的節點小的最大節點),進入等待。
對於解鎖操作,只需要將自身創建的節點刪除即可。
package com.snjx.common.utils;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import org.apache.log4j.Logger;
/**
*
* @ClassName: ZookImproveLock
* @Description:TODO(描述)
* @author: snjx
* @date: 2017年10月25日 下午7:38:02
*/
public class ZookeeperImproveLock implements Lock {
private static Logger log = Logger.getLogger(ZookeeperDistributedLock.class);
private static final String ZK_Adder = "localhost:2181";
private static final String Lock_Node = "/lock";
private ZkClient client = new ZkClient(ZK_Adder, 1000, 1000, new SerializableSerializer());
private CountDownLatch cdl = null;
private String beforePath;// 當前請求節點
private String currentPath;// 當前請求的節點前一個節點
// 判斷有沒有lock目錄,沒有則創建
public ZookeeperImproveLock() {
if (!this.client.exists(Lock_Node)) {
this.client.createPersistent(Lock_Node);
}
}
@Override
public void lock() {
if (!tryLock()) {
waitForLock();
lock();
} else {
log.info(Thread.currentThread().getName() + "-----------獲得分佈式鎖---------");
}
}
private void waitForLock() {
// 給節點添加監聽
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataDeleted(String dataPath) throws Exception {
log.info(Thread.currentThread().getName() + "----------獲得handleDataDeleted事件-------------");
if (cdl != null) {
cdl.countDown();
}
}
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
};
client.subscribeDataChanges(beforePath, listener);
if (client.exists(beforePath)) {
try {
cdl = new CountDownLatch(1);
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
client.unsubscribeDataChanges(beforePath, listener);
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock() {
// 如果currentPath爲空 則爲第一次嘗試加鎖,第一次加鎖賦值currentPath
if (currentPath == null || currentPath.length() <= 0) {
// 創建一個臨時順序節點
currentPath = this.client.createEphemeralSequential(Lock_Node + '/', "lock");
log.info("-------------------->>>>currentPath : >>" + currentPath);
}
// 獲取所有零時節點的並排序,臨時節點名稱爲自增長的字符串:0000000002
List<String> childernList = this.client.getChildren(Lock_Node);
Collections.sort(childernList);
// 如果當前節點在所有節點中的排名第一,則獲取鎖成功
if (currentPath.equals(Lock_Node + '/' + childernList.get(0))) {
return true;
} else {
// 如果當前節點在所有節點中的排名中不是排名第一(二分查找法),則獲取前面節點的名稱,並賦值給beforePath
int wz = Collections.binarySearch(childernList, currentPath.substring(6));
beforePath = Lock_Node + '/' + childernList.get(wz - 1);
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public void unlock() {
// 刪除當前節點
client.delete(currentPath);
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return null;
}
}
-----------------------------------------------------------------------------------------------------------------------------
基於redis實現的分佈式鎖
先來看看一些redis的基本命令: SETNX key value
如果key不存在,就設置key對應字符串value。在這種情況下,該命令和SET一樣。當key已經存在時,就不做任何操作。SETNX是”SET if Not eXists”。 expire KEY seconds
設置key的過期時間。如果key已過期,將會被自動刪除。 del KEY
刪除key
可以參考redis官網
鎖的實現
- 鎖的key爲目標數據的唯一鍵,value爲鎖的期望超時時間點;
-
首先進行一次setnx命令,嘗試獲取鎖,如果獲取成功,則設置鎖的最終超時時間(以防在當前進程獲取鎖後奔潰導致鎖無法釋放);如果獲取鎖失敗,則檢查當前的鎖是否超時,如果發現沒有超時,則獲取鎖失敗;如果發現鎖已經超時(即鎖的超時時間小於等於當前時間),則再次嘗試獲取鎖,取到後判斷下當前的超時時間和之前的超時時間是否相等,如果相等則說明當前的客戶端是排隊等待的線程裏的第一個嘗試獲取鎖的,讓它獲取成功即可