采用ExpiringMap实现锁功能

前段时间突然看到了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有相同的地方

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值