前段时间突然看到了ExpiringMap这个东西,然后上github上看了看,也查看了一些博客,觉得这东西还挺有趣的,ExpiringMap 和 map 一样,但是和map不同的是ExpiringMap可以设置key-value的过期时间,这个特性和Redis很像,Redis可以做锁,这个当然也可以做锁。然后尝试着做了做。
1 导入ExpiringMap
<dependency>
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.9</version>
</dependency>
2 首先先创建两个线程池,主要用于管理线程
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Component
@Configuration
@EnableAsync
public class ConfigThreadPool {
@Bean
public Executor customLockExecutor1() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
//线程核心数目
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
//最大线程数
threadPoolTaskExecutor.setMaxPoolSize(10);
//配置队列大小
threadPoolTaskExecutor.setQueueCapacity(50);
//配置线程池前缀
threadPoolTaskExecutor.setThreadNamePrefix("locktest1-");
//配置拒绝策略
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//数据初始化
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
@Bean
public Executor customLockExecutor2() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
//线程核心数目
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
//最大线程数
threadPoolTaskExecutor.setMaxPoolSize(10);
//配置队列大小
threadPoolTaskExecutor.setQueueCapacity(50);
//配置线程池前缀
threadPoolTaskExecutor.setThreadNamePrefix("locktest1-");
//配置拒绝策略
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//数据初始化
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
在这里线程池的线程池前缀名ThreadNamePrefix我的设置的名字是一样的,因为我是想通过线程池的前缀名来控制线程锁对象,意思就是说不同的线程池的线程他的锁是不一样的。
3 创建提供ExpiringMap实例化的一个工具类(这里采用单例模式)
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import java.util.concurrent.TimeUnit;
public class ExpiringMapUtils {
private static ExpiringMap<String, String> map = null;
public static ExpiringMap<String,String> getExpiringMapInstance(){
if (map == null){
map = ExpiringMap
.builder()
.variableExpiration()
.expirationPolicy(ExpirationPolicy.CREATED)
.build();
}
return map;
}
}
3 实现Lock
这里采用了redis分布式锁的思想,大概的原理就是首先判断有没有线程已经抢到锁了,如果没有就直接参与抢锁,知道抢到锁为止。但是如过当前已经有线程占锁,当然这里的锁对象要相同,那么就有三条路,第一个是占锁线程主动放弃锁资源,第二个是当该锁已经过期了,那么锁就应该被自动释放供各线程竞争,第三个就是锁的自动释放(防止死锁),在redis实现分布式锁中,主要是通过设置一个锁的过期时间,不管有没有线程来竞争锁,这个锁到了一定的时间都会自动释放,不会让某个线程一直占据这个锁的,这主要是解决了死锁的问题。
主要代码:
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
@Component
public class MapLock {
ExpiringMap<String, String> lockMap = ExpiringMapUtils.getExpiringMapInstance();
public boolean getLock(String key){
//当前时间戳
String currentTimeValue = String.valueOf(System.currentTimeMillis());
//如果没有这个key,说明还没有线程抢到这个锁,就可以直接尝试获取
if (lockMap.get(key) == null){
// 线程如果获取到锁,那么锁的有效时间是2000ms
//也就是说2000ms过后,这把锁释放,其他线程可以获取到锁资源,这个2000ms主要是防止死锁
lockMap.put(key,currentTimeValue,2000, TimeUnit.MILLISECONDS);
try {
//判断是不是当前线程加入的时间戳,如果是的话,就说明是当前线程抢到了
//如果和当前线程提供的时间戳不一致的话,说明是其他线程抢到了,当前线程不能获取锁
if (lockMap.get(key) == null ? false : lockMap.get(key).equals(currentTimeValue)){
return true;
}else {
//当前没有线程抢到这把锁,当前线程抢锁失败,尝试重新获取锁资源
getLock(key);
}
}catch (NullPointerException e){
//可能出现在判断锁存在的情况下,线程完成任务后主动释放锁,就会出现空指针,如果不处理就会出现丢失数据的情况
System.out.println("网络出错正在重试。。。");
getLock(key);
}
}
//如果有这个key了,说明是锁被其他线程获取了,当前线程就不能直接获取锁,要开始抢锁操作
while(true){
BigDecimal nowcuttrent = new BigDecimal(String.valueOf( System.currentTimeMillis()));
// System.out.println("当前时间"+nowcuttrent);
if (checkLock(key,nowcuttrent)) {
lockMap.put(key,nowcuttrent.toString(),2000, TimeUnit.MILLISECONDS);
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
public boolean checkLock(String key, BigDecimal value){
//老线程的上锁时间
String oldCuttrent = lockMap.get(key);
//查看到锁是不存在的,可能在过程中锁被释放了。
if (oldCuttrent == null){
//"当前没有线程抢到这把锁,可能在过程中锁被自动释放了
return true;
}
BigDecimal oldCuttrentBigD = new BigDecimal(oldCuttrent);
//当前时间和上个线程上锁时间相减
BigDecimal subtract = value.subtract(oldCuttrentBigD);
//如果当前时间和上个线程获取到的时间相差小于1000ms,表示上个线程的锁没有释放,获取锁失败
if (subtract.compareTo(new BigDecimal(1000)) == -1 ){
//这把锁正在被其他线程占用,尝试重新获取
return false;
}else {
//这把锁拥有时间已超时,释放给其他线程,获取锁成功
return true;
}
}
}
当时没有想到直接在时间戳上面加,也可以在时间戳上面直接加,然后比较当前时间的时间戳大小。
4 创建线程
根据刚刚建立的两个线程池来创建线程,线程中,执行任务之前要去获取锁
这里是实现-1的操作,查看线程是不是同步的
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;
@Component
@EnableScheduling
public class TestThread {
public static int num = 100;
@Autowired
private MapLock mapLock;
@Async("customLockExecutor1")
public void test1() {
ExpiringMap<String, String> expiringMapInstance = ExpiringMapUtils.getExpiringMapInstance();
String threadName = Thread.currentThread().getName();
String[] split = threadName.split("-");
//获取锁
if (mapLock.getLock(split[0])) {
System.err.println("执行静态定时任务时间1: " + Thread.currentThread()+ " num=" + num--);
//执行成功后,主动放弃锁资源
expiringMapInstance.remove(split[0]);
}
}
@Async("customLockExecutor2")
public void test2() {
ExpiringMap<String, String> expiringMapInstance = ExpiringMapUtils.getExpiringMapInstance();
String threadName = Thread.currentThread().getName();
String[] split = threadName.split("-");
//获取锁
if (mapLock.getLock(split[0])) {
System.err.println("执行静态定时任务时间2: " + Thread.currentThread() + " num=" + num--);
//执行成功后,主动放弃锁资源
expiringMapInstance.remove(split[0]);
}
}
}
5 执行线程
这里可以用while,for都可以,但是为了查看数量,我是每个定时写了20个。相当于每个线程池要启动20个线程来执行任务。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Component
@EnableScheduling
public class TestMain {
@Autowired
private TestThread testThread;
//创建一个定时任务,启动应用就可以跑起来,不用去写接口等等
@Scheduled(fixedDelay = 1000000)
public void maintest() {
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
testThread.test1();
}
@Scheduled(fixedDelay = 1000000)
public void maintest2() {
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
testThread.test2();
}
}
启动类:
@SpringBootApplication
@EnableScheduling // 1.开启定时任务
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
效果
可以发现线程是安全的
如果不加锁,即将TestThread 中的锁去掉
//if (mapLock.getLock(split[0])) {
System.err.println("执行静态定时任务时间1: " + Thread.currentThread()+ " num=" + num--);
expiringMapInstance.remove(split[0]);
// }
把两个线程池中的线程得锁都去掉后,效果:
很明显这里的线程是不安全的,说明我们的线程锁是起作用的。
将上面的线程池部分的代码customLockExecutor2中的
threadPoolTaskExecutor.setThreadNamePrefix("locktest1-");
改成
threadPoolTaskExecutor.setThreadNamePrefix("locktest2-");
然后再把线程锁加回来,然后再试一试,效果又不一样了。
可以发现,对不同的线程锁对象(这里线程对象就是保存的线程池前缀名称),就是说对于不同线程池中实例化的线程对象,锁的作用是独立的。会存在线程池不同的线程对象共同作用于同一变量,虽然使用了我们自己构建的锁,但是也会造成线程不同步。这个的原理和synchronized的锁对象是一样的,不同的锁对象仅仅对其作用的线程起作用,对不同锁对象的线程是不起作用的。
至此我的demo完成了,表明通过ExpiringMap的确是可以实现锁的功能,毕竟他的功能和特性在redis分布式锁中和redis有相同的地方