1. 锁

1.1. synchronized

  • 原理
    • 同步代码块:在进入同步代码块的时候,插入一条moniterenter指令,在退出的时候插入一条moniterexit指令,并且会加上一个默认的异常处理,在异常处理中加入额外的moniterexit来保证一定会释放锁资源
    • 成员方法或者静态方法的时候是利用权限修饰符ACC_SYNCHRONIZE来判定这个方法是否需要进行同步
    • 每个synchronized加锁的的锁对象(根据synchronized锁的位置不同分为类实例对象,和类的Class对象)会关联一个Mointor对象,在Monitor对象会有两个线程队列,一个是EntryList一个是WaitSet,EntryList是blocking状态的线程在此排队等候,WaitSet是waiting状态的线程在此排队等候,一个线程必须从waiting状态转为blocking状态后才有可能继续被CPU调度执行。所以当进入synchronized同步代码块失败的线程会进入EntryList中等待锁,是blocking状态的,当抢到锁后等待CPU调度执行,进入running状态;当调用了锁对象的类wait方法的时候,会释放锁同时进入WaitSet中等待唤醒,当被唤醒后,会进入blocking队列也就是从WaiSet队列转移至EntryList队列,等待抢锁然后被CPU调度执行
  • 锁升级
    • 无锁

对象没有没锁定。对象头的MarkWord的锁标志为是01,是否偏向为0

  • 偏向锁
    • 当只有一个线程访问的时候,会将线程的threadId更新到MarkWord中,此时锁标志位是01,是否偏向为1。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等到全局安全点,JVM首先会暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态
    • 过程
      1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
      2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
      3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
      4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
      5. 执行同步代码。
  • 轻量级锁
    • 如果在持有偏向锁的线程运行过程中,遇到了第二个线程来抢占锁,它会先尝试用CAS操作去更新抢锁,如果第一个线程已经退出了同步块的处理,就会释放掉锁,然后第二个线程就会抢锁成功。如果还在同步块中,那么当进入全局安全点的时候,JVM会将锁恢复到轻量级锁。此时的锁标志位是00。
    • JVM中的操作:此时JVM会在线程的栈帧中开辟一个名为Lock Record的空间,用来存储对象MarkWord的拷贝,这个拷贝被称为Displaced MarkWord,然后JVM利用CAS操作尝试把对象的MarkWord更新为指向LockRecord的指针。
      • 如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。
      • 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
  • 重量级锁
    • 在进入轻量级锁后,第二个线程会以自旋的方式尝试抢锁,如果在此期间抢锁成功,则锁标志位依旧是“00”,如果自旋结束都没有获取到锁或者有第三个线程过来抢锁,那么锁就会升级为重量级锁,持有锁的线程继续执行,没有获得锁的线程会被放入等待队列。对应的锁标志位是10
  • 适应性自旋锁
    • 指的是在上面的自旋过程中,如果该线程获取到了锁,那么JVM就会认为这个锁比较好获取,那么就会在后续的抢锁过程中适当增加自旋次数,以减少线程被操作系统挂起的几率。如果多次自旋都没有获取到锁,那么JVM就会认为这个锁通过自旋获取的几率很小或者几乎不可能,那么在后续的抢锁过程就就会响应减少甚至取消自旋过程,减少对CPU资源的浪费。
      • 总结
  • 偏向锁是基于 “大多数情况下,只会有一个线程访问同步资源”这种统计来设计的
  • 轻量级锁是基于“大多数情况下,线程持有锁的时间都不会太长”这种统计来设计的
  • 偏向锁相较于轻量级锁的CAS操作只有设置threadId的时候才会用到,而轻量级锁在更新MarkWord,进入/退出同步区等地方都会用到

    1.2. volatile

  • 作用:保证可见性和禁止指令重排序
  • 原理:可见性是依赖CPU的缓存一致性协议来解决的,在Intel系,是以MESI协议来处理的;禁止指令重排序是通过内存屏障来实现的

    • MESI缓存一致性协议
      • 解释:
        • M: modify,表示当前缓存行已被某个CPU修改
        • E: exclusive,表示当前缓存行被某个CPU独占访问
        • S: shared,表示当前缓存行被多个CPU共享访问
        • I: invalid,表示当前缓存行已被其他CPU修改,获取到的数据已失效
      • 问题:伪共享
        • 说明:根据“程序的局部性原理”,一般在对数据进行缓存的时候会顺带将某个数据的相邻数据一起缓存到高速缓存的中,而高速缓存的存储单位是缓存行,一个缓存行一般是64个字节。这样当不同的CPU对同一个缓存行中的不同数据块进行各自的修改时,因为缓存一致性协议,这样会导致这些个CPU互相之间设定该缓存行的数据标识为不可用,需要频繁读取该缓存行数据,这样就会降低效率
        • 解决:解决伪共享可以通过填充缓存行等手段,例如Disruptor,JDK8,加入了@Contended注解,需要加上:-XX:-RestrictContended JVM参数
    • 内存屏障
      • sFense:写屏障。在此屏障前后两侧的写操作不能重排序
      • lFense:读屏障。在此屏障前后两侧的读操作不能重排序
      • mFense:混合读写屏障。在此屏障前后两侧的读写操作不能重排序

        1.3. CAS 乐观锁/无锁

  • 作用:不利用传统的系统级锁来对某个变量进行原子操作

  • 原理:CPU的原语CAS
  • 问题
    • ABA问题
      • 解释:目标地址(Java对象)的内容被多次修改以后, 虽然对象未曾改变,但是里面的内容已经被修改过了。
      • 解决:加版本号。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
    • 只能保证一个共享变量的原子操作
      • 从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作
    • 循环时间长开销大
      • 自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。指定自旋次数,但是目前JDK提供的CAS原子操作类中貌似没有这个功能
    • 总线风暴带来的本地延迟
      • 在多处理架构中,所有处理器会共享一条总线,靠此总线连接主存,每个处理器核心都有自己的高速缓存,各核相对于 BUS 对称分布,这种结构称为“对称多处理器”即 SMP。当主存中的数据同时存在于多个处理器高速缓存的时候,某一个处理器的高速缓存中相应的数据更新之后,会通过总线使其它处理器的高速缓存中相应的数据失效,从而使其重新通过总线从主存中加载最新的数据,大家通过总线的来回通信称为“Cache 一致性流量”,因为总线被设计为固定的“通信能力”,如果 Cache 一致性流量过大,总线将成为瓶颈。而 CAS 恰好会导致 Cache 一致性流量,如果有很多线程都共享同一个对象,当某个核心 CAS 成功时必然会引起总线风暴,这就是所谓的本地延迟。而偏向锁就是为了消除 CAS,降低 Cache 一致性流量。
  • 基于CAS的新型锁

    • 原理:AQS
    • 种类

      • ReentrantLock
        • 可以替代synchronized
        • 公平锁
        • 非公平锁
        • 公平锁与非公平锁的区别
          • 公平锁先获取一下当前的state
          • 非公平锁上来直接cas设置state,如果失败采取获取当前的state
      • 读写锁
        • ReadWriteLock
          • 读写分开,写锁排他,读锁共享,有读锁的时候不能写。如果读太多,可能导致写饿死的情况
        • StampedLock
          • 读写分开,认为读不应该阻塞写,而是应该去重读。如果写太多,可能导致读会饿死的情况
      • 分段锁
        • ConcurrentHashMap
        • LongAdder
          • 相较于AtomicLong,将一个CAS操作分为多个以cell为单位的CAS操作,减少多线程CAS操作失败的问题,提高了并发。还使用了缓存行对齐,解决伪共享问题
        • LongAccumulator
      • 写时复制
        • CopyOnWriteArrayList
        • CopyOnWriteArraySet
      • 原子操作
        • Atomic*

          2. ThreadLocal

    • 应用

    • Java的四中引用类型
      • 强引用
        • Object o = new Object(); 虚拟机在任何时候都不会回收强引用
      • 软引用
        • 当内存空间不足,进行垃圾回收的时候,就会回收软引用
      • 弱引用
        • 只要进行垃圾回收就会回收弱引用
      • 虚引用
        • 只要进行垃圾回收的时候就会回收

          3. 同步容器

          4. 线程池

          4.1. ThreadPoolExecutor

          4.2. ForkJoinPool