基于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命令,尝试获取锁,如果获取成功,则设置锁的最终超时时间(以防在当前进程获取锁后奔溃导致锁无法释放);如果获取锁失败,则检查当前的锁是否超时,如果发现没有超时,则获取锁失败;如果发现锁已经超时(即锁的超时时间小于等于当前时间),则再次尝试获取锁,取到后判断下当前的超时时间和之前的超时时间是否相等,如果相等则说明当前的客户端是排队等待的线程里的第一个尝试获取锁的,让它获取成功即可