文章目录
  1. 内存模型与并发
    1. 1 内存模型
      1. 1.1 硬件层面的缓存一致性
      2. 1.2 Java内存模型
        1. 1.2.1 Java内存模型概述
        2. 1.2.2 volatile变量
        3. 1.2.3 long与double
        4. 1.2.4 原子性、可见性与有序性
        5. 1.2.5 先行发生原则
      3. 1.3 Java与线程
        1. 1.3.1 线程的实现
        2. 1.3.2 线程调度
        3. 1.3.3 状态转换
    2. 2 线程安全与锁优化
      1. 2.1 线程安全
        1. 2.1.1 Java的线程安全
        2. 2.1.2 Java的线程安全的实现方法
      2. 2.2 锁优化
        1. 2.2.1 自旋锁与自适应锁
        2. 2.2.2 锁消除
        3. 2.2.3 锁粗化
        4. 2.2.4 轻量级锁
        5. 2.2.5 偏向锁

内存模型与并发

1 内存模型

1.1 硬件层面的缓存一致性

  • CPU核心、高速缓存与主内存之间通过缓存一致性协议保持数据的版本问题

    1.2 Java内存模型

    1.2.1 Java内存模型概述

  • 每个线程拥有自己的工作内存,不同线程间的工作内存不可见,线程所有操作都操作工作内存
  • 工作内存与主内存之间的数据同步通过定义的一些原子操作实现(lock、unlock、read、load、use、assign(赋值)、store、write)(1.5之后不再使用这种方式描述内存模型,使用先行发生原则)

    1.2.2 volatile变量

  • volatile只保证可见性,并非线程安全,多线程仍然会出现不安全的
  • 运算结果不依赖当前volatile变量或不需要与其他变量共同参与不变约束时,推荐使用volatile
  • volatile禁止指令重排序,线程内表现为串行

    1.2.3 long与double

  • Java内存模型不保证64位数据类型的load、store、read、write 4个操作的原子性,long与double的非原子性协定,但现在的虚拟机已经实现原子性

    1.2.4 原子性、可见性与有序性

  • 原子性:字节码monitorenter与monitorexit实现(管程)
  • 可见性:新值同步主存,读取从主存刷新方式实现,另外,volatile保证新值立即同步主存,读取时立即从内存刷新,synchronized块中,对一个变量执行unlock之前必须刷回主存,final保证一单初始化完成,其他线程立即可见
  • 有序性:线程内观察,所有操作有序,观察其他线程,所有操作无序(线程内表现为串行语义;指令重排序与工作内存与主存的差异)

1.2.5 先行发生原则

  • Java内存模型中存在天然的先后操作顺序,无需同步措施,如果两个操作不属于这些规则,则不保证顺序性
  • 先行发生原则:
    • 程序次序: 最终按照代码顺序执行
    • 管程锁定: unlock操作先于同一个锁的后一个lock操作(时间的先后)
    • volatile变量:对一个volatile变量的写先于后面的读(时间的先后)
    • 线程启动: Thread的start先于线程的每一个动作
    • 线程终止: 线程中的左右操作都先于对该线程的终止检测
    • 线程中断: 对线程interrupt()方法的调用先于被中断线程的代码检测到中断事件的发生
    • 对象终结: 一个对象的初始化完成先于finalize()方法
    • 传递性: A先于B,B先于C,A先于C
  • 一个操作时间上的先发生不代表该操作先行发生

1.3 Java与线程

1.3.1 线程的实现

  • 使用内核线程实现:程序使用轻量级进程LWP(内核线程的高级接口)进行线程映射,每个轻量级进程对应一个内核线程,这种1:1的关系成为一对一的线程模型,线程的所有操作需要进行系统调用,需要用户态和内核态进行切换,轻量级进程需要消耗一定的内核资源,一个系统支持的轻量级进程数量是有限的

  • 使用用户线程实现:系统内核不能感知用户线程的存在,其建立、同步、销毁和调度全在用户态完成,不需要内核的帮助,这种进程与用户线程的1:N的关系成为一对多的线程模型,难以实现,逐步放弃

  • 使用用户线程+LWP:以上两种结合推理

  • Java实现:Windows与Linux使用一对一线程模型实现,Windows与Linux提供的线程模型就是一对一的

1.3.2 线程调度

  • 协同式调度(协程):线程主动通知系统切换线程,如Lua的协程,缺点执行时间不可控制,某个线程可能一直不通知
  • 抢占式调度:每个线程由系统来分配执行时间,Java采用,Java提供了10中线程的优先级来代表次序,但是Java的优先级不一定靠谱,有的OS提供的线程优先级不一致,如windows提供7种,无法与之一一对应,此外优先级可能被OS的各种优化与功能改变

1.3.3 状态转换

参见

2 线程安全与锁优化

2.1 线程安全

  • 多线程访问一个对象,不用考虑线程的调度与交替运行,也不需要进行额外的同步,调用这个对象的行为产生正确的结果

    2.1.1 Java的线程安全

  • Java的安全性:
    • 不可变
    • 绝对线程安全
    • 相对线程安全(单独的操作是线程安全的,但是对于特定的顺序连续调用,需要额外的同步手段,如Vector的读取与删除同时进行)
    • 线程兼容(对象不是线程安全,可以使用同步手段达到线程安全)
    • 线程对立(无法在多线程环境运行,Threadsuspend()、Thread.resume()可能引发死锁)

2.1.2 Java的线程安全的实现方法

  • 互斥同步:synchronized关键字实现,字节码指令对应monitorentermonitorexit,这两个字节码需要一个reference类型参数,如果Java代码未指定,则为对象实例或Class对象,进入该指令时,首先要获取对象的锁,如果当前线程已经获取则将计数器+1,线程可重入,不会锁死自己,由于该语句会导致线程状态切换,线程需要进入核心态,消耗较大

    • 也可以使用ReentrantLock,性能更好, 但1.6之后synchronized关键字的吞吐量已经大幅提高,推荐使用原生synchronized关键字(1.8 ConcurrentHashMap源码已经使用synchronized来实现,Doug lea已经认可了synchronized的性能)
  • 非阻塞同步:CAS指令,需要硬件指令集支持(比较和更新需要原子操作),需要三个参数,内存位置,预期值,新值,Java中使用Unsafe类提供该操作

    • CAS指令会有ABA问题,可以使用AtomicStampedReference来控制变量的版本,更推荐使用互斥同步解决,因为一般ABA不影响并发的正确性。
    • 该类只支持Bootstrap ClassLoader加载,用户程序默认无法使用,只可以通过反射:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      /**
      * 一个简单的示例
      * Date: 2017/6/13
      * Time: 17:04
      */
      public class CasSample {
      public volatile long casVar;
      private static long CASVAR_OFFSET;
      static Unsafe unsafe;
      static {
      try {
      Field field = Unsafe.class.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      unsafe = (Unsafe) field.get(null);
      Class<?> k = CasSample.class;
      //取偏移量 实例变量使用objectFieldOffset,类静态变量使用staticFieldOffset
      CASVAR_OFFSET = unsafe.objectFieldOffset(k.getField("casVar"));
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      public static void main(String[] args) {
      CasSample casSample = new CasSample();
      casSample.casVar = 1;
      casSample.CAS_Swap();
      }
      private void CAS_Swap() {
      unsafe.compareAndSwapInt(this, CASVAR_OFFSET, 1, 2);
      System.out.println("cas" + casVar);
      }
      }

2.2 锁优化

  • JVM层面的优化

    2.2.1 自旋锁与自适应锁

  • 自旋锁:互斥同步线程阻塞不先不进入内核态挂起,而是让线程执行一个自旋,期待循环之后别的线程就可以释放锁了,自旋占用CPU时间,不可无限制自旋,自旋次数默认10
  • 自适应锁:自旋的时间不固定了,由前一次在同一个锁上的自旋时间以及锁拥有着的状态决定,比如上次自旋成功获得了锁A,当前持有锁的线程正在运行,虚拟机判断本次自旋获取到锁的可能性很大,进而自旋100次

2.2.2 锁消除

  • 自动判断当前代码块内是否会出现共享数据竞争,如果没有竞争,则将该段的同步措施去除。

2.2.3 锁粗化

  • 锁的使用通常要求锁的范围越小越好,但是若一系列的操作对同一个对象频繁的加锁解锁,比如出现在循环体内,那么这种加锁导致不必要的性能损耗,虚拟机探测到后会将同步措施放在整个操作序列的外部。

2.2.4 轻量级锁

  • 假设是对于绝大部分锁,整个同步周期内不存在竞争,根据对象头中的信息来表示对象的锁信息,参考

  • 如果对象没有被锁定,即锁标志位为01,则JVM在栈帧中创建一个锁记录的空间(Lock Record),存储当前对象的Mark Word备份,CAS更新对象的Mark Word为Lock Record的指针,若成功,则表示该线程获得了锁,且锁标志位变为00,此时对象处于轻量级锁定状态、

  • 如果上述CAS失败,JVM检查该对象的Mark Word是否指向当前线程的栈帧,如果是则表示该线程已经获得锁,直接进入同步块执行,若失败,则锁膨胀为重量级锁,锁标志位变为10,Mark Word中存储重量级锁的指针(互斥量),后续等待线程进入阻塞状态

  • 解锁依然使用CAS,若Mark Word依然指向线程的锁记录,则CAS替换回来那份拷贝,成功则完毕,失败则表示锁定期间有其他线程尝试获取改锁(此时Mark Word指向的是互斥量),释放锁同时唤醒被挂起的线程

2.2.5 偏向锁

  • 据统计,在实际情况下,大部分的锁对象永远只被一个线程占用,轻量级锁在每次monitorenter和monitorexit的时候(非重入),都会进行一次cas操作,为了进一步减少cas操作,偏向锁(biased locking)诞生了。是否启用偏向锁由Mark Word中的一位表示,位于锁标记位前,可偏向为1
  • 锁对象第一次被线程获取,CAS将线程ID设置在Mark Word中,成功则该线程进入同步块时,不再进行任何同步操作
  • 另外一个线程获取锁时:
    1. 看一眼持有锁的线程是否还活着,如果已经死了,那将对象设置为无锁状态就可以了
    2. 如果这个线程还活着,那需要遍历持有这个锁的线程的栈帧中的所有lock record,如果所有lock record都不指向锁对象,那么这个线程实际上不持有这个对象锁。同1,将对象设置为无锁状态即可
    3. 如果这个线程正在占用这个锁对象,那么需要修改线程中的锁记录与对象头,将它们都修改为轻量级锁状态,然后正常走轻量级锁的流程即可,是否偏向锁标记位为0
文章目录
  1. 内存模型与并发
    1. 1 内存模型
      1. 1.1 硬件层面的缓存一致性
      2. 1.2 Java内存模型
        1. 1.2.1 Java内存模型概述
        2. 1.2.2 volatile变量
        3. 1.2.3 long与double
        4. 1.2.4 原子性、可见性与有序性
        5. 1.2.5 先行发生原则
      3. 1.3 Java与线程
        1. 1.3.1 线程的实现
        2. 1.3.2 线程调度
        3. 1.3.3 状态转换
    2. 2 线程安全与锁优化
      1. 2.1 线程安全
        1. 2.1.1 Java的线程安全
        2. 2.1.2 Java的线程安全的实现方法
      2. 2.2 锁优化
        1. 2.2.1 自旋锁与自适应锁
        2. 2.2.2 锁消除
        3. 2.2.3 锁粗化
        4. 2.2.4 轻量级锁
        5. 2.2.5 偏向锁