优秀的编程知识分享平台

网站首页 > 技术文章 正文

Java专家质问我为什么多此一举,反而造成线程不安全

nanyue 2024-09-01 20:39:33 技术文章 7 ℃

先上代码



场景是服务节点会不断获取满足执行条件的任务插入任务池中等待执行,任务池是有界的,任务池由一个线程池去不断执行任务,任务执行时会先获取锁,任务执行结束后释放锁(单体应用,目前是通过数据库乐观锁做的,哈哈)。

而因为做了高可用,所以会有锁过期后覆盖清空锁,等待被重新捞起执行的逻辑(watchDdog那一套)。

当在同一个服务节点内,出现任务执行异常导致阻塞,且锁过期时间未及时更新,导致任务锁状态被重置,从而又被本身这个服务节点扫描到的这种情况,如果允许服务节点继续往任务池塞这个数据,会出现这个任务把任务池塞满,导致无法扫描执行其他正常的任务的情况(比较极端)。

所以才有了上面的那个记录当前节点任务池里已存在的任务id的map,这也是本文需要分享的一个大大大BUG。

如图可以看到,采用ConcurrentHashMap来实现,ConcurrentHashMap可以满足记不重复提交taskId的需求且多线程add、remove元素是线程安全的。分为提交任务addTask和释放任务releaseTask两个方法。在提交任务前先尝试插入taskId,如果插入成功才提交任务至任务池,任务处理完调用releaseTask方法释放taskId。

本来可以实现如下(也是正确的方式):



但是为什么当时要加while循环去删除呢?这是因为当时想着,哎,这个删除如果不成功,岂不是会导致记录了这个taskId,实际上任务池已经没有这个任务,不就会导致这个taskId永远不会再被执行吗,还会有内存溢出的风险,就吭哧吭哧加了while循环来保证remove是成功的。所以就写成了这样:


乍一看又没有什么毛病,虽然这个remove如果是false不会出现(出现就是jdk逻辑错误了),如果失败,通过while不断重试删除,直至删除成功为止。貌似没问题啊,但是!



ConcurrentHashMap的remove方法根据key删除时,如果key不存在,返回值是null,如果key存在,那么remove方法的返回值是value值:


回顾一下一开始的releaseTask方法:

public boolean releaseTask(Long taskId) {
    boolean releaseTask = runningTaskMap.remove(taskId) == null;
    while (!releaseTask) {
        releaseTask = runningTaskMap.remove(taskId) == null;
        log.error("尝试释放定时任务失败:{},{}", taskId, releaseTask);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) {
        }
    }
    log.warn("尝试释放定时任务:{},{}", taskId, true);
    return true;
}

当第一次正常执行remove时,taskId存在,返回的是true(add值时value设的是true),但是true==null是false,会导致!releaseTask=true,进入while循环判断,第二次删除时,taskId已经不存在,null==null,!releaseTask=false,退出while循环,这么看貌似有没有什么问题,只是会多打一个释放锁失败的日志罢了。

但是,在多线程的情况下,线程A调用releaseTask方法,成功删除,此时!releaseTask=true,进入while循环,在执行while内remove前,另一个线程B可以就扫到了taskId的任务,此时判断addTask是满足的,因为已经释放了,所以此时这个线程B就把任务重新放进了任务池,线程A执行while里的remove方法又把这个taskId删除了,且还是判断remove结果true==null,又满足!releaseTask=true重新进入while循环,极端情况会出现线程A不停的删除,线程B不停的扫描插入任务。

究其根本,是这个remove结果判断翻车了啊,那是不是改成!=null这样就可以了呢?

public boolean releaseTask(Long taskId) {
    boolean releaseTask = runningTaskMap.remove(taskId)!=null;
    while (!releaseTask) {
        releaseTask = runningTaskMap.remove(taskId)!=null;
        log.error("尝试释放定时任务失败:{},{}", taskId, releaseTask);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) {
        }
    }
    log.warn("尝试释放定时任务:{},{}", taskId, true);
    return true;
}

乍一看也是可以的,但是!这个remove方法,当key不存在时也会返回null,当删除一个不存在的key时,相当于满足了删除失败的情况,这时你会获得:


当然程序没问题的话,不会出现删除一个不存在的taskId的情况(谁能保证呢,哈哈哈)。

这种情况下的删除,推荐还是使用remove(key, value)这个方法,就不用关系值不值,null不null的关系了:


使用这个方法当删除不存在的taskId时,结果还是false,所以还是不能写while保证删除那个,不然还是可能出现死循环的情况。

所以还是简简单单实现,不要搞那么花里胡哨没用的东西,==


你要问要是请求释放不存在的taskId怎么办?打个日志留痕就好啦~!!!

Tags:

最近发表
标签列表