线程安全

当多线程访问一个对象,不需要考虑线程的调度、交替执行和额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为能获得正确的结果。

线程安全的实现方法:

  • 互斥同步
    • synchronized/ReentrantLock等
    • 线程阻塞和唤醒带来性能问题-Blocking Synchronization
    • 悲观并发策略-认为只要不去做正确的同步就会出问题
  • 非阻塞同步
    • 基于冲突检测的乐观并发策略
    • 需要硬件指令集的支持—保证原子性
      • 保证一个语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这样子就不需要互斥同步了
        • Test-and-Set
        • Fetch-and-Increment
        • Swap
        • Compare-and-Swap, CAS
        • Load-Linked/Store-Conditional
  • 无同步方案
    • 可重入代码Reentrant Code / Pure Code
      • 不依赖存储在对上的数据和共用的系统资源、用到的状态量都由参数传入、不调用非可重入的方法
    • 线程本地存储Thread Local Storage

乐观锁 vs 悲观锁

link

  • 乐观思想,即认为读多写少, java基本使用CAS实现,
    • 自旋锁
      • 获取不到锁让线程执行一个忙循环,先等一等
      • 线程自旋是需要消耗cup的
    • 轻量锁—比偏向锁多了拷贝和cas操作
      • 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝
      • 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word
      • 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁
    • 偏向锁—Biased Locking
      • 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,
      • 当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而提升程序的性能
      • 偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁
  • 即认为写多,遇到并发写的可能性高,使用重量级锁Synchronized

线程阻塞消耗大

  • 要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换
    • 用户态与内核态都有各自专用的内存空间,专用的寄存器等
    • 用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等

CAS

compare and swap, CAS操作需要输入两个数值,一个旧值一个新值,在操作前比较旧值有没有发生变化,如果没有发生变化才交换成新的值,发生了变化则不交换。

  • 模拟实现

    1. //循环CAS
    2. /**
    3. * 使用 CAS 实现 线程 安全 计数器
    4. */
    5. private AtomicInteger atomicI = new AtomicInteger( 0);
    6. private void safeCount() {
    7. for (; ; ) {
    8. int i = atomicI.get();
    9. boolean suc = atomicI.compareAndSet(i, ++i);
    10. if (suc) {
    11. break;
    12. }
    13. }
    14. }
  • 如AtomicInteger里面getAndIncrement()方法实现

    1. public final int getAndIncrement() {
    2. for (; ; ) {
    3. int current = get();
    4. int next = current + 1;
    5. if (compareAndSet(current, next)) return current;
    6. }
    7. }
    8. public final boolean compareAndSet(int expect, int update) {
    9. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    10. }

处理器如何实现原子操作

  • 使用总线锁:
    • 使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求被阻塞住,该处理器就可以独占共享内存
  • 使用缓存锁
    • 执行锁操作回写内存时使用缓存一致性机制
      • 阻止同事修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存无效

synchronized

实现

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态方法,所示当前类的Class对象
  • 对于同步方法块,锁是synchronized括号里面的对象

image.png

ObjectMonitor

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下

monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的

  1. ObjectMonitor() {
  2. _header = NULL;
  3. _count = 0; //记录个数
  4. _waiters = 0,
  5. _recursions = 0;
  6. _object = NULL;
  7. _owner = NULL;
  8. _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
  9. _WaitSetLock = 0 ;
  10. _Responsible = NULL ;
  11. _succ = NULL ;
  12. _cxq = NULL ;
  13. FreeNext = NULL ;
  14. _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  15. _SpinFreq = 0 ;
  16. _SpinClock = 0 ;
  17. OwnerIsThread = 0 ;
  18. }

monitorenter && monitorexit

synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理

  • synchronized修饰的同步代码块
    • synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令
      • 当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权
      • 即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor
  • synchronized修饰的方法
    • JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法
      • 如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法
      • 方法完成(无论是正常完成还是非正常完成)时释放monitor


对long和double类型变量的特殊规则

long和double的非原子性协定—Nonatomic Treatment of double and long variables

  • JMM允许虚拟机不把long和double读写实现原子操作
    • JMM要求 lock、 unlock、 read、 load、 assign、 use、 store、 write 这8 操作都具原子性,
    • 但JMM允许没有被volatile修饰的64位数据的读写操作分为两次32位的操作来进行
    • 在JSR-133之前的旧内存模型中,一个64位long/double型变量的读/写操作可以被拆分为两个32位的读/写操作来执行。
    • JSR-133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR-133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)
  • JMM允许并且强烈建议虚拟机把long和实现具有原子性的操作,目前各种商业平台几乎都把64位数据读写操作作为原子操作来对待
    • 所以不需要把long和double变量专门声明为volatile.

why Thread.suspend() 和 Resume()方法deprecated

死锁风险

  • suspend()中断的线程就是即将要执行resume()的那个线程,那么resume方法则没机会执行所以会产生死锁
  • 线程a执行suspend线程c, 线程b执行resume线程c,以下情况会产生死锁
    • c线程获得了一个monitor m,并且被a suspended了,这时候不会释放锁m
    • b线程想获得锁m执行另外的逻辑后才resume线程c,但是锁一直被suspended的a线程获取了,所以导致死锁