Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >日常工作中最容易犯的几个并发错误

日常工作中最容易犯的几个并发错误

作者头像
捡田螺的小男孩
发布于 2020-04-15 10:38:25
发布于 2020-04-15 10:38:25
33900
代码可运行
举报
运行总次数:0
代码可运行

前言

列举大家平时在工作中最容易犯的几个并发错误,都是在实际项目代码中看到的鲜活例子,希望对大家有帮助。

First Blood

线上总是出现:ERROR 1062 (23000) Duplicate entry 'xxx' for key 'yyy',我们来看一下有问题的这段代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
UserBindInfo info = selectFromDB(userId);
 
if(info == null){
 
    info = new UserBindInfo(userId,deviceId);
 
    insertIntoDB(info);
 
}else{
 
    info.setDeviceId(deviceId);
 
    updateDB(info);
 
 }
 

并发情况下,第一步判断都为空,就会有2个或者多个线程进入插入数据库操作,这时候就出现了同一个ID插入多次。

正确处理姿势:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
insert into UserBindInfo values(#{userId},#{deviceId}) on duplicate key update deviceId=#{deviceId}多次的情况,导致插入失败。
 

一般情况下,可以用insert...on duplicate key update... 解决这个问题。

注意: 如果UserBindInfo表存在主键以及一个以上的唯一索引,在并发情况下,使用insert...on duplicate key,可能会产生死锁(Mysql5.7),可以这样处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
try{
 
 UserBindInfoMapper.insertIntoDB(userBindInfo);
 
}catch(DuplicateKeyException ex){
 
 UserBindInfoMapper.update(userBindInfo);
 
}
 

Double Kill

小心你的全局变量,如下面这段代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class GlobalVariableConcurrentTest {
 

 
 private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 

 
 public static void main(String[] args) throws InterruptedException {
 
 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));
 

 
 while (true){
 
            threadPoolExecutor.execute(()->{
 
 String dateString = sdf.format(new Date());
 
 try {
 
 Date parseDate = sdf.parse(dateString);
 
 String dateString2 = sdf.format(parseDate);
 
 System.out.println(dateString.equals(dateString2));
 
 } catch (ParseException e) {
 
                    e.printStackTrace();
 
 }
 
 });
 
 }
 

 
 }
 

 
}
 

可以看到有异常抛出

全局变量的SimpleDateFormat,在并发情况下,存在安全性问题,阿里Java规约明确要求谨慎使用它。

除了SimpleDateFormat,其实很多时候,面对全局变量,我们都需要考虑并发情况是否存在问题,如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Component
 
public class Test {
 

 
 public static List<String> desc = new ArrayList<>();
 

 
 public List<String> getDescByUserType(int userType) {
 
 if (userType == 1) {
 
            desc.add("普通会员不可以发送和查看邮件,请购买会员");
 
 return desc;
 
 } else if (userType == 2) {
 
            desc.add("恭喜你已经是VIP会员,尽情的发邮件吧");
 
 return desc;
 
 }else {
 
            desc.add("你的身份未知");
 
 return desc;
 
 }
 
 }
 
}
 

因为desc是全局变量,在并发情况下,请求getDescByUserType方法,得到的可能并不是你想要的结果。

Trible Kill

假设现在有如下业务:控制同一个用户访问某个接口的频率不能小于5秒。一般很容易想到使用redis的 setnx操作来控制并发访问,于是有以下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(RedisOperation.setnx(userId, 1)){
 
 RedisOperation.expire(userId,5,TimeUnit.SECONDS));
 
 //执行正常业务逻辑
 
}else{
 
 return “访问过于频繁”;
 
}
 

假设执行完setnx操作,还没来得及设置expireTime,机器重启或者突然崩溃,将会发生死锁。该用户id,后面执行setnx永远将为false,这可能让你永远损失那个用户

那么怎么解决这个问题呢,可以考虑用SET key value NX EX max-lock-time ,它是一种在 Redis 中实现锁的方法,是原子性操作,不会像以上代码分两步执行,先set再expire,它是一步到位

客户端执行以上的命令:

  • 如果服务器返回 OK ,那么这个客户端获得锁。
  • 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
  • 设置的过期时间到达之后,锁将自动释放

Quadra Kill

我们看一下有关ConcurrentHashMap的一段代码,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//全局变量
 
Map<String, Integer> map = new ConcurrentHashMap(); 
 

 
Integer value = count.get(k);
 
if(value == null){
 
       map.put(k,1);
 
}else{
 
    map.put(k,value+1);
 
}
 

假设两条线程都进入 value==null,这一步,得出的结果是不是会变小?OK,客官先稍作休息,闭目养神一会,我们验证一下,请看一个demo:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public static void main(String[] args) {
 
 for (int i = 0; i < 1000; i++) {
 
            testConcurrentMap();
 
 }
 
 }
 
 private static void testConcurrentMap() {
 
 final Map<String, Integer> count = new ConcurrentHashMap<>();
 
 ExecutorService executorService = Executors.newFixedThreadPool(2);
 
 final CountDownLatch endLatch = new CountDownLatch(2);
 
 Runnable task = ()-> {
 
 for (int i = 0; i < 5; i++) {
 
 Integer value = count.get("k");
 
 if (null == value) {
 
 System.out.println(Thread.currentThread().getName());
 
                        count.put("k", 1);
 
 } else {
 
                        count.put("k", value + 1);
 
 }
 
 }
 
                endLatch.countDown();
 
 };
 

 
        executorService.execute(task);
 
        executorService.execute(task);
 

 
 try {
 
            endLatch.await();
 
 if (count.get("k") < 10) {
 
 System.out.println(count);
 
 }
 
 } catch (Exception e) {
 
            e.printStackTrace();
 
 }
 

表面看,运行结果应该都是10对吧,好的,我们再看运行结果 :

运行结果出现了5,所以这样实现是有并发问题的,那么正确的实现姿势是啥呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Map<K,V> map = new ConcurrentHashMap(); 
 
V v = map.get(k);
 
if(v == null){
 
        V v = new V();
 
        V old = map. putIfAbsent(k,v);
 
 if(old != null){
 
                  v = old;
 
 }
 
}
 

可以考虑使用putIfAbsent解决这个问题

(1)如果key是新的记录,那么会向map中添加该键值对,并返回null。

(2)如果key已经存在,那么不会覆盖已有的值,返回已经存在的值

我们再来看看以下代码以及运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 public static void main(String[] args) {
 
 for (int i = 0; i < 1000; i++) {
 
            testConcurrentMap();
 
 }
 
 }
 

 
 private static void testConcurrentMap() {
 
 ExecutorService executorService = Executors.newFixedThreadPool(2);
 
 final Map<String, AtomicInteger> map = Maps.newConcurrentMap();
 
 final CountDownLatch countDownLatch = new CountDownLatch(2);
 

 
 Runnable task = ()-> {
 
 AtomicInteger oldValue;
 
 for (int i = 0; i < 5; i++) {
 
                    oldValue = map.get("k");
 
 if (null == oldValue) {
 
 AtomicInteger initValue = new AtomicInteger(0);
 
                        oldValue = map.putIfAbsent("k", initValue);
 
 if (oldValue == null) {
 
                            oldValue = initValue;
 
 }
 
 }
 
                    oldValue.incrementAndGet();
 
 }
 
            countDownLatch.countDown();
 
 };
 

 
        executorService.execute(task);
 
        executorService.execute(task);
 

 
 try {
 
            countDownLatch.await();
 
 System.out.println(map);
 
 } catch (Exception e) {
 
            e.printStackTrace();
 
 }
 
 }
 

Penta Kill

现有如下业务场景:用户手上有一张现金券,可以兑换相应的现金,

错误示范一

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(isAvailable(ticketId){
 
 1、给现金增加操作
 
 2、deleteTicketById(ticketId)
 
}else{
 
 return “没有可用现金券”
 
}
 

解析: 假设有两条线程A,B兑换现金,执行顺序如下:

  • 1.线程A加现金
  • 2.线程B加现金
  • 3.线程A删除票标志
  • 4.线程B删除票标志

显然,这样有问题了,已经给用户加了两次现金了

错误示范2

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(isAvailable(ticketId){
 
 1、deleteTicketById(ticketId)
 
 2、给现金增加操作
 
}else{
 
 return “没有可用现金券”
 
}
 

并发情况下,如果一条线程,第一步deleteTicketById删除失败了,也会多添加现金。

正确处理方案

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(deleteAvailableTicketById(ticketId) == 1){
 
 1、给现金增加操作
 
}else{
 
 return “没有可用现金券”
 
}
 

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 捡田螺的小男孩 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
日常开发中并发与一致性的一些坑
并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。在这里,我举一个生活的例子,来比喻并发与并行。
捡田螺的小男孩
2020/04/15
8660
日常开发中并发与一致性的一些坑
90%面试都会问到的知识点,你看会吗?
HashTable:线程安全,每个方法都加了 synchronized 修饰。类似 Collections.synchronizedMap(hashMap)
好好学java
2018/12/28
5140
BATJ面试必会之并发篇
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
乔戈里
2019/03/09
6500
并发编程- java.util.concurrent用户指南
本指南根据 Jakob Jenkov 最新博客翻译,请随时关注博客更新:http://tutorials.jenkov.com/java-util-concurrent/index.html。 本指南已做成中英文对照阅读版的 pdf 文档,有兴趣的朋友可以去 Java并发工具包java.util.concurrent用户指南中英文对照阅读版.pdf[带书签] 进行下载。
高广超
2018/12/12
1.1K0
【纯干货】Java 并发进阶常见面试题总结
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
Java技术江湖
2019/12/06
5920
【纯干货】Java 并发进阶常见面试题总结
(82) 理解ThreadLocal / 计算机程序的思维逻辑
本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨。 基本概念和用法 线程本地变量是说,每个线程都有同一个变量的独有拷贝,这个概念听上去比较难以理解,我们先直接来看类TheadLocal的用法。 ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法,有两个主要的public方法: public T get() public void set(T value) set就是设置值,
swiftma
2018/01/31
6310
Java并发知识点快速复习手册(下)
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
Rude3Knife的公众号
2019/08/07
4860
Java并发知识点快速复习手册(下)
服了,一个ThreadLocal被问出了花
地铁上,小帅无力地倚靠着杆子,脑子里尽是刚才面试官的夺命连环问,“用过ThreadLocal么?ThreadLocal是如何解决共享变量访问的安全性的呢?你觉得啥场景下会用到ThreadLocal? 我们在日常用ThreadLocal的时候需要注意什么?ThreadLocal在高并发场景下会造成内存泄漏吗?为什么?如何避免?......”
程序员老猫
2024/02/22
1880
服了,一个ThreadLocal被问出了花
《玩转Java并发工具、精通JUC、成为并发多面手》构建高性能缓存
《玩转Java并发工具、精通JUC、成为并发多面手》构建高性能缓存这部分的个人笔记。本节为单纯的实战,主要是把之前学习并发编程的知识点串起来。
阿东
2023/06/27
2870
多线程之并发工具类
在开发过程中经常会碰到一个任务需要开启多个线程,然后将多个线程的执行结果汇总。比如说查询全量数据,考虑数据量的问题,我们基本上会做分页,这时候就需要多次循环调用。CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。 执行原理 CountDownLatch内部实现了AQS,初始化CountDownLatch的时候,会调用Sync的构造方法将count赋值给state变量。多个线程调用countDown的时候,是使用CAS递减state的值;调用await方法的线程会被放在AQS阻塞队列中,等待计数器为0时,唤醒该线程。 核心方法
OPice
2019/10/23
3500
Java 并发编程·Java 并发
可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。
数媒派
2022/12/01
2.9K0
Java并发知识点快速复习手册(上)
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
Rude3Knife的公众号
2019/08/07
4210
Java并发知识点快速复习手册(上)
JUC并发编程
并发编程是指多个线程同时执行程序的情况。在并发编程中,由于多个线程可能同时访问共享资源,因此需要考虑线程同步、原子性、可见性等问题。
一只
2024/06/26
970
ThreadLocal探索使用
场景:利用SimpleDateFormat格式化时间,因为SimpleDateFormat线程不安全,每次都new一个优点浪费,想着每个线程内部维持1个SimpleDateFormat,然而自定义Pet是可以做到的,SimpleDateFormat做不到,好奇怪啊
九转成圣
2024/04/10
1010
Java日常开发的21个坑,你踩过几个?
最近看了极客时间的《Java业务开发常见错误100例》,再结合平时踩的一些代码坑,写写总结,希望对大家有帮助,感谢阅读~
捡田螺的小男孩
2020/12/29
1.4K0
Java日常开发的21个坑,你踩过几个?
Java多线程并发编程一览笔录
知识体系图: 1、线程是什么? 线程是进程中独立运行的子任务。 2、创建线程的方式 方式一:将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法 方式二:声明实现 Runn
斯武丶风晴
2018/03/01
8890
Java多线程并发编程一览笔录
Java并发:ThreadLocal的简单介绍
前面在线程的安全性中介绍过全局变量(成员变量)和局部变量(方法或代码块内的变量),前者在多线程中是不安全的,需要加锁等机制来确保安全,后者是线程安全的,但是多个方法之间无法共享
汤圆学Java
2021/05/27
3150
Java并发学习之ThreadLocal使用及原理介绍
ThreadLocal使用及原理介绍 线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的(即隔离的) 1. 使用姿势一览 使用方式也比较简单,常用的三个方法 // 设置当前线程的线程局部变量的值 void set(Object value); // 该方法返回当前线程所对应的线程局部变量 public Object get(); // 将当前线程局部变量的值删除 public void remove(); 下面给个实例,来瞅一下,这个东西一般的使用姿势。通常要获取线程变量,
一灰灰blog
2018/02/06
5180
探索ThreadLocal的使用与SimpleDateFormat的多线程问题
在Java的多线程编程中,我们常常会遇到某些类在多线程环境下不安全的问题,例如SimpleDateFormat。由于SimpleDateFormat不是线程安全的,直接在多线程中共享一个实例会导致各种奇怪的问题。因此,我们需要寻找一种有效的方法来使每个线程拥有一个独立的SimpleDateFormat实例。本文将深入探讨如何利用ThreadLocal实现这个目标,并分析其中的一些陷阱和解决方案。
九转成圣
2024/06/05
2250
Java并发编程与高并发之线程安全策略
1、安全的发布对象,有一种对象只要发布了,就是安全的,就是不可变对象。一个类的对象是不可变的对象,不可变对象必须满足三个条件。
别先生
2020/02/12
5050
相关推荐
日常开发中并发与一致性的一些坑
更多 >
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验