极客时间课程-Java 开发常见错误100例

作者: followtry | 2020-09-29 | 阅读
「编辑」 「本文源码」

1. 使用了并发安全类库,线程安全就高枕无忧了吗?

1. 未意识到线程重用导致的用户信息错乱-ThreadLocal

线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。Java代码本来就运行在多线程环境中,不能因为没有显示开启多线程就不会有安全问题。

使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。

2. 使用了线程安全的并发工具,并不代表解决了所有线程安全问题 - concurrentHashMap

ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的

ConcurrentHashMap 对外提供的方法或能力的限制:

  1. 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
  2. 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
  3. 诸如 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 时内存的申请释放消耗会很大

小结

  1. 对于 Threadlocal,需要考虑在多线程情况下的内存泄漏,用完后清理ThreadLocal 内的数据
  2. 对于 ConcurrentHashMap ,充分使用期 CAS 的特性
  3. 对于 CopyOnWriteArrayList,要熟悉其使用场景,读多写少或无锁读。避免滥用。

2. 代码加锁:不要让“锁”事成为烦心事

1. 加锁前要清楚锁和被保护的对象是不是一个层面的

  1. 静态字段属于类,类级别的锁才能保护。
  2. 而非静态字段属于类实例,实例级别的锁就可以保护。

synchronized 加在实例方法上,属于实例级别的锁。加载静态方法上属于类级别的锁。实际级别的锁是无法保证静态字段的线程安全问题的。

2. 加锁要考虑锁的粒度和场景问题

  1. 在方法上加锁synchronized,通常情况下是没有必要的,锁的粒度太粗。粗粒度的锁可能会影响性能。
  2. 加锁尽可能最小粒度,仅对必要的代码块甚至是需要保护的资源进行加锁。
  3. 区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。

更细粒度的锁

  1. 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
  2. 如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。
  3. JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。

3. 多把锁要小心死锁问题

避免循环无限等待。

死锁的四个必要条件

  1. 互斥条件:资源独占
  2. 请求和保持条件:进程相互持有资源并等待另一资源
  3. 循环等待:两个进行相互持有并相互等待
  4. 不剥夺条件:无限等待知道占用进程释放

版权声明:本文由 followtry 在 2020年09月29日发表。本文采用CC BY-NC-SA 4.0许可协议,非商业转载请注明出处,不得用于商业目的。
文章题目及链接:《极客时间课程-Java 开发常见错误100例》




  相关文章:

「游客及非Github用户留言」:

「Github登录用户留言」:

TOP