多线程这些线程安全的坑,你在工作中踩了么?

由线程引起的问题往往在测试中难以发现,到了线上就会造成重大的故障和损失

使用多线程的问题很大程度上源于多个线程对同一变量的操作权,以及不同线程之间执行顺序的不确定性


安全性问题

例如有一段很简单的扣库存功能操作,如下:

csharp 复制代码
public int decrement(){
 return --count;//count初始库存为10
}

活跃性问题

活跃性问题指的是,某个操作因为阻塞或循环,无法继续执行下去

最典型的有三种,分别为死锁、活锁和饥饿

死锁

最常见的活跃性问题是死锁

死锁是指多个线程之间相互等待获取对方的锁,又不会释放自己占有的锁,而导致阻塞使得这些线程无法运行下去就是死锁,它往往是不正确的使用加锁机制以及线程间执行顺序的不可预料性引起的

如何预防死锁

性能问题

案例1

使用线程不安全集合(ArrayList、HashMap等)要进行同步,最好使用线程安全的并发集合

在多线程环境下,对线程不安全的集合遍历进行操作时,可能会抛出ConcurrentModificationException的异常,也就是常说的fail-fast机制

下面例子模拟了多个线程同时对ArrayList操作,线程t1遍历list并打印,线程t2向list添加元素

ini 复制代码
List<Integer> list = new ArrayList<>();
list.add(0); 
list.add(1); 
list.add(2);  //list: [0,1,2]
System.out.println(list);

//线程t1遍历打印list
Thread t1 = new Thread(() -> {
  for(int i : list){
    System.out.println(i);
  }
});  

//线程t2向list添加元素
Thread t2 = new Thread(() -> {
  for(int i = 3; i < 6; i++){
    list.add(i);
  }
});

t1.start();
t2.start();

进到抛异常的ArrayList源码中,可以看到遍历ArrayList是通过内部实现的迭代器完成的

调用迭代器的next()方法获取下一个元素时,会先通过checkForComodification()方法检查modCountexpectedModCount是否相等,若不相等则抛出ConcurrentModificationException

modCount是ArrayList的属性,表示集合结构被修改的次数(列表长度发生变化的次数),每次调用add或remove等方法都会使modCount加1

expectedModCount是迭代器的属性,在迭代器实例创建时被赋与和遍历前modCount相等的值(expectedModCount=modCount

所以当有其他线程添加或删除集合元素时,modCount会增加,然后集合遍历时expectedModCount不等于modCount,就会抛出异常

使用加锁机制操作线程不安全的集合类

scss 复制代码
List<Integer> list = new ArrayList<>();
list.add(0); 
list.add(1); 
list.add(2);
System.out.println(list);

//线程t1遍历打印list
Thread t1 = new Thread(() -> {
  synchronized (list){   //使用synchronized关键字
    for(int i : list){
      System.out.println(i);
    }
  }
});  

//线程t2向list添加元素
Thread t2 = new Thread(() -> {
  synchronized (list){
    for(int i = 3; i < 6; i++){
      list.add(i);
      System.out.println(list);
    }
  }
});  

t1.start();
t2.start();

案例2

不要将SimpleDateFormat作为全局变量使用

SimpleDateFormat实际上是一个线程不安全的类,其根本原因是SimpleDateFormat的内部实现对一些共享变量的操作没有进行同步

ini 复制代码
public static final SimpleDateFormat SDF_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static void main(String[] args) {
  //两个线程同时调用SimpleDateFormat.parse方法
  Thread t1 = new Thread(() -> {
    try {
      Date date1 = SDF_FORMAT.parse("2019-12-09 17:04:32");
    } catch (ParseException e) {
      e.printStackTrace();
    }
  });

  Thread t2 = new Thread(() -> {
    try {
      Date date2 = SDF_FORMAT.parse("2019-12-09 17:43:32");
    } catch (ParseException e) {
      e.printStackTrace();
    }
  });

  t1.start();
  t2.start();
}

****

java 复制代码
//初始化
public static final ThreadLocal<SimpleDateFormat> SDF_FORMAT = new ThreadLocal<SimpleDateFormat>(){
  @Override
  protected SimpleDateFormat initialValue() {
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  }
};
//调用
Date date = SDF_FORMAT.get().parse(wedDate);

推荐使用Java8的LocalDateTime和DateTimeFormatter

ini 复制代码
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime time = LocalDateTime.now();
System.out.println(formatter.format(time));

锁的正确释放

假设有这样一段伪代码:

csharp 复制代码
Lock lock = new ReentrantLock();
...  
try{
  lock.tryLock(timeout, TimeUnit.MILLISECONDS)
  //业务逻辑
}
catch (Exception e){
  //错误日志
  //抛出异常或直接返回
}
finally {
  //业务逻辑
  lock.unlock();
}
...

正确使用线程池

案例1

不要将线程池作为局部变量使用

ini 复制代码
public void request(List<Id> ids) {
  for (int i = 0; i < ids.size(); i++) {
     ExecutorService threadPool = Executors.newSingleThreadExecutor();
  }
}

所以尽量将线程池作为全局变量使用

案例2

谨慎使用默认的线程池静态方法

scss 复制代码
Executors.newFixedThreadPool(int);     //创建固定容量大小的线程池
Executors.newSingleThreadExecutor();   //创建容量为1的线程池
Executors.newCachedThreadPool();       //创建一个线程池,线程池容量大小为Integer.MAX_VALUE

上述三个默认线程池的风险点:

  • 所以需要根据自身业务和硬件配置创建自定义线程池

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

相关推荐
!!!52538 分钟前
在SpringBoot项目中策略模式的使用
spring boot·后端·策略模式
5177 小时前
django三级联动
后端·python·django
星辰离彬9 小时前
9.Java 集合框架:List、Set、Map 的使用与选择
java·开发语言·后端
多多*10 小时前
单例模式,饿汉式,懒汉式,在java和spring中的体现
java·开发语言·数据库·后端·spring
不争先.12 小时前
Pycharm and Flask 的学习心得(9)
后端·python·flask
言之。13 小时前
Go 语言接口入门指南
开发语言·后端·golang
代码老y14 小时前
深度解析Apache Tomcat:技术深度、应用场景、最佳实践与未来发展
java·后端·tomcat·apache
星辰离彬14 小时前
Spring Boot + OpenCSV 数据清洗实战:CSV 结构化处理与可视化
java·人工智能·spring boot·后端·算法
攻城狮7号17 小时前
Rust 1.0 发布十周年,梦想再度扬帆起航!
开发语言·后端·游戏·rust
我爱Jack18 小时前
JVM 深度解析
java·开发语言·jvm·后端