深入理解Java虚拟机::线程安全和锁优化

线程安全和锁优化

  • 线程安全
    • 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下 的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
    • 代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保证多线程环境下的正确调用
    • Java 语言中的线程安全
      • 不可变
        • 不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。
        • 最简单的一种就是把对象里面带有状态的变量都声明为 final
        • 常见不可变类型
          • String
          • 枚举类
          • java.lang.Number 的部分子类
          • Long
          • Double
          • BigInteger
          • BigDecimal
      • 绝对线程安全
        • 不管运行时环境如何,调用者都不需要任何额外的同步措施
      • 相对线程安全
        • 需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
      • 线程兼容
        • 线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用
      • 线程对立
        • 线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码
    • 线程安全的实现方法
      • 互斥同步
        • 同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用。
      • 互斥是实现同步的一种手段,临界区(Critical Section)、互斥量 (Mutex)和号量(Semaphore)都是常见的互斥实现方式
      • synchronized
        • synchronized 关键字经过 Javac 编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令 。
        • 这两个字节码指令都需要一个 reference 类型的参数来指明要锁定和解锁的对象
        • 被 synchronized 修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
        • 被 synchronized 修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
        • 持有锁是一个重量级
      • ReentrantLock
        • 等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
        • 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致 ReentrantLock 的性能急剧下降,会明显影响吞吐量。
        • 锁绑定多个条件:是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。在 synchronized 中,锁对象的 wait()跟它的 notify()或者 notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而 ReentrantLock 则无须这样做,多次调用 newCondition()方法即可。
      • 比较
        • 性能接近
        • 只需要基础的同步功能时,更推荐 synchronized。
        • Lock 应该确保在 finally 块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不 会释放持有的锁。这一点必须由程序员自己来保证,而使用 synchronized 的话则可以由 Java 虚拟机来确保即使出现异常,锁也能被自动释放。
      • 非同步阻塞
        • 基于冲突检测的乐观并发策略
        • 如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization),使用这种措施的代码也常被称为无锁(Lock-Free) 编程。
        • 硬件指令保证原子性
          • 测试并设置(Test-and-Set) ;
          • 获取并增加(Fetch-and-Increment) ;
          • 交换(Swap);
          • 比较并交换(Compare-and-Swap,下文称 CAS);
          • 加载链接/条件储存(Load-Linked/Store-Conditional,下文称 LL/SC)。
      • 无同步方案
        • 可重入代码
          • 可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响。
          • 不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等。
        • 线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
  • 锁优化
    • 自旋锁与自适应自旋
      • 自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。
    • 锁消除
      • 锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
    • 锁粗化
      • 如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
    • 轻量级锁
      • 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,然后,虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后两个比特)将转变为“ 00”,表示此对象处于轻量级锁定状态.
    • 偏向锁
      • 它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不去做了