锁
锁
synchronized
- 修饰实例方法:给当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰静态方法:给当前类加锁,进入同步代码前要获得指定类对象的锁
- 修饰代码块:给指定对象加锁,进入同步代码前要获得指定对象的锁
synchronized原理
- 早期
- 基于对象内部的监视器实现
- 监视器依赖于操作系统的Mutex Lock(互斥锁)
- 偏向锁:加锁和解锁不需要额外的消耗,如果线程间存在锁竞争,会带来额外的锁撤销的消耗,适用于只有一个线程访问同步块场景
- 轻量级锁:竞争的线程不会阻塞,如果始终得不到锁竞争的线程使用自旋会消耗CPU,追求响应时间,锁占用时间很短
- 重量级锁:线程竞争不使用自旋,不会消耗CPU,线程阻塞,响应时间慢,追求吞吐量,锁占用时间较长
- 锁消除:逃逸分析(分析变量能否逃出它的作用域)
- 锁粗化:将多个连续的加锁,解锁操作连接在一起,扩展成一个范围更大的锁
ReentrantLock
ReentrantReadWriteLock
- 线程允许获取读锁的条件
- 不能有其他线程的写锁
- 没有写请求;或者有写请求,但调用线程和持有锁的线程是同一个
- 线程允许获得写锁的条件
- 必须没有其他线程的读锁
- 没有其他线程的写锁
StampedLock
- JDK8引入
- ReentrantReadWriteLock的增强版
- 获取锁的方法,都会返回stamp,如果stamp=0表示操作失败,其他值表示成功
- 释放锁的方法,需要用stamp作为参数,参数的值必须和获得锁时返回的stamp一致
- 提供三种访问模式
- Writing模式:类似于ReentrantReadWriteLock的写锁
- Reading(悲观读模式):类似于ReentrantReadWriteLock的读锁(执行读过程中,不允许有写操作)
- Optimistic reading:乐观读模式(执行读过程中,允许写操作)
- 提供读锁和写锁相互转换的功能
- 不可重入
锁调优
- 减少锁的持有时间
- 减少锁的粒度
- 锁粗化
- 锁分离
- 读写分离
- 操作分离
- 无锁(CAS)
分布式锁
借助第三方公共组件
基于数据库实现
select ... for update
访问同一条数据for update
锁定数据,其它线程只能等待- 优点:简单方便,易于理解,易于操作
- 缺点:并发量大时,对数据库压力较大
- 建议:作为锁的数据库与业务数据库分开
基于redis的setnx实现
- 获取锁的redis命令
set resource_name my_random_value NX PX 30000
resource_name:资源名称,可根据不同的业务区分不同的锁
my_random_value:随机值,每个线程的随机值都不同,用于释放锁时的校验
NX:key不存在时设置成功,key存在则设置不成功
PX:自动失效时间,出现异常情况,锁可以过期失效
- 实现原理
- 利用NX的原子性,多个线程并发时,只有一个线程可以设置成功
- 设置成功即获得锁,可以执行后续的业务处理
- 如果出现异常,过了锁的有效期,锁自动释放
- 释放锁采用redis的delete命令
- 释放锁时校验之前设置的随机数,相同才能释放
- 释放锁的LUA脚本
if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1] else return 0 end)
基于redisson实现
- 代码示例
RLock rLock = redissonClient.getLock("order"); try { rLock.lock(30, TimeUnit.SECONDS); }finally { rLock.unlock(); }
基于zookeeper的瞬时节点实现
- 实现原理
- 利用zookeeper的瞬时有序节点的特性
- 多线程并发创建瞬时节点,得到有序的序列
- 序号最小的线程获得锁
- 其它线程监听自己序号的前一个序号
- 前一个线程执行完成,删除自己的序号节点
- 下一个序号的线程得到通知,继续执行
- 以此类推,创建节点时,已经确定了线程的执行顺序
- 代码示例
@Slf4j
public class ZkLock implements AutoCloseable, Watcher {
private ZooKeeper zooKeeper;
private String znode;
public ZkLock() throws IOException {
this.zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, this);
}
public boolean getLock(String businessCode) {
try {
// 创建业务根节点
Stat stat = zooKeeper.exists("/" + businessCode, false);
if (stat == null) {
zooKeeper.create("/" + businessCode, businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
// 创建瞬时有序节点
znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_",
businessCode.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取业务节点下所有节点
List<String> childrenList = zooKeeper.getChildren("/" + businessCode, false);
// 子节点排序
Collections.sort(childrenList);
String firstNode = childrenList.get(0);
// 如果创建的节点是第一个节点,则获得锁
if (znode.endsWith(firstNode)) {
return true;
}
// 如果不是第一个子节点,则监听前一个节点
String lastNode = firstNode;
for (String node : childrenList) {
if (znode.endsWith(node)) {
zooKeeper.exists("/" + businessCode + "/" + lastNode, true);
}else {
lastNode = node;
}
}
synchronized (this) {
wait();
}
return true;
} catch (Exception e) {
log.error("获取锁失败:", e);
}
return false;
}
@Override
public void close() throws Exception {
zooKeeper.delete(znode, -1);
zooKeeper.close();
}
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getType() == Event.EventType.NodeDeleted) {
synchronized (this) {
notify();
}
}
}
}
基于zookeeper的curator客户端实现
// 连接
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
curatorFramework.start();
//
InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/order");
try {
if (lock.acquire(30, TimeUnit.SECONDS)) {
try {
log.info("获得锁");
}finally {
lock.release();
}
}
} catch (Exception e) {
log.error("获得锁失败");
}
curatorFramework.close();
对比
- 数据库
- 实现简单,易于理解
- 对数据库压力大
- redis
- 易于理解
- 自己实现,不支持阻塞
- redisson
- 提供锁的方法,可阻塞
- zookeeper
- 支持阻塞
- 需要理解zookeeper,程序复杂
- curator
- 提供锁方法
- 依赖zookeeper,强一致