由线程引起的问题往往在测试中难以发现,到了线上就会造成重大的故障和损失
使用多线程的问题很大程度上源于多个线程对同一变量的操作权,以及不同线程之间执行顺序的不确定性
安全性问题
例如有一段很简单的扣库存功能操作,如下:
public int decrement(){
return --count;//count初始库存为10
}
活跃性问题
活跃性问题指的是,某个操作因为阻塞或循环,无法继续执行下去
最典型的有三种,分别为死锁、活锁和饥饿
死锁
最常见的活跃性问题是死锁
死锁是指多个线程之间相互等待获取对方的锁,又不会释放自己占有的锁,而导致阻塞使得这些线程无法运行下去就是死锁,它往往是不正确的使用加锁机制以及线程间执行顺序的不可预料性引起的
如何预防死锁
性能问题
案例1
使用线程不安全集合(ArrayList、HashMap等)要进行同步,最好使用线程安全的并发集合
在多线程环境下,对线程不安全的集合遍历进行操作时,可能会抛出ConcurrentModificationException
的异常,也就是常说的fail-fast
机制
下面例子模拟了多个线程同时对ArrayList操作,线程t1遍历list并打印,线程t2向list添加元素
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()
方法检查modCount
和expectedModCount
是否相等,若不相等则抛出ConcurrentModificationException
modCount是ArrayList的属性,表示集合结构被修改的次数(列表长度发生变化的次数),每次调用add或remove等方法都会使modCount加1
expectedModCount是迭代器的属性,在迭代器实例创建时被赋与和遍历前modCount相等的值(expectedModCount=modCount
)
所以当有其他线程添加或删除集合元素时,modCount会增加,然后集合遍历时expectedModCount不等于modCount,就会抛出异常
使用加锁机制操作线程不安全的集合类
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的内部实现对一些共享变量的操作没有进行同步
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();
}
//初始化
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
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime time = LocalDateTime.now();
System.out.println(formatter.format(time));
锁的正确释放
假设有这样一段伪代码:
Lock lock = new ReentrantLock();
...
try{
lock.tryLock(timeout, TimeUnit.MILLISECONDS)
//业务逻辑
}
catch (Exception e){
//错误日志
//抛出异常或直接返回
}
finally {
//业务逻辑
lock.unlock();
}
...
正确使用线程池
案例1
不要将线程池作为局部变量使用
public void request(List<Id> ids) {
for (int i = 0; i < ids.size(); i++) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
}
}
所以尽量将线程池作为全局变量使用
案例2
谨慎使用默认的线程池静态方法
Executors.newFixedThreadPool(int); //创建固定容量大小的线程池
Executors.newSingleThreadExecutor(); //创建容量为1的线程池
Executors.newCachedThreadPool(); //创建一个线程池,线程池容量大小为Integer.MAX_VALUE
上述三个默认线程池的风险点:
- 所以需要根据自身业务和硬件配置创建自定义线程池
最后说一句(求关注!别白嫖!)
如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。
关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!