java并发编程

《java并发编程的艺术》读书笔记

Posted by Gong on December 11, 2018

创建时间:2019/1/11 20:31

更新时间:2019/2/24 19:35

第一章:并发编程的挑战

  1. 上下文切换

    1. 单核处理器也能支持多线程执行代码,CPU通过给每个线程分配cpu时间片实现。ms级别;cpu通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。一次上下文切换:任务的保存到再加载。

    2. 减少上下文切换的方法:

      1. 无锁并发编程:如将数据ID按照hash算法取模分段
      2. CAS算法:Atomic包使用cas算法跟新数据
      3. 使用最少线程
      4. 使用协程:单个线程里实现多任务调度,并在单线程里维持多个任务间的切换
  2. 死锁避免

    1. 避免同一个线程同时获取多个锁
    2. 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    3. 尝试使用定时锁
    4. 数据库加锁解锁在同一个数据库连接中进行

第二章:java并发机制的底层实现

  1. volatile

    1. volatile是轻量级的synchronized,保障了共享变量的可见性;使用成本较低,不会引起线程上下文切换和调度

    2. volatile实现原理:对声明了volatile的变量进行写操作,jvm会向处理器发送Lock前缀的指令,将这个变量所在缓存行的数据回写到系统内存。两条实现原则:

      1. Lock前置的指令会引起处理器缓存 回写到内存;多处理器下,通过缓存一致性协议,保证所有处理器缓存的值一致。“缓存锁定”
      2. 一个处理器缓存回写到内存会导致其他处理器的缓存无效;MESI控制协议维护处理器内部缓存和其他处理器缓存的一致性;多处理器下,处理器嗅探技术保证他的内部缓存、系统内存、其他处理器缓存的数据在总线上保持一致。
    3. 优化:追加64字节提高效率。L1,L2,L3高速缓存不支持部分填充缓存行;追加64字节填满高速缓冲区的缓存行,避免头结点和为节点加载到同一个缓存行,使得头结点为节点在修改时不会相互锁定。
  2. synchronized

    1. jvm中synchronized原理:jvm基于进入和退出Monitor对象来实现方法同步和代码块同步;

    2. java对象头

      1. synchronized使用的锁存在java对象头中,对象头长度:

        • 数组类型对象:3个字宽存储对象头;(32位虚拟机中:1字宽=4字节=32bit);mark word+类型指针+数组长度
        • 非数组类型对象:2字宽存储;mark word+类型指针
      2. mark word:

        • hashCode+分代年龄+锁标记位;
        • markword随锁标记位变化:无锁状态(0 01),轻量级锁(00),重量级锁(10),GC标记(11),偏向锁(1 01)
    3. 锁的四种状态(级别低到高):无锁状态(0 01),偏向锁(1 01),轻量级锁(00),重量级锁(10)。随着竞争升级,不能降级。
      1. 偏向锁
      2. 轻量级锁
    4. synchronized 锁优化:

      1. 锁自旋

        1. 原因:互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。
        2. 自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间(循环CAS),如果在这段时间内能获得锁,就可以避免进入阻塞状态。
        3. 自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
        4. 在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
      2. 锁消除:

        1. 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
        2. 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
      3. 锁粗化

        1. 如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
        2. 如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。这样只需要加锁一次就可以了
      4. 偏向锁:多数情况,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得;

        1. 当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中;
        2. 如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作(cas操作),只需要测试对象头的markword中是否存储指向当前线程的偏向锁。
        3. 偏向锁的撤销:等到竞争出现才释放锁;此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
      5. 轻量级锁:相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。

        1. 加锁:

          • 线程在执行同步块之前,jvm在当前线程的栈帧中创建用于存储锁记录的空间,并将markword复制到锁记录,称为【Displaced Mark Word】;
          • 然后线程尝试使用cas将对象头中的markword替换为指向锁记录的指针;如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态;
          • 替换失败,则表示其他线程竞争锁,尝试使用自旋来获取锁;
        2. 解锁:使用cas将Displaced Mark Word替换到对象头,失败则表示存在竞争,锁膨胀为重量级锁。为了避免无用的自旋,锁不能降级。锁处于重量级锁情况下,其它线程获取锁都会被阻塞,当持有锁的线程释放锁后才会唤醒这些线程。
  3. 原子操作的实现原理

    1. 32位处理器实现多处理器之间原子操作

      1. 对总线加锁:使用处理器提供的一个lock信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,name该处理器可以独占共享内存。(开销较大,因为其他处理器不能操作其他内存地址的数据)

      2. 对缓存加锁:内存区域如果频繁使用可以缓存到L1-L3高速缓存中。“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在lock操作期间被锁定,那么当他执行锁操作回写到内存时,处理器不在总线上声言Lock#信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性。

        • 两种情况不使用缓存锁定:
        • 当操作的数据不能被缓存在处理器内,或者操作的数据跨越多个缓存行;处理器会调用总线缓存;
        • 不支持缓存锁定的处理器
    2. java实现原子操作

      1. 锁:jvm内部实现了多种锁:偏向锁,轻量级锁,互斥锁;其中除了偏向锁,都是使用了循环CAS实现的锁;一个线程进入同步块,使用CAS获取锁;退出同步块,使用CAS释放锁

      2. 循环CAS

        1. 自旋CAS基本思路就是循环进行CAS操作指导成功为止;

        2. CAS实现原子操作的三大问题:

          1. ABA问题——使用版本号解决:变量前加上版本号
          2. 循环时间长,开销大
          3. 只能保证一个共享变量的原子操作;——可以把多个变量放到一个对象中,通过AtomicReference类保证引用对象的原子性,进行CAS操作;