【JUC知识】单一JVM同步锁实现

同步锁实现

一、背景

在并发场景下,需要单一线程或限定并发数操作某些逻辑,这时候就需要用到一个锁来保证线程安全。

二、思路

  • 使用ConcurrentHashMap实现,但只支持同一个jvm下的线程(暂时满足)
  • 使用Semaphore信号量作为锁
  • 数量操作都使用java原子操作类,例:AtomicInteger、AtomicLong等

三、实操

  1. 构建Key锁对象

    import java.util.concurrent.Semaphore;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * key锁对象
     */
    public class ThreadData {
        private String lockKey;
        private Semaphore lock;
        //线程id
        private long threadId;
        //当前线程获取的锁数量
        private AtomicInteger acquireNum;
    
        // 第一次加锁时间
        private Long startTime;
    
        public ThreadData(String lockKey, long threadId, AtomicInteger acquireNum) {
            this.lockKey = lockKey;
            this.threadId = threadId;
            this.acquireNum = acquireNum;
            this.lock = new Semaphore(acquireNum.get());
            this.startTime = System.currentTimeMillis();
        }
    
        public static ThreadData newInstance(String lockKey, int maxNum) {
            return new ThreadData(lockKey, Thread.currentThread().getId(), new AtomicInteger(maxNum));
        }
    
        /**
         * 增加当前线程占用锁的数量
         *
         * @return
         */
        public int incrementAcquireNum() {
            return this.acquireNum.incrementAndGet();
        }
    
        /**
         * 减少当前线程锁的占有数量
         *
         * @return
         */
        public int decrementAcquireNum() {
            return this.acquireNum.decrementAndGet();
        }
    
        public long getThreadId() {
            return threadId;
        }
    
        public void setThreadId(long threadId) {
            this.threadId = threadId;
        }
    
        public Integer getAcquireNum() {
            return acquireNum.get();
        }
    
        public void setAcquireNum(Integer acquireNum) {
            this.acquireNum.set(acquireNum);
        }
    
        private Semaphore getLock() {
            return lock;
        }
    
        public Long getStartTime() {
            return startTime;
        }
    
        /**
         * 获取锁
         */
        public void lock() throws InterruptedException {
            getLock().acquire();
        }
    
        /**
         * 在指定时间内获取锁
         *
         * @param waitTimeout
         * @param timeUnit
         */
        public Boolean tryLock(int waitTimeout, TimeUnit timeUnit) throws InterruptedException {
            return getLock().tryAcquire(waitTimeout, timeUnit);
        }
    
        /**
         * 只有被当前线程持有锁的情况下才能释放锁
         */
        public void release() {
            lock.release();
        }
    }
    
  2. 编写同步锁工具类

    import com.jravity.jrlive.utils.StringHelper;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 同步锁,ConcurrentHashMap 实现,只支持同一个jvm下的线程
     */
    @Slf4j
    public class KeyLock {
    
        private final static ConcurrentHashMap<String, ThreadData> HAPPY_LOCK_MAP = new ConcurrentHashMap<String, ThreadData>();
        private final String lockKey;
        //最大并发数
        private int maxNum;
    
        public KeyLock(String lockKey) {
            if (StringHelper.isBlank(lockKey)) {
                throw new NullPointerException("lock key can not be null");
            }
            this.lockKey = lockKey;
            maxNum = 1;
        }
    
        public KeyLock(String lockKey, int maxNum) {
            if (StringHelper.isBlank(lockKey)) {
                throw new NullPointerException("lock key can not be null");
            }
            this.lockKey = lockKey;
            this.maxNum = maxNum;
        }
    
    
        /**
         * 获取🔒
         * 规定时间内未获取到返回true
         *
         * @return
         * @throws InterruptedException
         */
        public Boolean acquire(int waitTimeout, TimeUnit timeUnit) {
            ThreadData threadData = ThreadData.newInstance(lockKey, maxNum);
            ThreadData threadExistsValue = HAPPY_LOCK_MAP.putIfAbsent(this.lockKey, threadData);
            if (threadExistsValue == null) {
                threadExistsValue = threadData;
            }
            try {
                return threadExistsValue.tryLock(waitTimeout, timeUnit);
            } catch (InterruptedException e) {
                log.error(String.format("thread was interrupted ,key=%s threadName=%s", this.lockKey, Thread.currentThread().getName()));
                return false;
            }
        }
    
        /**
         * 尝试获取锁,不带超时时间
         *
         * @return
         * @throws InterruptedException
         */
        public void acquire() throws InterruptedException {
            ThreadData threadData = ThreadData.newInstance(lockKey, maxNum);
            ThreadData threadExistsValue = HAPPY_LOCK_MAP.putIfAbsent(this.lockKey, threadData);
            if (threadExistsValue == null) {
                threadExistsValue = threadData;
            }
            //重置过期时间
            threadExistsValue.lock();
        }
    
        /**
         * 释放🔒
         * 谁加锁谁释放,可以释放多次.
         */
        public void release() {
            ThreadData threadData = HAPPY_LOCK_MAP.get(lockKey);
            if (threadData == null) {
                log.error(String.format("%s 释放数据为空 ", lockKey));
                return;
            }
            threadData.release();
        }
    
        public static Map<String, ThreadData> getLockMap() {
            return HAPPY_LOCK_MAP;
        }
    }
    

    备:可常用ConcurrentHashMap的putIfAbsent()方法来作为是否有key的标准

四、工具类备注

1、Semaphore(信号量)

白话:Semaphore是一间可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binary semaphore。一般的用法是,用于限制对于某一资源的同时访问

常见方法:

  • acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。

  • release(释放)实际上会将信号量的值加1,然后唤醒等待的线程

posted @ 2022-12-06 14:53  simonlee_java  阅读(32)  评论(0)    收藏  举报