1. 使用了并发安全类库,线程安全就高枕无忧了吗?
1. 未意识到线程重用导致的用户信息错乱-ThreadLocal
线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。Java代码本来就运行在多线程环境中,不能因为没有显示开启多线程就不会有安全问题。
使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。
2. 使用了线程安全的并发工具,并不代表解决了所有线程安全问题 - concurrentHashMap
ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。
ConcurrentHashMap 对外提供的方法或能力的限制:
- 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
- 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
- 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。
使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最后返回 Value。由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。
像 ConcurrentHashMap 这样的高级并发工具的确提供了一些高级 API,只有充分了解其特性才能最大化其威力,而不能因为其足够高级、酷炫盲目使用。
3. 没有认清并发工具的使用场景,因而导致性能问题 copyOnWriteArrayList
在 Java 中,CopyOnWriteArrayList 虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景。
每次 add 时,都会用 Arrays.copyOf 创建一个新数组,频繁 add 时内存的申请释放消耗会很大
小结
- 对于 Threadlocal,需要考虑在多线程情况下的内存泄漏,用完后清理ThreadLocal 内的数据
- 对于 ConcurrentHashMap ,充分使用期 CAS 的特性
- 对于 CopyOnWriteArrayList,要熟悉其使用场景,读多写少或无锁读。避免滥用。
2. 代码加锁:不要让“锁”事成为烦心事
1. 加锁前要清楚锁和被保护的对象是不是一个层面的
- 静态字段属于类,类级别的锁才能保护。
- 而非静态字段属于类实例,实例级别的锁就可以保护。
synchronized 加在实例方法上,属于实例级别的锁。加载静态方法上属于类级别的锁。实际级别的锁是无法保证静态字段的线程安全问题的。
2. 加锁要考虑锁的粒度和场景问题
- 在方法上加锁synchronized,通常情况下是没有必要的,锁的粒度太粗。粗粒度的锁可能会影响性能。
- 加锁尽可能最小粒度,仅对必要的代码块甚至是需要保护的资源进行加锁。
- 区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。
更细粒度的锁
- 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
- 如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。
- JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。
3. 多把锁要小心死锁问题
避免循环无限等待。
死锁的四个必要条件
- 互斥条件:资源独占
- 请求和保持条件:进程相互持有资源并等待另一资源
- 循环等待:两个进行相互持有并相互等待
- 不剥夺条件:无限等待知道占用进程释放
版权声明:本文由 followtry 在 2020年09月29日发表。本文采用CC BY-NC-SA 4.0许可协议,非商业转载请注明出处,不得用于商业目的。
文章题目及链接:《极客时间课程-Java 开发常见错误100例》