第一讲:使用并发工具库类,建议
容易犯的四类错:
-
只知道使用并发工具,但并不清楚当前线程的来龙去脉,解决多线程问题却不了解线程;--错误
-
误以为使用了并发工具就可以解决一切线程安全问题,期望通过把线程不安全的类替换为线程安全的类来一键解决问题。--错误
-
没有充分了解并发工具的特性,还是按照老方式使用新工具导致无法发挥其性能。
-
没有了解清楚工具的适用场景,在不合适的场景下使用了错误的工具导致性能更差。
两点建议:
-
一定要认真阅读官方文档(比如 Oracle JDK 文档)。充分阅读官方文档,理解工具的适用场景及其 API 的用法,并做一些小实验。了解之后再去使用,就可以避免大部分坑。
-
如果你的代码运行在多线程环境下,那么就会有并发问题,并发问题不那么容易重现,可能需要使用压力测试模拟并发场景,来发现其中的 Bug 或性能问题。
第二讲:代码加锁
加锁范围
import lombok.Getter;
import java.util.stream.IntStream;
public class Test {
public static void main(String[] args) throws Exception {
IntStream.rangeClosed(1, 100000).parallel().forEach(i -> new Data().wrong());
System.out.println(Data.getCounter());
}
static class Data {
@Getter
private static int counter = 0;
public Data() {
}
public synchronized void wrong() {
counter++;
}
}
}
以上代码的输出结果为:36529;为什么不是100000。
问题分析:在非静态的 wrong 方法上加锁,只能确保多个线程无法执行同一个实例的 wrong 方法,却不能保证不会执行不同实例的 wrong 方法。而静态的 counter 在多个实例中共享,所以必然会出现线程安全问题。
修改后:
import lombok.Getter;
import java.util.stream.IntStream;
public class Test {
public static void main(String[] args) throws Exception {
IntStream.rangeClosed(1, 100000).parallel().forEach(i -> new Data().right1());
System.out.println(Data.getCounter());
}
static class Data {
@Getter
private static int counter = 0;
private static Object locker = new Object(); // 添加一个静态类锁
public Data() {
}
public void right1() {
synchronized (locker) {
counter++;
}
}
public static synchronized void right2(){
counter++;
}
} }
以上有两种方法可以解决这个问题,一个是代码块级别的synchronized ,另一个是方法上标记 synchronized 关键字;
这两种解决方式有什么差异:
1- 代码块级别:更灵活一些,锁的范围也更小一些; 最主要的差异,非静态的同步方法是实例级别的锁;
2- static方法级别:灵活性差些。是类级别的锁;
作者给的答案是第一种,第二种属于滥用synchronized的做法,原因如下:
1- 对于无状态的方法,没有必要;
2- 可能会极大地降低性能。
即使我们确实有一些共享资源需要保护,也要尽可能降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁。
如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。
多把锁要小心死锁问题
日志:Found one Java-level deadlock

死锁:线程4等待线程3,线程3等待线程4;
错误代码:
@GetMapping("wrong")
public long wrong() {
long begin = System.currentTimeMillis();
//并发进行100次下单操作,统计成功次数
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Item> cart = createCart();
return createOrder(cart);
})
.filter(result -> result)
.count();
log.info("success:{} totalRemaining:{} took:{}ms items:{}",
success,
items.entrySet().stream().map(item -> item.getValue().remaining).re
System.currentTimeMillis() - begin, items);
return success;
}
正确代码(排序):
@GetMapping("right")
public long right() {
...
long success = IntStream.rangeClosed(1, 100).parallel()
.mapToObj(i -> {
List<Item> cart = createCart().stream()
.sorted(Comparator.comparing(Item::getName))
.collect(Collectors.toList());
return createOrder(cart);
})
.filter(result -> result)
.count();
...
return success;
}
如果业务逻辑中锁的实现比较复杂的话,要仔细看看加锁和释放是否配对,是否有遗漏释放或重复释放的可能性;并且要考虑锁自动超时释放了,而业务逻辑却还在进行的情况下,如果别的线线程或进程拿到了相同的锁,可能会导致重复执行。
我理解的是: 1- 锁机制排序加锁; 2- 超时释放锁; 这两种方式是目前解决死锁的两个基本方法。