锁的由来 :java
多线程环境中,常常遇到多个线程访问同一个 共享资源 ,这时候做为开发者必须考虑如何维护数据一致性,这就须要某种机制来保证只有知足某个条件(获取锁成功)的线程才能访问资源,而不知足条件(获取锁失败)的线程只能等待,在下一轮竞争中来获取锁才能访问资源。redis
两个知识点:sql
1.高级缓存Cache缓存
CPU为了提升处理速度,不和内存直接进行交互,而是使用Cache。多线程
可能引起的问题:架构
若是多个处理器同时对共享变量进行读改写操做 (i++就是经典的读改写操做),那么共享变量就会被多个处理器同时进行操做,这样读改写操做就不是原子的了,操做完以后共享变量的值会和指望的不一致。并发
形成此结果的缘由:jvm
多个处理器同时从各自的缓存中读取变量i,分别进行加1操做,而后分别写入 系统内存中。分布式
处理器层面的解决方案:高并发
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其余处理器的请求将被阻塞住,那么该处理器能够独占共享内存。
2.CAS(Compare And Swap)+volatile
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。执行CAS操做的时候,将内存位置的值与预期原值比较,若是相匹配,那么处理器会自动将该位置值更新为新值。不然,处理器不作任何操做。
java的Atomic以及一些它自带的类中的cas操做都是经过借助cmpxchg指令完成的。他保证同一时刻只能有一个线程cas成功。
举个例子
以AtomicIneger的源码为例来看看CAS操做:
for(;;)表示循环,只有当if判断为true才退出。而if判断的内容就是是否CAS成功。
volatile的做用:
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操做会使在其余CPU里缓存了该内存地址的数据无效。
循环CAS+volatile是实现锁的关键。
Lock锁的部分细节
不一样场景锁的表现不一样:独占?共享?读写?
分布式锁(redis的简单实现)
分布式锁实现的三个核心要素:
最简单的方法是使用setnx命令。key是锁的惟一标识,按业务来决定命名。好比想要给一种商品的秒杀活动加锁,能够给key命名为 “lock_sale_商品ID” 。而value设置成什么呢?咱们能够姑且设置成1。加锁的伪代码以下:
setnx(key,1)
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不作任何动做。
SETNX 是『SET if Not eXists』(若是不存在,则 SET)的简写。 时间复杂度: O(1) 返回值: 设置成功,返回 1 。 设置失败,返回 0 。
当一个线程执行setnx返回1,说明key本来不存在,该线程成功获得了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
有加锁就得有解锁。当获得锁的线程执行完任务,须要释放锁,以便其余线程能够进入。释放锁的最简单方式是执行del指令,伪代码以下:
del(key)
释放锁以后,其余线程就能够继续执行setnx命令来得到锁。
若是一个获得锁的线程在执行任务的过程当中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。
因此,setnx的key必须设置一个超时时间,以保证即便没有被显式释放,这把锁也要在必定时间后自动释放。setnx不支持超时参数,因此须要额外的指令,伪代码以下:
expire(key, 30)
综合起来,咱们分布式锁实现的初版伪代码以下:
if(setnx(key,1) == 1){ expire(key,30) do something ...... del(key) }
上述代码的问题:
setnx刚执行成功,还将来得及执行expire指令,节点1 Duang的一声挂掉了。
这样一来,这个锁就长生不死了。
解决方案:
Redis 2.6.12以上版本为set指令增长了可选参数,伪代码以下:
set(key,1,30,NX)
能够在del释放锁以前作一个判断,验证当前的锁是否是本身加的锁
至于具体的实现,能够在加锁的时候把当前的线程ID当作value,并在删除以前验证key对应的value是否是本身线程的ID。
加锁:
String threadId = Thread.currentThread().getId() set(key,threadId ,30,NX)
解锁:
if(threadId .equals(redisClient.get(key))){ del(key) }
这样作又隐含了一个新的问题,判断和释放锁是两个独立操做,不是原子性。
这一块要用Lua脚原本实现:
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
redis官方说:eval命令在执行lua脚本时会看成一个命令去执行,而且直到命令执行完成redis才会去执行其余命令,因此就变成了一个原子操做。
进程1在超时时间内未执行完代码,此时进程2是能够获取锁的,会出现两个进程同时访问一个资源的状况。
解决方案:能够在进程1所在的jvm环境中开一个线程专门用来“续命”,当须要解锁的时候,通知这个续命线程结束执行。
private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 线程Id * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }
欢迎工做一到五年的Java工程师朋友们加入Java架构开发: 855835163 群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用本身每一分每一秒的时间来学习提高本身,不要再用"没有时间“来掩饰本身思想上的懒惰!趁年轻,使劲拼,给将来的本身一个交代!