大道至简,知易行难
广阔天地,大有作为

synchronize关键字用于可重用对象时的并发问题

      今天使用FindBugs扫描之前写的某个项目时发现了一个此前没有注意过的问题,在解决这个问题的过程中发现之前对该问题引出的多处细节知识理解严重不足,故整理如下。
      FindBugs将这个问题描述为“Synchronization on Boolean”,并Rank为Scariest级别:
      具体到笔者所写的代码,该Pattern对应了一段多线程环境下在一定意义上使用了延迟加载策略的代码,这一段类似的逻辑笔者曾经在多个工程中的许多个功能点上使用:
11上述代码的基本作用就是在出现并发时,通过同步锁使得只能由一个线程延迟加载一个或一组对象供后续使用(例如一个巨大的List或需要缓存的对象等等)。然而,此前竟然从未意识到可能存在的严重问题。

根据FindBugs的分类,该问题归属于可能导致死锁的多线程问题(Type: DL, Category: Multithreaded correctness)。类似于该问题的还有Synchronization on boxed primitive(在装箱基本类型上使用了synchronize关键字)、Synchronization on interned String(在内部字符串上使用了synchronize关键字)和Synchronization on boxed primitive values(在显式实例化的装箱对象上使用了synchronize关键字):

      这个问题在《CERT Oracle Coding Standard for Java》中属于LCK(Locking)的规则之一,原始出处为《LCK01-J. Do not synchronize on objects that may be reused》。其中,作者认为错误地在基本类型上使用synchronize关键字是导致并发问题的常见原因之一,可能导致死锁或其他不可预测的结果,并将其抽象为不应该在任何可能被重用的对象[包括可能在JVM内部重用的对象]上使用synchronize关键字(Misuse of synchronization primitives is a common source of concurrency issues. Synchronizing on objects that may be reused can result in deadlock and nondeterministic behavior. Consequently, programs must never synchronize on objects that may be reused.)。在CERT的此条规范中,共指出了四种情况:
1、Synchronizes on a Boolean object(在Boolean类型的对象上使用了Synchronize关键字)
此种情况就是本文开头中FindBugs发现的问题,即Boolean类型的对象不可用于synchronization同步锁。其问题的实质在于Boolean.TRUE和Boolean.FALSE这两个常量在JVM中实际是java.lang.Boolean类的两个静态成员变量,因而可能在程序中被多处引用,在JVM源码中可见:
例如,当上例中的inited指向Boolean.FALSE时,如果有其他的同步代码块在无意中使用了相同的Boolean常量,那么就有可能导致死锁(In this example, inited refers to the instance corresponding to the value Boolean.FALSE. If any other code were to inadvertently synchronize on a Boolean literal with this value, the lock instance would be reused and the system could become unresponsive or could deadlock.)。
2、Synchronizes on a boxed primitive object(在装箱基本类型的对象上使用了Synchronize关键字)

上述代码将int类型的count自动装箱为Interger包装类型的对象Lock,然后使用synchronize关键字对包装类型的Lock变量加锁。可以预见,出于存储和性能等等的考虑,在自动装箱时JVM内部必然会重用具有相同值类型的包装类,因而Lock指向的对象极有可能被重用,进而在后续引发与Boolean类型变量存在的相同问题(Boxed types may use the same instance for a range of integer values; consequently, they suffer from the same reuse problem as Boolean constants. The wrapper object are reused when the value can be represented as a byte; JVM implementations are also permitted to reuse wrapper objects for larger ranges of values.)。

3、Synchronizes on a interned String lock object(在内部字符串对象上使用了Synchronize关键字)

根据Java API文档,intern()方法实际是返回对象池中的对象,因而调用intern()方法后获得到对象相当于JVM中的一个全局变量:

所以,即便是像上述错误代码中使用private和final关键字修饰lock变量,lock变量指向的仍然是同一个可能被重用的字符串常量。这种情况所带来的问题与之前提到的两种问题类似。

4、Synchronizes on a String Literal(在字符串常量上使用了Synchronize关键字)
7

基于之前的解释很容易理解第四种情况,即JVM中的字符串常量是全局重用的。

CERT规范给出了如下的几个应对方法
1、使用非装箱的Interger
当显式实例化Integer类型的变量时,相应的Integer变量会装箱相同的简单类型值但是建立唯一的引用,从而避免自动装箱导致的问题:
      虽然使用非装箱的Interger可以解决自动装箱类型导致的问题,但是由于程序员很难区分一个Integer到底是自动装箱的还是显式实例化的所以同样会导致维护性问题(因而最好使用private final类型的java.lang.Object,也就是下述的第三种解决方法)。这一段笔者觉得翻译成中文可能不太好理解,英文原文如下:When explicitly constructed, an Integer object has a unique reference and its own intrinsic lock that is distinct not only from other Integer objects, but also from boxed integers that have the same value. While this is an acceptable solution, it can cause maintenance problems because developers can incorrectly assume that boxed integers are also appropriate lock objects. A more appropriate solution is to synchronize on a private final lock object as described in the final compliant solution for this rule.
      这种情况实际上就是FindBugs中的“Synchronization on boxed primitive values”,FindBugs认为使用这种解决方法可能是正确的,但同样是令人困惑的而且可能会由于后续的重构[例如在IntelliJ中去除装箱时]而引发问题(The existing code might be OK, but it is confusing and a future refactoring, such as the “Remove Boxing” refactoring in IntelliJ, might replace this with the use of an interned Integer object shared throughout the JVM, leading to very confusing behavior and potential deadlock.)。
2、使用字符串实例
与字符串常量不同,字符串实例的引用是唯一的,因而不存在字符串常量可能导致的问题。
3、使用private final类型的java.lang.Object

CERT规范中提到,这种解决方法是少数可以直接利用到java.lang.Object的情况。在此处之所以强调使用Raw Object,同时还在CERT规范的原文中多次提到《Use private final lock objects to synchronize classes that may interact with untrusted code》,是由于不使用Raw Object可能导致被exploit的问题,在此不再赘述。

转载时请保留出处,违法转载追究到底:进城务工人员小梅 » synchronize关键字用于可重用对象时的并发问题

分享到:更多 ()

评论 2

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #1

    梅老本总结的很到位👍, 基本类型的字面量有缓存池,Boolean的就是true和false,Integer对象的缓存了-128-+127之间的值,所以直接使用字面量作为lock对象的话,这个区间内的值其实是同一个资源,会有死锁问题。用这个区间范围外的,比如128~~非boxed也是可以的。

    Moon7个月前 (12-06)回复
    • 感谢,学习了!

      mlkui6个月前 (12-10)回复