lock-下
自旋锁
CAS算法是乐观锁的一种实现,CAS算法中涉及到自旋锁,这里简述一下
CAS算法说明
CAS的单词Compare And Swap(比较并交换),一种知名的无锁算法。无锁编程,即不使用锁实现多线程间的变量同步,也是在线程没有被阻塞时实现变量同步,也叫非阻塞同步(Non-blocking synchronization)
CAS算法涉及三个操作数:
1,要读写的内存值V
2,进行比较的值A
3,要写入的新值B
更新变量的时候,只有当变量的预期值A和内存V中的实际值相等时(证明之前没有被修改),才将内存中的V值修改为新值B,否则不会执行任何操作
自旋锁说明
指当某线程在获取锁的时候,若锁已经被其他线程获取,则该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
该锁是为了实现保护共享资源而提出的一种锁。自旋锁与互斥锁比较类似,都是为了解决对某项资源的互斥使用。这两种锁在任意时刻最多只能有一个保持者,也就是说,在任何时刻最多只有一个执行单元获得锁。
两者在调度机制上有所不同:
对于互斥锁,如果资源已经被占用,资源的申请者只能进入睡眠状态。
对于自旋锁,它不会引起调度者睡眠,如果自旋所已经被别的执行单元保持,调用者就在那里一直旋转查看保持着是否释放
Java实现
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
自旋锁存在的问题
1,自旋锁不会让现成状态发生改变,一直处于用户态。即多线程一直都是active;不会让线程阻塞,减少了不必要的上下文切换,执行速度快
2,非旋转锁在拿不到锁的情况下会进入阻塞状态,进而进入内核态。当拿到锁的时候需要从内核态恢复,需要线程上下文切换。
(进程的执行模式划分为用户模式和内核模式,如果当前运行的是用户程序、应用程序或者内核之外的系统程序,那么对应进程就在用户模式下运行;如果在用户程序执行过程中出现了系统调用或者发生中断时间就要运行操作系统程序,即核心,进程模式就会变成内核模式。在内核模式下运行的进程可以执行机器的特权指令并且不受用户的干扰即使是root用户。用户进程即可以在用户模式下运行也可以在内核模式下运行)
可重入的自旋锁和不可重入的自旋锁
上面写过的自旋锁代码,仔细看他是不支持重入的。既当一个线程第1次已经拿到了锁,在该所释放之前又一次重新获得锁住,第2次就不可能成功拿到。由于不满足CAS,所以第2次获取的时候就会进入循环等待状态,如果是可重入锁,第2次应该也能够成功获取到。
public class ReentrantSpinLock { private AtomicReference<Thread> cas = new AtomicReference<Thread>(); private int count; public void lock() { Thread current = Thread.currentThread(); if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回 count++; return; } // 如果没获取到锁,则通过CAS自旋 while (!cas.compareAndSet(null, current)) { // DO nothing } } public void unlock() { Thread cur = Thread.currentThread(); if (cur == cas.get()) { if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟 count--; } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。 cas.compareAndSet(cur, null); } } } }
总结
1,线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待直到获取到锁
2,自旋锁所等待期间线程的状态不会改变,线程一直都是用户态并且是活动的
3,自旋锁如果持有锁的时间太长,会导致其他等待获取锁的线程耗尽CPU
4,自旋锁本身无法保证公平性这也无法保证可重入性
5,基于自旋锁,可以实现具备公平性和可重入性质的锁
分布式锁
分布式服务下,同一个变量分布在不同的服务中,为了保证变量在高并发情况下同时间只能被同一个线程执行。原有单机环境中的并发控制锁策略失效,需要一种跨JVM的互斥机制控制共享资源的访问。分布式锁同样适应CAP理论,需要在一致性、可用性,容错性三这种取舍。主流锁选取的是AP并达到最终一致性。
数据库锁
实现方式
方案1
方法名做唯一性约束,多个请求同时提交仅保证一个成功
方案2
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`version` int NOT NULL COMMENT '版本号',
`PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
先获取锁的信息
select id, method_name, state,version from method_lock where state=1 and method_name='methodName';
占有锁
update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;
如果没有更新影响到一行数据,则说明这个资源已经被别人占位了。
处理情景
数据库要配集群,防止单个节点挂了
定时清理超时数据,防止解锁失败,锁无法解开其他线程无法使用
自旋锁保证加锁成功,防止insert报错无法再次加锁
表增加能确认是那个服务及线程的信息,可以保证同线程可再次获取到,保证重入性
缓存锁
实现方式
try{
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK);
logger.info("cancelCouponCode是否获取到锁:"+lock);
if (lock) {
// TODO
redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); //成功设置过期时间
return res;
}else {
logger.info("cancelCouponCode没有获取到锁,不执行任务!");
}
}finally{
if(lock){
redisTemplate.delete(lockKey);
logger.info("cancelCouponCode任务结束,释放锁!");
}else{
logger.info("cancelCouponCode没有获取到锁,无需释放锁!");
}
}
处理场景
在主从场景中,客户端A从主节点取锁,若数据还未同步到从节点时,主节点宕机了。此时客户端B也能从新主节点(从节点升级为主节点)中获取到锁,安全时效
Zookeeper锁
zookeeper有四种节点:持久节点(断开连接节点保存)、临时节点(断开连接删除)、持久顺序节点(排序命名),临时顺序节点(断开删除且排序)
实现方式
借助临时顺序节点的性质,A请求加锁,则在持久节点下添加临时顺序节点,若该节点是第一个则加锁成功。后续B请求,则在第一个临时顺序节点后添加临时顺序节点,同时B节点会监视A节点是否存在。这表示B抢锁失败,等待锁释放,C来也一样
可以直接使用zookeeper第三方库Curator客户端,这个客户端中封装了一个可重入的锁服务。
缺点:性能没缓存高,也有并发问题(详细见原文,最后一篇)
三者比较
难易程度:数据库 > 缓存 > Zookeeper
复杂性:Zookeeper >= 缓存 > 数据库
性能:缓存 > Zookeeper >= 数据库
可靠性:Zookeeper > 缓存 > 数据库