第五篇Java技术分享(再见JUC-CAS&Redisson)
Jpunster
这篇文章主要讲解JUC下深度理解CAS 以及 基于Redis实现的分布式锁(Redisson)。
前文:
第三篇技术分享中我们留下的坑:自旋锁。所以我们先解决自旋锁问题。
自旋锁:spinlock 官方解释:自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。
通俗解释:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
我们自己写一个自旋锁来从代码方面解释下自旋锁。
我们进一下comparAndSet中看一下源码。
上述unsafe类我在第二篇技术分享(8锁)中有提到过。我们再进unsafe类中找到getAndInt方法,看一下他具体实现自旋锁是怎样实现的。
我们这三张图对比着来看。先来解释getAndInt方法,参数 1(参数) 2(参数) 4(偏移量) ,方法中定义了5,dowhile做了一个自旋判断。如果从内存中获取的5的值还是传参进来的1,2的期望值,那么返回更新后的5+4,如果不是,就循环判断(暂时阻塞)。这样上面两个图的参数以及方法我想大家都听懂了。如果还不是很明白。我们来写一个例子。
我们看一下结果并分析一下:
T1 先获得锁,在T1加锁时将null->thread,并延时5S的时候,T2在做自旋判断发现期待的应该是null,但是实际上已经被T1改成thread,所以一直在循环判断,直到T1释放锁后将thread->null,T2自旋判断发现期待值是null,符合预期,所以先加锁将null->thread,延时1S后解锁完成,也将thread->null,完成解锁。
至此,自旋锁已经讲解完毕。
正文:
1.上文讲解自旋锁的时候,将CAS也一并阐释了。既然是深入理解CAS,那么接下来我们补充一下CAS的知识。
CAS :比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环!
缺点:1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
穿插真实面试题:
在字节跳动5面的时候,最开始,面试官先问了下JUC下的相关知识,然后聊到了CAS,问你知道ABA问题吗?如何解决ABA问题?
何为ABA问题?
这就是ABA问题,显然线程2拿到的A=1,已经不是最开始的A=1,已经被线程1动过手脚了。解决ABA 问题,引入原子引用!对应的思想:乐观锁(带版本号的原子操作)!
2.分布式锁
什么是分布式锁。在分布式环境下,即多台计算机,每个计算机上会启动jvm执行程序的运行环境下,单机锁显然不能胜任这种情况。那么分布式锁就应运而生,根据锁的本质和原理,我们就要找到另外的对于多机上的线程都可见的标志,以它来作为锁,就可以了。这样的锁,就是分布式锁。
分布式锁实现方式:分布式锁有多种实现方式,比如
1.基于数据库实现分布式锁
2.基于缓存(Redis等)实现分布式锁;
3.基于Zookeeper实现分布式锁;
接下来我们讲解的分布式锁是基于Redis实现的。
Redis 原生可以控制分布式锁,基于Redis单线程,Key唯一来做的,方法是
setIfAbsent(setnx)来做的,依赖Lua脚本可以更好的控制过期时间。我们今天介绍的是Redisson,是封装了Redis,性能更好,可控性更强一些。
我们先来搭建Redisoon环境。
Pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>piggymetrics</artifactId>
<groupId>com.piggymetrics</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Jpunster</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
application.yml:
spring:
redis:
host: 127.0.0.1
database: 0
port: 6379
server:
port: 9001
然后通过@Bean的方式注入容器,三种方式我都写在下面了。
package locks;
import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/***
* @author Jpunster
* @date 2020/4/7 5:02 下午
*/
@SpringBootApplication
public class JpunsterApplicaiton {
public static void main(String[] args) {
SpringApplication.run(JpunsterApplicaiton.class,args);
}
@Bean
public Redisson redisson(){
Config config = new Config();
config.useSingleServer().setConnectionMinimumIdleSize(10);
//主从(单机)
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
//哨兵
// config.useSentinelServers().setMasterName("mymaster");
// config.useSentinelServers().addSentinelAddress("redis://192.168.1.1:26379");
// config.useSentinelServers().addSentinelAddress("redis://192.168.1.2:26379");
// config.useSentinelServers().addSentinelAddress("redis://192.168.1..3:26379");
// config.useSentinelServers().setDatabase(0);
// //集群
// config.useClusterServers()
// .addNodeAddress("redis://192.168.0.1:8001")
// .addNodeAddress("redis://192.168.0.2:8002")
// .addNodeAddress("redis://192.168.0.3:8003")
// .addNodeAddress("redis://192.168.0.4:8004")
// .addNodeAddress("redis://192.168.0.5:8005")
// .addNodeAddress("redis://192.168.0.6:8006");
// config.useSentinelServers().setPassword("xx");//密码设置
return (Redisson) Redisson.create(config);
}
}
这样我们就建立了我们的Redisson的连接了,我们来看一下如何使用吧。模拟一个并发扣库存场景:
package locks;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/***
* @author Jpunster
* @date 2020/4/7 4:59 下午
*/
@RestController
@RequestMapping("/v1")
public class RedissonDemo {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private Redisson redisson;
@GetMapping(value = "/getLock")
public String getLock() {
String lockKey = "lock";
RLock redissonLock = redisson.getLock(lockKey);
try {
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("售卖成功,剩余" + realStock + "");
return "success";
} else {
System.out.println("剩余库存不足");
return "fail";
}
} finally {
redissonLock.unlock();
}
}
}
我们用Jmeter测一下并发环境下Redisson是否有问题:
我们用Jmeter测一下并发环境下Redisson是否有问题:
模拟两个jvm,100个线程同时执行:
Redis中设置库存为50
好了,准备工作搞定,测试开始。
port1:
port2:
OK,我们看到两个jvm,0S内100个线程抢库存,没有出现死锁、超卖等现象,效率比用原生Redis要高很多~
新文预告:1. 面试题整理 2.深入理解Nosql系列