解决多线程下锁不住String问题


文章目录

  • 解决多线程下锁不住String问题
  • 业务场景
  • 解决思路
  • 1. 用String的intern方法,
  • 2. 利用其他常量池,例如Integer(-128~127)
  • 3. 使用ConcurrentHashMap+信号量方式
  • 4. 利用Redis分布式锁解决


业务场景

同一时间只能保证有一个线程在修改User信息因此加了Synchronized锁,锁住Student中name(String类型),但由于每个线程的name都不是同一个对象,因此锁不住

/**
  * 修改用户信息
  * @param student
  * @return
*/
public int update(Student student){
    synchronized (student.getName()){
        try {
            //模拟业务操作执行5秒
            Thread.sleep(5000);
            //打印当前时间,如果两条线程打印时间间隔没有超过5秒,证明没锁住
            log.info(Thread.currentThread().getName()+"提交修改,当前时间为:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return 1;
}

解决思路

1. 用String的intern方法,

该调用该方法则会在String常量池中寻找值跟String一样,同时返回它的地址的String,如果没有找到,则创建一个地址,返回该地址String

/**
  * 修改用户信息
  * @param student
  * @return
*/
public int update(Student student){
    synchronized (student.getName().intern()){ //调用intern方法寻找同值常量
        try {
            //模拟业务操作执行5秒
            Thread.sleep(5000);
            //打印当前时间,如果两条线程打印时间间隔没有超过5秒,证明没锁住
            log.info(Thread.currentThread().getName()+"提交修改,当前时间为:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return 1;
}

结果如图:

Throttlestop fivr被锁_多线程

优点:

  1. 实现简单,方便

缺点:

  1. 很明显,调用一次intern方法,就进行一次String常量池进行扫描,效果可想而知

2. 利用其他常量池,例如Integer(-128~127)

取String的hash对127取模,锁住该Interger常量

/**
     * 修改用户信息
     * @param student
     * @return
     */
public int update(Student student){
    Integer hash = (student.getName().hashCode())%128; //计算hash
    log.info(Thread.currentThread().getName()+"当前hash为:"+hash); //打印hash
    synchronized (hash){ //hash必定是-127~127,因此地址是取自Integet常量池中,锁住它
        try {
            //模拟业务操作执行5秒
            Thread.sleep(5000);
            //打印当前时间,如果两条线程打印时间间隔没有超过5秒,证明没锁住
            log.info(Thread.currentThread().getName()+"提交修改,当前时间为:"+new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    return 1;
}

结果:

Throttlestop fivr被锁_java_02

优点:

  1. 不需要浪费其他的内存空间,直接使用Integet常量池

缺点:

  1. 常量池太小 只有254个,很容易造成不同hashCode 取模后碰撞情况,一次碰撞就需要等待一个业务执行的时间
    经测试:随机500个字符串,最大碰撞个数为6个,执行所有线程时间为6次业务时长

3. 使用ConcurrentHashMap+信号量方式

public enum SaveDataLock {
    APPLICANT
    ;
    
    //静态map,存放各种需要锁的字段
    private static final ConcurrentHashMap<String, ConcurrentHashMap<String, Semaphore>> lockMap = new ConcurrentHashMap<>();
    
    private static final Object SYN_STR = new Object();
    
    //为静态map赋值
    static {
        for(SaveDataLock lock : SaveDataLock.values()) {
            SaveDataLock.lockMap.put(lock.name(), new ConcurrentHashMap<>());
        }
    }
    
    public ConcurrentMap<String, Semaphore> getMap() {
        return SaveDataLock.lockMap.get(this.name());
    }
    
    //获取锁
    public Semaphore lock(String lockStr) {
        ConcurrentHashMap<String, Semaphore> locks = SaveDataLock.lockMap.get(this.name());
        Semaphore lock = locks.get(lockStr);
        if(lock == null) {
            synchronized (SYN_STR) {
                lock = locks.get(lockStr);
                if(lock == null) {
                    lock = new Semaphore(1);
                    locks.put(lockStr, lock);
                }
            }
        }
        //使该线程不允许被中断
        lock.acquireUninterruptibly();
        return lock;
    }
    
    //释放锁
    public void release(String lockStr, Semaphore lock) {
        if(lock == null) {
            return ;
        }
        lock.release();
        //判断是否有线程在等待该信号量被释放
        if(!lock.hasQueuedThreads()) {
            ConcurrentHashMap<String, Semaphore> locks = SaveDataLock.lockMap.get(this.name());
            locks.remove(lockStr);
        }
    }
}

获取锁:

当调用获取锁方法时,将会传入一个string字符串参数作为key,在Map中进行查找,如果不为空,则等待别人释放,如果为空则申请信号量操作

释放锁:

当调用释放锁方法时,将锁释放,再判断是否有再等待的线程,如果没有,则删除该信号量

4. 利用Redis分布式锁解决

利用Redis操作的原子性,模拟获取锁和解锁,道理跟信号量差不多,网上大把分布式锁例子,在这里不做阐述。