Hunter的博客

读《深入理解Java虚拟机》-高效并发

读书笔记

java内存模型与线程

  • 硬件的效率与一致性

    处理器为了更快速的读写内存,引入了高速缓存;为了缓存一致性问题,各处理器访问缓存时需要遵循一些协议(MSI、MESI、MOSI、Synapse、Firefly、Dragon Protocol)

    内存模型:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
    处理器、高速缓存、主内存间的交互关系

    乱序执行优化:程序语句的计算先后顺序与输入代码的顺序可能不一致,但是结果是一致的。这样做是为了使得处理效率更高

  • Java内存模型

    JMM:(Java Memory Model)来屏蔽各种硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
    线程、主内存、工作内存的交互关系
    1、线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量
    2、线程之间不能互相访问各自的工作内存,需要通过主内存来完成变量值的传递

    内存间交互操作:关于主内存和工作内存之间具体的交互协议,JMM定义了8种操作(lock、unlock、read、load、use、assign、store、write)来完成,每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,有例外,可以不用考虑例外)

    volatile关键字:

    1. 具备可见性和一致性,但是基于volatile变量的运算在并发下可能存在不安全的问题,仍然需要通过加锁(synchronized或java.util.concurrent中的原子类)来保证原子性。

    2. 禁止指令重排序优化,JVM默认会对指令进行重排序优化,但是指令重排序优化会对程序的并发执行造成干扰,使用volatile可以禁止指令重排序优化

    3. 与其他并发工具的比较:

      • 在某些情况下,volatile的同步机制的性能要优于锁(synchronized或java.util.concurrent包里面的锁),但是由于虚拟机对锁实现了许多的消除和优化,使得很难量化的认为volatile就会比synchronized快多少。

      • volatile变量的读操作的性能消耗与普通变量几乎没有什么差别,但是写操作可能会慢些,因为需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

      • volatile与锁之间选择的唯一依据是volatile的语义能否满足使用场景的需求

    原子性
    1、由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基础数据类型的访问读写是具备原子性的(long和double的例外可以不用考虑)
    2、应用场景需要更大范围的原子性保证,可以使用synchronized关键字。

    可见性
    1、一个线程修改了共享变量的值,其他线程能立即得知这个修改。
    2、volatile、synchronized和final都可以保证共享变量的可见性

    有序性
    1、volatile、synchronized来保证线程之间操作的有序性

    先行发生原则

  • Java与线程

    java线程调度:协同式线程调度、抢占式线程调度(Java线程默认)

    状态转换

    1. 新建(New):创建后未启动的线程

    2. 运行(Runable):线程正在运行或可能正在等待CPU为它分配执行时间

    3. 无限期等待(Waiting):不会被分配CPU执行时间,要等待被其他线程显式唤醒,以下方法会让线程处于无限期的等待状态:

      • 没有设置Timeout参数的Object.wait()方法。
      • 没有设置Timeout参数的Thread.join()方法
      • LockSupport.park()方法
    4. 限期等待(Timed Waiting):不会被分配CPU执行时间,不需要等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒,以下方法会让线程处于限期的等待状态:

      • Thread.sleep()方法
      • 设置了Timeout参数的Object.wait()方法
      • 设置了Timeout参数的Thread.join()方法
      • LockSupport.parkNanos()方法
      • LockSupport.parkUntil()方法
    5. 阻塞(Blocked):线程被阻塞了,在等待获取一个排它锁。例如线程A和B在执行同步方法C时,线程A先拿到排它锁,那么线程B的状态就是阻塞状态,等待线程B释放排它锁

    6. 结束(Terminated):线程执行完毕

      线程状态转换

线程安全与锁优化

  • 线程安全

    • Java语言中的线程安全:

      不可变:final定义

      绝对线程安全:例如Vector是一个线程安全的容器,他的add()、get()和size()方法都被synchronized修饰,但是在多线程环境下(两个线程同时分别调用add、get方法),如果不在方法调用端做额外的同步措施的话,可以说使用还是会存在线程不安全。

      相对线程安全:大部分线程安全类都属于这种类型,例如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合

      线程兼容:指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说一个类不是线程安全,绝大多数指这种情况

      线程对立:指无论调用端是否采取同步措施,都无法在多线程环境中并发使用的代码

    • 线程安全实现的方法:

      1. 互斥同步:临界区、互斥量和信号量都是主要的互斥实现方法;互斥是因,同步是果;互斥是方法,同步是目的;
        synchronized:java中实现互斥同步的重要手段;是一个重量级的操作,会将用户态转换到核心态中,这种转换很耗费处理器时间。有可能状态转换消耗的时间比用户代码执行的时间还要长。虚拟机本身也进行了一些优化,例如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁的切入到核心态之中。

        ReentrantLock:同synchronized功能类似,相比前者,ReentrantLock增加了一些高级功能:等待可中断、可实现公平锁、锁可以绑定多个条件

      2. 非阻塞同步:基于冲突检测的乐观并发策略-CAS
        java.util.concurrent包里面的整数原子类,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。

  • 锁优化

    • 自旋锁与自适应自旋:避免用户态到内核态的切换

    • 锁消除:对代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除

    • 锁粗化:锁同步范围扩展。例如:如果在循环里对同一个对象加锁,虚拟机会默认将锁同步的范围扩展(粗化)至循环体外部,这样就只需要加锁一次。

    • 轻量级锁:在无竞争的情况下使用CAS操作去消除同步使用的互斥量

    • 偏向锁:在无竞争的情况下把整个同步都消除掉