java并发编程-JUC-一文看完

img

image

线程

基础

  1. 线程、进程:
    • 线程:是操作系统能够进行运算调度最小单位,其仅仅需要少量独立资源和本进程的其他线程共同占有进程的资源;所以其具有:并发性、可共享进程资源、切换代价小、几乎不独立占用系统资源的特点;
    • 进程:进程是资源分配的基本单位,是具有一定独立功能的程序关于某个数据集合的一次运行活动;进程具有四个特征:动态独立异步和并发
  2. 协程和线程:
    • 相比于前面的进程和线程,协程是一种用户态的轻量级线程协程的调度由用户控制,拥有自己独立的寄存器上下文和栈;
  3. 进程和线程的区别:
    • image-20211203160818743

Thread类和Runnable接口

Thread类

重点看一下Thread的核心变量:

	/**
	线程id
	线程名
	优先级
	线程组
	是否为守护线程
	这种运行的run方法的实例
	线程的栈大小
	线程的ThreadLocals
	线程的InheritableThreadLocals提供从父线程到子线程的值继承:当创建子线程时,子线程接收父线程具有值的所有可继承线程局部变量的初始值。 
	*/
    private long tid;
    private volatile String name;
    private int priority;
    private ThreadGroup group;
    private boolean     daemon = false;
    private Runnable target;
    private long stackSize;
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
	//……后面的还没用到跳过

构造器

重点关注第一个:设置线程组、真正运行的线程、线程名、线程的栈大小(0标识忽略)

public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }

状态和状态转移

线程的基本状态:初始阻塞运行【包括活动和就绪状态】、等待超时等待【利用加定时器的等待】、终止

image-20211203161250337
  • 初始状态:实例化出来的实现了Runnable接口或者继承了Thread类的重写了run方法的类被实例化后,实例化的线程就进入初始状态
  • 运行状态初始状态调用start方法或者等待状态被唤醒超时等待状态到时就进入RUNNABLE状态,进入后其可能在活动状态也可能就绪状态;在运行状态中,如果希望线程让出执行可以通过yield
  • 等待状态:通过Object.wait方法、Thread.jion和LockSupport.park都会使其进入等待状态,其和超时等待的最大区别是只能等待其他线程对他进行唤醒,本线程不会自动苏醒;
  • 超时等待:到达指定等待时间或者其他线程唤醒都可以苏醒;
  • 阻塞状态:这个状态和synchronized密切相关,线程必须能够进入该区域代码段才会唤醒或者说只有获取到执行该代码权力才会唤醒;
  • 终止状态:当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。

状态转移方法

  • start:就是将一个线程对象放入线程组调用其run方法多次启动一个线程是不合法的。 特别是,线程一旦完成执行就可能不会重新启动。
  • yeild:该转移是在RUNNABLE状态内部,向调度程序提示当前线程愿意放弃其当前对处理器的使用,如果被处理则然后进入就绪状态,当时其是否让出成功是不一定的,让出后其还可能再次被调用;
  • jion:当前线程A等待thread线程终止之后才从thread.join()返回
  • sleep:仅仅是让线程休眠进入(超时)等待状态,既不会释放锁也不需要在synchronized代码段中使用;
  • wait、notify、notifyAll【着三个方法本质就是在montitor内部进行状态转移的】:这三个方法都是仅仅只能在synchronized中使用,wait方法会释放对象的“锁标志”,同样;notify唤醒在此对象的监视器上等待的单个线程。 如果有任何线程正在等待这个对象,则选择其中一个线程被唤醒。 选择是任意的,并由实现自行决定。notifyAll则是唤醒全部在此对象监视器上等待的线程;这三个的具体参考synchronized的实现;

线程间通讯

  • 共享内存:主要是通过volatile
  • 管道方式:就是一个输入流一个输出流,可以认为是一个线程给另一个线程发送消息;其实现还是通过共享内存
    • image-20211203173715775
  • 同步方式:synchronized

java线程模型JMM

硬件线程模型

在硬件系统中,由于CPU和内存、cache速度严重不匹配所以设置了多级存储的存储系统,按照存储系统每个CPU可能又一个或者多个核,每个核有各自的个L1、L2,所有核共用L3,L3缓存主内存中的数据所以模型如下【cache的缓存数据就是缓存行】:

image-20211204092915556

为了实现缓存一致性,所以设置了缓存一致性协议,缓存一致性协议规定:

  • 缓存行有四种状态:I、S、M、E;其中I为无效状态、S为共享状态(即同时被一个以上的核缓存着),M为修改状态【即当前缓存行的数据核L3不一致了】,E就是独享状态(即当前只有该核拥有该缓存行)
  • 四种操作LR(Local Read)、LW(Local Write)、RR(Romte Read)、RW(Romte Write);对应本核的读写核非本核的读写
  • 在I状态下不需要处理非本核的读写、对于本核的读写均需要先检测是否其他核是否有该缓存行,**对于LR:如果没有直接在L3读出标记为E状态,如果有且且其他核标记状态为S或者E,直接在L3读出,并标记为S;如果其他核缓存行状态为M则需要让其写回L3再从L3读出再标记为S; ** 对于LW;其会先进行读出【就是先进行LR】、再修改并改为M状态;其他则比较简单,重点是其他会监听RW事件,一旦有缓存行发生RW立刻失效,RR事件则比较简单只需看是否为M,为M先写回,不是的话就直接修改为S;
  • image-20211204094743559

MESI协议解决了缓存一致性问题,但是频繁的请求与响应,会产生大量的等待时间,请求等待响应的返回之后才能将数据写入高速缓存中,为了避免减少这种性能问题,硬件层面引入了写缓存(store/write buffer)和无效化队列(invalidate queue)

  • 写缓冲器使得处理器在执行写操作的时候,写入写缓冲器中,而不需要等待response响应,来减少写操作的延时,在节省的时间内可以执行更多其它指令,从而提高处理器的执行效率。
  • 无效队列处理器在收到Invalidate消息之后,并不立马删除地址中对应的副本数据(其实是更新缓存行的状态为无效),而是将消息存入无效化队列之后就直接响应Invalidate Response消息了,从而减少了写操作执行处理器的等待时间。
image-20211204095748130

由于加了俩个可以提升CPU执行效率的中间缓存,这就导致会使得CPU对于上面说的缓存一致性协议的执行后会滞后,就是其还没进入L1缓存行自然不存在缓存一致性协议不满足的问题但是其实际确实不满足:

  • 写缓冲器导致StoreLoad重排:Store1; StoreLoad; Load2;StoreLoad Barriers可以避免Load2发生在前面;(下面同理)
  • 写缓冲器导致StoreStore重排
  • 无效化队列导致LoadLoad重排
image-20211204100519897

JMM

JMM模型

image-20211203175421918

Java内存模型的主要目的是定义程序中各种变量的访问规则,这里所说的变量是指具有多线程共享能力的static字段、实例变量和数组元素等;【回顾jvm知识可以简单的认为所谓的具有共享能力的变量就是一个不是存放在虚拟机栈的局部变量表中的变量,或则说是在堆内存中的数据部分】;

规则如下:

  • 所有该类型变量存放在主内存中
  • 所有线程通过下面规定进行读写【相当于jvm中定义的缓存一致性协议】
    • jvm定义了8种原子操作进行维护主内存的变量lock、unlock、read、writeload、assign、store、use】;只有俩个特例就是long和double,显然他们是64位的,但是有的操作系统将他们的赋值设置为俩个32位这就导致可能他们不能符合这种要求【但是现在一般没这个问题】
      • lock和unlock:堆主内存种的变量进行加锁和解锁,一个线程可以多次加锁但是不能对非本线程加锁的变量加锁,多少次加锁对应多少次解锁,同样只能解本线程加的锁
      • read和load:load就是read的一个随后接着的操作,read就是线程向主内存获取一个未加锁变量,然后通过load获得本线程对该变量的副本
      • write和store:store就是进行主内存write前的操作,通过store将本线程的值传入主内存、如果未加锁通过wirte将该值写入主内存
      • use和assign:use就是需要操作线程中的主内存获取到的变量的副本的时候将其值传给执行引擎;assign显然就是:将执行引擎返回的值赋值给线程中的主内存变量的副本的时候使用的操作
    • 不允许read和load、store和write操作之一单独出现【很显然】
    • 写回操作有以下约束:简单来说进行store和write前必须使用过assign使用过assign必须立刻写回、unlock前必须是已经完成写回操作
      • 变量在工作内存中改变了之后必须把该变化同步回主内存,就是执行了assign必须写回;
      • 写回操作必须发生了assign【就是线程只有修改了后才能重写写回内存】
      • unlock操作之前,必须先把此变量同步回主内存中
  • 所有线程不能直接操作主内存的变量,必须通过副本操作
  • 线程间不能访问对方持有的该变量的副本,只能通过主内存访问

happen-before

  • Java语言设置了happen-before规则规定了一些天然先行发生关系在不影响先行先发生规则正确性下,cpu可以进行指令重排,或者说先行先发生规则理论上只对于本线程有效,因为如果不影响先行先发生规则理论上代码执行顺序是未知的【例如存在一个类变量i=false,t=false;线程A是存在俩个赋值a为true和b为true然后再赋值回去而且是先让a为true再为false再让b为true再让b为false假设这段赋值不会影响本线程其他代码执行,线程b有一个代码块需要a、b都是true才能执行,这个时候该代码块是否执行时未知的
    • 传递性A操作先于B,B操作先于C,那么可以说A先于C发生
    • 符合程序次序规则在一个线程内按照控制流顺序书写在前面的操作逻辑上先行发生于书写在后面的操作,但是如果不违背逻辑执行结果,CPU和JVM允许指令重拍
    • 具有管程锁定规则一个lock操作先行发生于后面对同一个锁的unlock操作【这里的先于是指时间上的先于】
    • volatile变量规则volatile变量的写操作先行发生于后面对这个变量的读操作【同样是时间上的先于】
    • 线程控制规则start先发生、线程内部所有操作、先于线程终止检测;对于线程中断先于线程终止检测
    • 对象生命周期规则一个对象的初始化完成先行发生于它的finalize()方法的开始

并发的三个特性

  • 原子性jmm提供了6+2个原子操作,【read、load、use、assign、store、write】使得大致可以认为基本数据类型的访问、读、写都是具备原子性的java程序可有通过synchronized(利用lock和unlock)实现非基本数据类型的原子性
  • 有序性在本线程内观察,所有的操作都是有序的;换句话说对于影响本线程执行顺序的操作都会有效执行,但是不影响的操作可以进行指令重排;可以通过volatile和synchronized两个关键字来保证线程之间操作的有序性
  • 可见性:可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。volatile、synchronized和final都能保证可见性【final因为是常量当然可以】

指令重排:编译器或则CPU优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度;【主要是由于CPU速度和缓存速度严重不匹配导致的】

volatile、final和ThreadLocal

volatile

​ 前面我们知道volatile可以满足并发下的俩个特征:可见性和有序性;注意的可见性仅仅只是设置volatile的部分换句话说就是对于引用和数组都是不能保证其内部可见性的(就是说例如对于引用不能保证引用的内部字段属性改变后所有线程可见,仅仅只是改变引用的时候保证可见性)

  • 很明显,对于数组可见性往往指的是元素的可见性,但是由于Java语法糖没法设置数组元素为volatile,
    • 所以需要使用unsafe+内存偏移直接在内存中获取来保证读取最新数据;
    • 或者使用copyOnWrite的思想,使用数组引用的volatile机制,每次复制全量更新引用(参考CopyOnWriteList、CopyOnWriteSet)

可见性的实现:

  • 如果要执行use操作必须前一个操作时load操作,如果执行了load操作必然接着就是执行use操作;
  • 如果要执行assign操作后面必须是store操作,如果要执行store操作必须前一个操作是assign操作;
  • 上面的硬件实现参考可见性;
  • 简单来说就是use和loadstore和assign在一个线程中是连续执行的

有序性的实现:

  • 有序性是通过内存屏障实现的,这里的内存屏障是指volatile变量前的代码不能移动到改变了后执行、volatile后的代码不能移动到该变量前执行【就是说限制了CPU指令重排的范围】
  • 在每个volatile写操作前插入StoreStore【SS】屏障(这个屏障前后的2个Store指令不能交换顺序),在写操作后插入StoreLoad【SL】屏障(这个屏障前后的2个Store Load指令不能交换顺序);
  • 在每个volatile读操作前插入LoadLoad【LL】屏障(这个屏障前后的2个Load指令不能交换顺序),在读操作后插入LoadStore【LS】屏障(这个屏障前后的2个Load Store指令不能交换顺序);
  • 硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障:LL保证后续操作指令不能被移动到LL前;SS
  • img

fianl域

​ 我们知道final既可以修饰局部变量也可以修饰类变量、实例变量;在java中局部变量除非是基本数据类型,否则没有任何作用(急救和普通变量没有区别),如果是基本数据类型最大区别是编译器会优化为常量;重点是final修饰类变量或者实例变量;

  • 构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

ThreadLocal

https://www.cnblogs.com/shoshana-kong/p/13865685.html

​ 上面介绍了volatile是为了使线程将可见和有序;但是有的时候我们希望线程间是不可见的,这个时候局部变量然后传参显然是可行的,但是有时候我们不方便传参;例如:在AOP中,我们在记录所有用户线程在某一个核心方法中操作时间(这个时候就可以用ThreadLocal);例如:【在进入方法前调用begin,在AOP日志中调用end获取方法即可】

public class Profiler {
    // 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
        protected Long initialValue() {
        	return System.currentTimeMillis();
        }
    };
    public static final void begin() {
    	TIME_THREADLOCAL.set(System.currentTimeMillis());
    }
    //这里调用后认为该线程的在TIME_THREADLOCAL保存的数据已经失效可以清空防止内存泄漏
    public static final long end() {
    	long a = System.currentTimeMillis() - TIME_THREADLOCAL.get();
        TIME_THREADLOCAL.remove ();
        return a;
    }
}
  • 首先ThreadLocal只会保存基本数据类型和引用的副本,这点和volatile类似【就是如果修改引用的实例变量、类变量ThreadLocal是不会察觉的,但是如果修改引用,不会改变ThreadLocal set的引用,下面为例子每个线程都改变testThisT,但是各个线程ThreadLocal的testThis还是不变】

    • public class TestJuc {
          private testT testThisT  = new testT();
          @Test
          public void testBase() throws InterruptedException {
              ThreadLocal<TestJuc.testT> local = new ThreadLocal<>();
              local.set(testThisT);
              local.get().setI(2);
              Thread a = new Thread() {
                  @Override
                  @SneakyThrows
                  public void run() {
                      testThisT = new testT();
                       local.set(testThisT);
                       local.get().setI(3);
                      local.remove();
                  }
              };
              Thread thread = new Thread() {
                  @Override
                  @SneakyThrows
                  public void run() {
                      testThisT = new testT();
                      local.set(testThisT);
                      local.get().setI(4);
                  }
              };
              thread.start();
              a.start();
      
          }
      
          class testT{
              private volatile int i = -1;
              public void setI(int i) {
                  this.i = i;
              }
      
              public int getI() {
                  return i;
              }
          }
      }
      
  • 其常用方法就是get、set、remove;注意如果是线程池一定要每次归还前remove防止线程泄露,具体看前面例子;

  • 其实现是通过一个ThreadLocalMap保存的,这个map保存在Thread类中,所有ThreadLocal都是通过Thread获取这个的;这么说:一个Thread(线程)可以有多个ThreadLocal,一个ThreadLocal也可以属于多个Thread,ThreadLocalMap通过ThreadLocal获取value,通过Thread获取对应的ThreadLocalMap;【有一点点绕:简单来说就是每个Thread类有一个ThreadLocalMap,每个ThreadLocalMap的key就是ThreadLocal,通过每个ThreadLocal的threadLocalHashCode获取其桶,每个桶就是一个Entry,里面就是一个Object

    • image-20211207161029039
    • 为什么不用简单的替换ThreadLocal保存ThreadLocalMap、通过Thread作为key呢?主要是线程数动态变化很明显、而且线程个数多则ThreadLocal存储的KV数就变多;另外最重要的是线程销毁后ThreadLocalMap就会消耗,而如果使用这种方法显然线程销毁后甚至这个桶都不会被消耗;
    • image-20211207162540044
    • 注意其使用的是弱引用
  • ThreadLocal在数据传递和线程隔离有明显的优势;

  • ThreadLocal为什么推荐使用static:主要就是方便操作,因为往往都是在线程的各个位置操作又不传参,每次new一个ThreadLocal很浪费空间

  • 内存泄漏:内存泄漏是指已动态分配的堆内存由于某种原因程序未释放或无法释放造成系统内存的浪费

    • 首先内存泄漏和强弱引用没有任何关系,强引用会导致无法回收(因为引用链一致存在),弱引用可以回收但是没回收,需要通过remove移除来回收(弱引用的话key会被回收,就是Entry的key为null,但是Entry数组还又这个的Entry,存在强引用导致Entry不能回收,所以需要通过remove移除回收);

      • image-20211207165926101
      • image-20211207165952584
    • 为什么要用弱引用Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(threadLocal对象)和value;很明显前面都是强引用,如果Entry也是强引用,那么关键看东西分析,只要线程不死亡,永远是可达的,但是如果Enrty的key 是弱引用,WeakReference引用本身是强引用,它构造器传入的(T reference)才是真正的弱引用字段,所以下面的ThreadLocal就是弱引用字段(可以回收),而Entry又可以通过移除回收(当ThreadLocal是弱引用的话,回收后key就是null),就算不移除,我们下次调用set、get时由于该key为空还是可以移除;

    • static class Entry extends WeakReference<ThreadLocal<?>> {
                  /** The value associated with this ThreadLocal. */
                  Object value;
      
                  Entry(ThreadLocal<?> k, Object v) {
                      super(k);
                      value = v;
                  }
              }
      

synchronized

synchronized的锁机制

preview

基础

首先我们知道:synchronized锁定的范围是对象

  • 如果加锁的是实例方法的话锁定的是实例对象
  • 如果加锁的是静态方法的话那么锁的就是类变量
  • 如果加锁的是同步方法块中配置的对象显然锁的就是对应的对象

在jdk5、6对synchronized进行了大幅度的锁优化,不再是直接使用重量级锁,优化后的synchronized具有以下特征:

  • 首先其是一个非公平锁,换句话说可能会出现饿死;

  • 锁的状态可以分为:无锁、偏向锁、轻量级锁和重量级锁

  • 锁升级是不可逆【之所以无锁升级为偏向锁是可逆的是由于他们本质在一个锁状态中都是01】

  • 锁的升级主要依靠CAS和自旋;

  • 即时编译器可以进行锁消除、锁粗化和自适应自旋

    • 锁消除就是JIT的C2进行逃逸分析判断不存在和其他线程锁竞争进行取消锁的编译优化,之所以有这个功能主要是编译器编译代码期间可能会给我们的代码自动加上锁机制
    • 锁粗化:我们在进行编写代码的时候推荐锁细化,如果JIT发现在一个线程中一个锁被频繁的获取和释放,又很少有其他线程竞争;就会将锁的粒度加大;
    • 自适应自旋:是对于自旋次数的控制,其依据是平均自旋次数和上一次自旋获取到锁的次数,如果JIT认为可以通过自旋获取会让其自旋;

    在jvm中我们知道锁信息保存在对象头的Mark_Word中:下面是mark_Word信息在不同锁的转换

    image-20211204105045502

    image-20211204105120551

首先是:轻量级锁和重量级锁都会让获取到锁的栈帧保存锁Mark_Word信息,对象中的Mark_Word仅仅需要保存锁标志和指向恢复的信息即可;对于偏向锁,之所以能用对象的hashCode保存线程ID是因为默认对象的hashCode是不会变得,重写了hashCode得对象应该也要注意满足这个条件;

偏向锁

  • 偏向锁就是偏向第一个获取它的线程,换句话说偏向锁一旦遇到竞争会可能会升级为轻量锁【这里的可能会和epoch密切相关,epoch就是版本或者说冲突次数,如果epoch大于一定的值就会自动升级为轻量级锁,默认是20次】;
  • 偏向锁设计的初心是认为大部分情况下不存在锁竞争没必要直接加实际上的锁,【如果一般存在锁竞争可以通过-XX:-UseBiasedLocking=false 关闭;jdk1.6默认打开】;
  • 偏向锁机制启动会有延迟,之所以延迟主要是jvm启动会有大量的同步而且这些同步不适合使用偏向锁;
  • 一旦偏向锁机制启动,所有在这个时刻后创建的对象都是匿名偏向锁对象(之所以是匿名估计和markWord的hashCode没有被赋值有关)等待填入线程ID
  • 一旦中途使用Object::hashCode()或者System::identityHashCode(Object)方法,将会对偏向锁进行升级为重量锁(因为这个时候hashCode将被写入,不再匿名)
偏向锁的实现
  • 偏向锁通过使用对象头的markWord的hashCode保存指向本线程的ID
  • 只要是无锁状态或者本来就是指向本线程的偏向锁才可以获取偏向锁
  • 尝试获取偏向锁过程是通过CAS修改hashCode,写入本线程ID
  • 默认情况下hashCode为中的线程ID为NULL(即0)才可以加入偏向锁,偏向锁的hashCode如果被赋值将不能使用偏向锁,一般要将hashCode写入markWord偏向锁将立刻失效升级为重量锁;
偏向锁获取和撤销
  • 首先检查对象是否处于无锁(或偏向锁)状态,如果是则检查对象头是否指向本线程,是则获取到锁;
  • 否则就要尝试进行CAS替换对象头的线程ID(这里注意替换的时候CAS进行的检查是只有线程为NULL号即0才会替换成功);替换成功获得锁;
  • 否则jvm线程就会设置安全点,等待所有线程到安全点停止;然后jvm线程进行偏向锁的撤销,如果撤销成功,俩个线程重新开始竞争锁,一般而言如果撤销后俩个线程竞争锁会很快就进入轻量级锁
  • 偏向锁撤销的时候会将epoch加1;
  • 如果撤销后员持有偏向锁的线程还在同步代码中就是升级为轻量级锁
image-20211204112504270

轻量级锁

基础

​ 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争过度发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

  • 只有锁升级或者本身就是轻量级锁(锁状态为00)才会将使用轻量级锁
  • 其和偏向多的区别是需要jvm线程赋值markWord到线程的虚拟机栈中
  • 仍然使用的是CAS来修改markWord使其指向自己
  • 如果一个线程自旋次数超过jvm预期就会让该线程设置锁碰撞,原来持有的锁尽管还是轻量级锁但是后面的线程如果也要获取锁就会进入阻塞,持有锁线程释放的时候CAS也会失败,这时其会唤醒阻塞等待资源的对象;
  • 如果自旋期间成功获取锁,也就是持有锁的线程在预期内释放锁就会进行CAS将MarkWord替换回去
加锁和解锁过程
image-20211204152353174

重量级锁的实现

基础

  • 对于同步块的实现使用了monitorenter和monitorexit指令
  • 同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。
  • 无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器

​ 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态

monitor

所有需要同步对象(或者说锁对象都会由JVM自动创建一个ObjectMonitor对象保存锁信息实现monitor)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //monitor进入数
    _waiters      = 0,	//等待数
    _recursions   = 0;  //线程的重入次数
    _object       = NULL; //锁对象
    
    _owner        = NULL; //标识拥有该monitor的线程
    _WaitSet      = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁进入时的单项链表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

  • 三个链表:cxq同步栈、_EntryList就绪队列、waiter阻塞队列,都是由ObjectWaiter组成的链表结构;
    • ObjectWaiter就是包装的后的在monitor中的线程
    • cxq同步栈:当多个线程竞争时,在_cxq进行排队,并且cxq还是一个使用头插法先进后出的队列(此处体现了非公平锁之一,另外就是线程释放资源的时候,可以由指定算法直接在cxq或者_EntryList头部获取,如果是cxq获取同样不公平)JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此 _cxq是一个后进先出的stack(栈
    • _EntryList就绪队列:就是可以获取临界资源的线程,先进先出;
    • waiter阻塞队列:显然就是存放使用waiter主动释放共享资源,线程阻塞的进行队列
  • 可重入通过 _count计算本线程重入的次数
  • 独占_owner保持独占线程的线程对象;
  • 重量级的体现:主要就是通过使用内核函数(需要由用户态切换到系统态)进行锁的权利的获取和释放(Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒;)

线程的状态转换

image-20211204155917657

锁的使用场景

image-20211204153539256

显示加锁Lock

理论基础

前面学习隐式锁:synchronized,尽管其使用了锁升级机制加快了并发的效率,而且隐式使用monitor降低了锁的使用难度,但是由于是隐式锁所以其非常不灵活,体现在编程上有:

  • 首先是monitor
    • 加锁对象是对象的ObjectMonitor对象,换句话说,就是加锁和解锁都在一个对象中不能跨对象加锁解锁;
    • monitor不支持共享锁;
    • 同样不支持对于指定阻塞线程的唤醒,要么唤醒全部要么随机唤醒一个;
    • 使用的是非公平的算法,可能或出现线程饿死;
  • 然后就是由于其队列cxq、waiterset、EntryList
    • 都是不可中断的,除了占用资源的线程可以中断自己外;
    • 只有一个阻塞队列,不能分类阻塞和唤醒(生产者消费者问题难以实现)
  • 不能超时等待不能非阻塞获取锁,换句话说就是:一旦到达同步代码块将会使得本线程将会阻塞到直到获取到锁为止

​ 显然隐式锁具有很多的问题,比如非公平、而且隐式锁很难实现复杂的加锁和控制线程协同(管程)比如让A线程先进行在到B线程或C线程,A线程只有B执行了且C未执行该同步代码块时才会执行否则要等待C执行到X同步块才能执行】,如果线程不运行【例如IO】却占用锁等等;简单来说就是隐式加锁尽管比较简单但是不利于线程间的通讯;这时就需要显示加锁;

针对前面的问题就有了显示加锁的需求前提:

  • 首先需要解决monitor的各种不足:跨对象、指定特定线程、可选公平调度、可共享
  • 然后就是需要对于同步队列进行改进:可选允许外部中断阻塞线程,可以自定义阻塞队列的数目和阻塞条件
  • 然后就是针对非阻塞获取锁、超时等待的需求

设计思想:按照java程序设计的学习思路,首先就是拆分功能:

  • 提供给程序员直接使用的外部API

    • Lock:具体就是说的获取锁(阻塞、非阻塞、可中断、超时等待)、释放锁
  • 内部实现分解

    • LockSupport:与内核交互的线程阻塞和唤醒支持(实现)
    • Condition:MonitorObject支持(模拟MonitorObject阻塞队列)显然为了更好的实现功能在实现Monitor(即下文的AQS时候并不能直接使用LockSupport)
    • AQS:同步队列的管理器、监视器的实现:显然这个是实现显式锁的重点,需要在这里实现:
      • 可公平入队
        • 通过设置fair,主要就是检查队列中是否有等待时间比将要获取线程等待时间长的线程,有则不允许其获取;
      • 可共享
        • 独占锁通过state记录重入的次数
        • 读写锁
          • 通过将state分为俩部分,高16位为读锁总重入次数、低16位为写重入;
          • 对于共享锁,每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,通过getReadHoldCount()获取
      • 超时等待、可中断阻塞获取的实现
        • 通过利用LockSupport的超时等待和可中断阻塞实现
      • Condition实现:即ConditionObject这是AQS对synchronized的优化的重点之一)
        • AQS本身不保存ConditionObject,但是AQS支持条件队列的外部化,这个是和synchronized核心区别之一,而且正是ConditionObject外部化使得AQS区别于ConditionObject可以实现多条件队列(或者说多条monitor的阻塞队列);
        • AQS的Condition支持多条件队列,这就是说AQS的阻塞队列将可以根据不同的状态设置多条阻塞队列;
      • 换句话说这个就是Monitor的加强版
  • 换句话说如果我们需要自定义Lock只需要实现Lock接口定义、通过实现内部类继承AQS

    image-20211224153700079

Lock接口

public interface Lock {
    //阻塞获取锁。
    //如果锁不可用,则当前线程将被禁用以用于线程调度目的并处于休眠状态,直到获得锁为止。
    void lock();
     //运行中断的阻塞获取锁
    void lockInterruptibly() throws InterruptedException;
	//非阻塞获取锁,如果获取到则返回true,否则未false
    boolean tryLock();
	//设置最长等待时间获取锁,1.在等待时间内获取到锁返回true,2.超出等待时间没有获取到锁返回false;3.等待时候被中断线程抛出InterruptedException异常
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
	//释放锁
    void unlock();
	//获取等待通知组件、只有线程获取到锁才能调用该组件的wait方法进行阻塞和释放锁
    Condition newCondition();
}

Condition接口

  • 首先我们知道synchronized是通过监视器ObjectMonitor对象实现进入monitor线程的管理的,而我们并不能显示的使用监视器的该类,所以java提供了实现监视器概念的Condition接口定义相关方法;
  • 对比ObjectMonitor和Condition【也就是使用synchronized和Lock的区别】
    • image-20211205145547449
    • 首先对于Condition,等待队列个数可以有多个,等待队列的节点可以是:超时等待、可中断的阻塞等待、阻塞等待、自动唤醒的等待
    • 所以其最终会形成:image-20211205145920987
public interface Condition {
//简单来说下列一系列的await都是会进入阻塞状态的会自动释放其持有的该锁对象,相当于montitor的wait
    //使当前线程等待,直到它收到信号或被中断。
    void await() throws InterruptedException;
  	//导致当前线程等待,直到它被发出信号。如果当前线程在进入该方法时被设置为中断状态,或者在等待时被中断,则将继续等待直到发出信号。 当它最终从这个方法返回时,它的中断状态仍将被设置。
    	//简单来说就是一个不会因为中断退出该方法,但是最后中断标记还是会设置
    void awaitUninterruptibly();
    
	//相当于wait(XXX)
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
//相当于monitor的notify    
	//相当于notify
    void signal();
	//相当于notifyAll
    void signalAll();
}

同步器AQS

基础

AQS同步器就是java的Monitor的抽象实现:

  • 管理各个同步队列
  • 实现监视器Condition(即ObjectCondition)
  • 管理线程的状态转移
  • 公平和非公平算法的实现、阻塞获取和非阻塞获取的实现、共享锁和非共享锁的实现、可中断和非可中断的实现

AQS体系:

  • 基础
    • AbstractOwnableSynchronizer由线程独占的同步器(基本上就是用来记录独占线程、获取和修改独占线程;)
  • 俩个AQS
    • AbstractQueuedSynchronizer:其继承了AbstractOwnableSynchronizer,队列同步器,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
    • AbstractQueuedLongSynchronizer:同样是继承了AbstractOwnableSynchronizer,其中方法和参数的含义和AbstractQueuedSynchronizer一样,只是不同之处在于所有与状态相关的参数和结果都定义为long而不是int在创建需要 64 位状态的多级锁和屏障等同步器时,此类可能很有用。
  • 一般都是使用AbstractQueuedSynchronizer
AbstractOwnableSynchronizer

这个类定义了创建类似写锁的基本方法

/**
 * 可由线程独占的同步器  ,此类为可能包含独占概念的类提供了基础的创建锁和相关同步提供了基础
 * 此类自身不管理或者使用相关的信息,但是子类和工具类可以使用合适的值来帮助控制和监视访问并提供诊断*/
//简单来说就是这个类定义了创建类似写锁的基本方法
public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** 即使所有的字段都是transient 也要使用 serial id */
    private static final long serialVersionUID = 3737899427754241961L;

    /**
     * 空构造器
     */
    protected AbstractOwnableSynchronizer() { }

    /**
     * 当前得到独占锁的线程
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * 设置当前拥有独占锁的线程
     * null 意味着没有线程拥有访问权限
     */
    	//不会强制任何同步或者任何(volatile)字段访问  
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * 返回最后被setExclusiveOwnerThread设置的值或者null当从未被设置时 
     */
    	//不会强制任何同步或者任何(volatile)字段访问  
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}
AbstractQueuedSynchronizer

前面已经说过AQS主要是完成对于monitor的实现,不过AQS的monitor没有ObjectMonitor的cxq队列*(或者说cxq栈和entry_list合并了)*,,AQS已经实现了基本功能,而且这些功能我们也没理由修改;

  • 实现了阻塞获取和CAS操作、换句话说就是我们如果调用接口直接使用CAS即可;

    image-20211224172012300

  • 同步队列*(即monitor的EntryList+cxq)*:CLH队列同步队列,已经实现通过acquire入队等

  • ObjectCondition的条件队列即momitor的WaiterSet),AQS没有提供获取方法(主要是共享锁没理由有该接口,而且AQS允许多条件队列,也没法指定)所以一般都是通过自己实现即可;(后面的框架中的阻塞队列就是通过ObjectCondition条件队列实现的

    protected Condition newCondition() {
        return new ConditionObject();
    }
    
  • 阻塞实现:ObjectCondition已经实现,调用方法即可

Node
  • Node有俩个特性:有状态有前后指针,这个是实现:超时等待和性能良好的的核心

队列节点node

同步队列(一个双向链表,就是上面的head、tail和有效节点组成的)和条件队列(单向链表,同样使用有head、tail)。

节点的状态很重要:

  • 初始状态:【同步队列】就是waitStatus默认值是0,之所以初始状态为0是因为区分是否有后继节点。如果没有后继节点将为0;
  • 有后继节点的状态 signal:-1 【同步队列】
  • 处于条件队列中 condition:-2【该状态只在条件队列有】
  • 传播状态 propagate:-3 【这个需要特别的关注,再早期的AQS是没有这个状态的使用初始状态代替。后来存在bug增加了该状态,具体到读写锁力分析
  • 中断或超时状态(即失效状态) cancelled:1
image-20211205105728887
		//共享模式的标记
		static final Node SHARED = new Node();
        //独占模式的标记
        static final Node EXCLUSIVE = null;
		//等待状态:
			/**
			            0:初始状态,之所以初始状态为0是因为区分是否有后继节点;
			CANCELLED   1:超时或者被中断,相当于失效状态(进入该状态后,该节点状态不会再改变,等待出队)
			SIGNAL      -1:后继节点处于等待状态,如果当前节点释放同步状态或被取消将通知后继节点运行
							换句话说就是所有节点入队后必须保证前面节点是SINGLE状态除非器前节点是									CANCELLED或者器未头节点
			CONDITION   -2:当前节点在Condition中,等待被调用Condition的single方法唤醒,然后从Collection加入到同步队列
			PROPAGATE   -3:共享式同步状态,将无条件传播到下一节点,就是该状态会向后传播状态
			*/
		volatile int waitStatus;
		//前后指针
        volatile Node prev;
        volatile Node next;
		//节点value,就是队列中的线程
        volatile Thread thread;
		//链接到下一个等待条件的节点,或特殊值 SHARED;下一个condition队列等待节点
        Node nextWaiter;
基础
  • CLH队列:同步队列,同步器的同步队列首先会自旋尝试获取锁或者修改通过修改前一节点的为SINGLE然后阻塞本线程(注意这里阻塞是在同步队列中阻塞不是条件队列)
    • image-20211205173844873
  • ConditionObject:Java版的ObjectMonitor对象,维持着条件队列的阻塞队列
  • 通过CAS实现节点的添加和移除
//重要实例变量
	//同步队列头节点
	private transient volatile Node head;
	//同步队列尾节点
    private transient volatile Node tail;
	//同步状态
    private volatile int state;
//构造器,这体现了同步器实现一般使用继承,而且继承后通过静态内部类实现
protected AbstractQueuedSynchronizer() { }

//状态变换的实现方法
    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }
	//很显然通过CAS实现线程安全
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
Condition实现
ConditionObject
  • 实现了Condition接口的所有方法,具体实现阻塞、唤醒操作的是LockSupport,之所以使用LockSupport是因为可以执行指定激活线程【那么LockSupport又是怎么实现指定线程激活的呢,其使用UNSAFE.unpark直接通过底层操作,我们知道jvm并不会直接操作线程的调度都是HotSportVM将java线程1:1映射到底层】
  • 其实现基于LockSupport
  • ConditionObject通过头尾指针保存的是单向链表,Node是其数据节点的保存形式
  • 条件队列和同步队列之间的节点均为Node
LockSupport
  • 其是一个基本的线程阻塞工具,提供了阻塞线程和唤醒线程的静态方法;就是类似监视器对象monitor的wait和notify(只不过这里的unpark可以指定激活的线程)
  • 用于创建锁和其他同步类的基本线程阻塞原语
  • 方法park和unpark提供了阻塞和解除阻塞线程的有效方法,park支持中断返回、超时设计;【其实就是wait】
  • 下面是阻塞线程的3+3中方式【前后三个作用一样,之所以出现是为了方便使用调试工具查看blocker【即阻塞对象】】image-20211205162930278

LockSupport

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

	/**
	使给定线程的许可可用(如果它尚不可用)。 如果线程在park上被阻塞,那么它将解除阻塞。 
	否则,保证它的下一次park调用不会阻塞。 如果给定的线程尚未启动,则无法保证此操作有任何效果。
	参数:
		thread – 要取消停放的线程,或null ,在这种情况下,此操作无效
	*/
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
//…… 其他方法


}

ConditionObject

实现
public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        //头节点
        private transient Node firstWaiter;
        //尾节点
        private transient Node lastWaiter;

        public ConditionObject() { }

        // Internal methods

       	//添加阻塞节点
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果 lastWaiter 被取消,清除。
            if (t != null && t.waitStatus != Node.CONDITION) {
                //从条件队列中取消链接已取消的等待节点。 仅在持有锁时调用。
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //新节点及添加
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

		//真正实现唤醒一个节点的方法,传入的时候要求frist不为空
        private void doSignal(Node first) {
            //遍历所有节点均执行唤醒方法transferForSignal
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } //transferForSignal将节点从条件队列转移到同步队列,如果成功则返回真。这里是!所以最多只有一个成功
            //first不为空
			while (!transferForSignal(first) && (first = firstWaiter) != null);
        }

        /**
		真正实现唤醒所有节点
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

        /**
        从条件队列中取消链接已取消的等待节点,仅在持有锁时调用。 当在条件等待期间发生取消时,以及在看到 lastWaiter 已被取消时插入新的服务员时,将调用此方法。 需要这种方法来避免在没有信号的情况下垃圾保留。 因此,即使它可能需要完全遍历,它也仅在没有信号的情况下发生超时或取消时才起作用。 它遍历所有节点而不是在特定目标处停止以取消所有指向垃圾节点的指针的链接,而无需在取消风暴期间进行多次重新遍历。
         */
    //简单来说就是遍历取消全部没用的条件队列的节点
        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

        // public methods

        /**
		将等待时间最长的线程(如果存在)从此条件的等待队列移动到拥有锁的等待队列。
         */
        public final void signal() {
            //首先检查是否获取到执行能力;
            //就是检查当前线程是否获取到锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //唤醒第一个有效节点
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

        /**
        同上
         */
        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

        /**
        实现不间断的条件等待。
        1.保存getState返回的锁定状态。
        2.使用保存状态作为参数调用release ,如果失败则抛出 IllegalMonitorStateException。
        3.阻止直到发出信号。
        4.通过以保存状态作为参数调用特定版本的acquire来重新获取
         */
        public final void awaitUninterruptibly() {
            //添加节点到当前条件队列
            Node node = addConditionWaiter();
            //释放锁,唤醒下一节点
            int savedState = fullyRelease(node);
            boolean interrupted = false;
            //如果一个节点(始终是最初放置在条件队列中的节点)现在正在等待重新获取同步队列,则返回 true。
            //简单来说就是被唤醒就返回true
            while (!isOnSyncQueue(node)) {
                //阻塞当前节点
                LockSupport.park(this);
                if (Thread.interrupted())
                    interrupted = true;
            }
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }

        /** 在退出等待时重新中断 */
        private static final int REINTERRUPT =  1;
        /** 在退出等待时抛出 InterruptedException */
        private static final int THROW_IE    = -1;

        /**
        检查中断,如果在发出信号之前中断,则返回 THROW_IE,
        如果发出信号则返回 REINTERRUPT,
        如果未中断则返回 0。
         */
        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

        /**
        根据模式,抛出 InterruptedException、重新中断当前线程或不执行任何操作。
         */
        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //加入节点
            Node node = addConditionWaiter();
            //使用当前状态值调用 release; 返回保存状态。 取消节点并在失败时抛出异常。
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //等待中断或者唤醒
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

        public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            final long deadline = System.nanoTime() + nanosTimeout;
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();
        }

        public final boolean awaitUntil(Date deadline)
                throws InterruptedException {
            long abstime = deadline.getTime();
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            boolean timedout = false;
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (System.currentTimeMillis() > abstime) {
                    timedout = transferAfterCancelledWait(node);
                    break;
                }
                LockSupport.parkUntil(this, abstime);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return !timedout;
        }

        public final boolean await(long time, TimeUnit unit)
                throws InterruptedException {
            long nanosTimeout = unit.toNanos(time);
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            final long deadline = System.nanoTime() + nanosTimeout;
            boolean timedout = false;
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {
                    timedout = transferAfterCancelledWait(node);
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return !timedout;
        }

        //  support for instrumentation

        final boolean isOwnedBy(AbstractQueuedSynchronizer sync) {
            return sync == AbstractQueuedSynchronizer.this;
        }

        protected final boolean hasWaiters() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            for (Node w = firstWaiter; w != null; w = w.nextWaiter) {
                if (w.waitStatus == Node.CONDITION)
                    return true;
            }
            return false;
        }

        protected final int getWaitQueueLength() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int n = 0;
            for (Node w = firstWaiter; w != null; w = w.nextWaiter) {
                if (w.waitStatus == Node.CONDITION)
                    ++n;
            }
            return n;
        }

        protected final Collection<Thread> getWaitingThreads() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            ArrayList<Thread> list = new ArrayList<Thread>();
            for (Node w = firstWaiter; w != null; w = w.nextWaiter) {
                if (w.waitStatus == Node.CONDITION) {
                    Thread t = w.thread;
                    if (t != null)
                        list.add(t);
                }
            }
            return list;
        }
    }

到这里我们知道了Lock的监视器是通过Condition实现的;最常用的同步器AQS就是通过ConditionObject实现Condition接口,其阻塞和唤醒是通过LockSupport实现,LockSupport直接通过操作Unsafe操作底层实现指向线程的唤醒和直接通过底层阻塞;

核心实现
独占锁

acquire为代表的获取锁:acquireInterruptibly、tryAcquire、tryAcquireNanos基本一样,最大的区别就是多了检测释放线程中断,如果中断直接抛出异常不入队,而tryAcquireNanos再多一个每次自旋CAS都检测是否超时,超时直接返回false;

  • 独占锁允许锁降级:指线程拥有写锁,获取读锁后,释放写锁的过程,将持有的锁降级为读锁,但是不能锁升级,即持有读锁的线程不能将锁升级为写锁(因为本身读锁没有记录持有锁的线程,其可能有多个线程持有)
  • 首先尝试非阻塞获取(tryAcquire),否则就入队
    • tryAcquire根据是否为持有锁线程重入分为俩种,如果不是
      • tryAcquire非阻塞根据是否是公平锁可以分为俩种:公平锁检查同步队列中是否有线程排队时间比自己久,没有才进入下一步,非公平锁则不检查;
      • 然后通过CAS尝试获取锁,获取到了修改Lock的线程和state
    • 如果是:
      • 直接重入
  • 入队同样是快速入队、入队失败就CAS+自旋
  • 队后会进行自旋
    • 由于还没阻塞还会通过tryAcquire进行检查能否获取到锁;
    • 如果获取不到会在shouldParkAfterFailedAcquire将上一节点模式修改为SINGLE,除非前一节点为取消状态
      • 如果前一节点为取消状态,让前一节点出队,本线程进入下一次自旋尝试获取锁;
    • 只要前一节点是SINGLE就会进入阻塞
public final void acquire(int arg) {
    //tryAcquire:首先尝试非阻塞获取
    //addWaiter:添加等待到同步队列,其为EXCLUSIVE(独占模式)模式;很显然需要保证其原子性,所以使用CAS尝试一次,可能添加失败,失败的化将进入enq;换句话说就是理论上调用acquireQueued时必然已经成功进入同步队列
    //acquireQueued:以独占不间断模式获取已在队列中的线程
    //selfInterrupt
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

//这里选择可ReentrantLock的公平锁做为例子
	//非阻塞获取锁,或则说参试获取一次锁。
	protected final boolean tryAcquire(int acquires) {
            //获取参试获取锁的线程
            final Thread current = Thread.currentThread();
    		//获取重入的次数
            int c = getState();
        	//如果重入的次数位0说明没有线程获取到锁
            if (c == 0) {
                //hasQueuedPredecessors查询是否有任何线程等待获取的时间比当前线程长,有的话退出获取;这也是公平锁和非公平锁的区别之一
                //CAS获取锁
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
        	//如果获取锁的是由于锁的线程
            else if (current == getExclusiveOwnerThread()) {
                //重入次数+acquires(可重入锁为1)
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }


//为当前线程和给定模式创建和排队节点
//其会参试直接进行CAS,如果成功的化就不需要通过enq的自旋+CAS入队
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        //试试enq的fast path;失败时备份到完整的 enq
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }


//将节点插入队列,必要时进行初始化;插入的方式是不断自旋+CAS
//所谓的必要时就是头节点为空
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //头节点为空进行
            if (t == null) { // Must initialize
                //无论如何金国这一步头节点不再为null
                if (compareAndSetHead(new Node()))
                    tail = head;
            }
            //否则CAS插入尾节点后面
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

//以独占不间断模式获取已在队列中的线程。
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋不断参试获取锁
            for (;;) {
                //返回上一个节点,如果为 null,则抛出 NullPointerException。 当前身不能为空时使用。
                final Node p = node.predecessor();
                //如果当前节点的前面节点是头节点,尝试非阻塞获取【就是获取锁】
                if (p == head && tryAcquire(arg)) {
                    //获取到设置当前节点为头节点
                    setHead(node);
                    //帮助释放当前节点的前节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //检查和更新未能获取的节点的状态。 如果线程应该阻塞,则返回 true。 
                //如果需要阻塞再检查是否中断的便捷方法,否则阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //返回中断指示为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

//检查是否需要阻塞
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
       //前一节点本来就处于存在后继节点状态
        if (ws == Node.SIGNAL)
            //这个节点已经设置了状态,要求释放信号,所以它可以安全地停放。
            return true;
       //其前一个节点已经失效,需要先前寻找其真正有效的前一节点。知道到达最前面,最前面的节点是再允许中的节点必然有效
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //修改有效节点的后一节点
            pred.next = node;
        } else {
            //说明前一节点状态不是无效也不是标记为有下一节点;
            //换句话说前一节点要么为0要么为PROPAGATE传播状态,如果是0的化直接CAS将其换为由下一节点状态
            //否则可以认为不适合将该节点阻塞
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

//阻塞和返回中断状态
rivate final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

release

    public final boolean release(int arg) {
        //非阻塞释放当前线程持有的锁,释放成功返回true
        	//如果不是持有锁的线程将不能释放,对于可重入锁还需要其持有的全部该对象锁均释放才能释放锁
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //唤醒下一个线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
//tryRelease实现基本一致就是检查是否是持有锁线程调用、检查是不是释放全部锁
//读写锁的tryRelease
protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
//可重入锁的tryRelease
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
共享锁

acquireShared即其他共享锁

public final void acquireShared(int arg) {
    //首先尝试非阻塞获取,获取失败再进入阻塞共享锁
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

   private void doAcquireShared(int arg) {
       //共享节点入队
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //如果前节点就是头节点
                if (p == head) {
                    //尝试非阻塞获取共享锁,-1失败,1成功
                    int r = tryAcquireShared(arg);
                    //获取成功,如果获取失败说明
                    if (r >= 0) {
                    	//设置为传播状态
                        setHeadAndPropagate(node, r);
                        //将自身设置为头节点,替换标兵
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

//读写锁的tryAcquireShared
        protected final int tryAcquireShared(int unused) {
            /*
             * Walkthrough:
             * 1. If write lock held by another thread, fail.
             * 2. Otherwise, this thread is eligible for
             *    lock wrt state, so ask if it should block
             *    because of queue policy. If not, try
             *    to grant by CASing state and updating count.
             *    Note that step does not check for reentrant
             *    acquires, which is postponed to full version
             *    to avoid having to check hold count in
             *    the more typical non-reentrant case.
             * 3. If step 2 fails either because thread
             *    apparently not eligible or CAS fails or count
             *    saturated, chain to version with full retry loop.
             */
            //如果是写锁持有,且不是当前线程就直接失败返回-1
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            //当前线程尝试获取:readerShouldBlock检测阻塞策略,最大共享数,尝试获取成功都满足的情况下成功获取到锁
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //获取锁的时候只有其尝试获取锁
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

releaseShared

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }


private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
API

我们知道该同步器的实现时模仿synchronized的所以方法具体也和synchronized类似,只是扩充了对于共享锁、非阻塞获取、可中断等等操作;

  • 独占同步器
    • 阻塞获取:acquire
    • 阻塞释放:release
    • 非阻塞获取:tryAcquire
    • 非阻塞释放:tryRelease
    • 非阻塞超时获取:tryAcquireNanos
    • 可中断阻塞获取:acquireInterruptibly
  • 共享同步器(方法名就是独占同步器后加上shared后缀)
    • 阻塞获取:acquireShared
    • 阻塞释放:releaseShared
    • 非阻塞获取:tryAcquireShared
    • 非阻塞释放:tryReleaseShared
    • 非阻塞超时获取:tryAcquireSharedNanos
    • 可中断阻塞获取:acquireInterruptiblyShared
  • 其他
    • isHeldExclusively:否在未独占线程独占状态
    • 一系列关于queue的操作:
      • getSharedQueuedThreads、getQueuedThreads:获取队列中的线程
      • 获取队列长度、是否为空等等
image-20211205100450090

示例:

class Mutex implements Lock {
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
        	return getState() == 1;
        }
        // 当状态为0的时候获取锁
        public boolean tryAcquire(int acquires) {
        	if (compareAndSetState(0, 1)) {
        		setExclusiveOwnerThread(Thread.currentThread());
        	return true;
        	}
        	return false;
        }
        // 释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
        	if (getState() == 0) 
                throw new IllegalMonitorStateException();
        	setExclusiveOwnerThread(null);
        	setState(0);
        	return true;
        	}
        // 返回一个Condition,每个condition都包含了一个condition队列
        	Condition newCondition() { 
                return new ConditionObject(); 
            }
        }
    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();
    public void lock() { 
        sync.acquire(1); 
    }
    public boolean tryLock() { 
        return sync.tryAcquire(1); 
    }
    public void unlock() { 
        sync.release(1); 
    }
    public Condition newCondition() { 
        return sync.newCondition(); 
    }
    public boolean isLocked() { 
        return sync.isHeldExclusively(); 
    }
    public boolean hasQueuedThreads() { 
        return sync.hasQueuedThreads(); 
    }
    public void lockInterruptibly() throws InterruptedException {
    	sync.acquireInterruptibly(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    	return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

ReentrantLock

基础

  • 可重入锁:允许一个线程多次申请和获取同一个锁
  • 可重入锁设置为独占锁,通过state标识冲入的次数【通过setState、getState、compareAndSetState获取和设置states】
  • 该锁的还支持获取锁时的公平和非公平性选择;【主要就是体现在一个新线程获取锁是否需要通过同步队列,如果允许线程不通过同步队列且同步队列不为空就会不公平】
  • 其继承了Lock类,换句话说就是通过Lock提供对外服务接口、除此之外就是一个是否为公平锁的接口和独占锁队列查询接口

公平锁和非公平锁:等待时间最长的线程先获取锁就是公平锁,如果有可能等待时间不是最长的获取到锁就是非公平锁;

实现

重要参数和构造器

//重要参数	
   private final Sync sync;

//构造器
	//空构造
	public ReentrantLock() {
        sync = new NonfairSync();
    }
	//选择是否使用公平锁:true未公平锁,默认是非公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

内部类

​ 其实Sync就是怎么实现前面所得同步器,这里实现的是AQS同步器(AbstractQueuedSynchronizer);然后针对是否是公平锁分别又有俩个不同的实现类NonfairSync、NonfairSync重写Sync的tryAcquire

API

除了Lock接口以外就是如下接口:

image-20211205113042098

读写锁

基础

  • 前面说到同步器可以实现共享和独占,恰好符合读写锁的规范;
  • 在读多于写的情况下,读写锁能够提供比单纯排它锁更好的并发性和吞吐量
  • 读写锁维护了一对锁,一个读锁和一个写锁通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升
  • 读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞;
  • ReentrantReadWriteLock是读写锁ReadWriteLock的实现;
  • ReentrantReadWriteLock读写锁具有三个特点:可重入、允许锁降级、可选择是否启用公平锁;
  • 读写锁中,读锁不允许获取ObjectCondition(主要是读锁作为共享锁,理论上不应该有update操作,自然不应该有条件队列

ReadWriteLock接口

读写锁的基本接口,如下:其实现主要就是定义获取俩把锁的方法:读锁和写锁

public interface ReadWriteLock {

    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock

基础
  • 其具有:可重入、是否开启公平锁、锁降级锁降级:所谓的锁降级就是写进程可以通过获取读锁然后释放写锁将写进程变为读进程,但是ReentrantReadWriteLock不支持锁升级
  • 我们知道可重入锁通过state记录重入的次数,但是ReentrantReadWriteLock需要记录的是读锁插入的次数、写锁插入的次数比较复杂,为此ReentrantReadWriteLock将state非为上下16位分别标记读锁的重入次数和写锁的重入次数,即高16位标识读锁,低16位标识写锁;【这也是读写锁最大重入的数量short最大值2^16-1】;同时,每个线程获取读锁的次数保存在该线程的ThreadLocal中;
    • image-20211206091348278
  • 对于同步器的实现仍然是在Sync中,公平和非公平主要是重写tryAcquire方法,之所以能实现读写锁显然是应为这个Sync实现实现了共享方法和独占方法;
  • 首先读写锁的读写支持分别在分离的续写锁中,ReentrantReadWriteLock提供读写queue的基本查询和是否为公平锁
  • 读写锁均实现Lock接口,所以读写锁基本操作就是Lock接口定义的操作
实现

主要变量和构造器

//读写锁和Sync读写锁的抽象类
	private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;
//构造器
	public ReentrantReadWriteLock() {
        this(false);
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

//读、写锁移位和标记
		static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//读、写锁计数的实现
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

doAcquireShared

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //尝试非阻塞获取共享锁,返回0、1、n成功。-1失败
                    //-1就是当前位独占锁持有锁,即写锁
                    //1:
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //重点看这里
                        //设置队列头,并检查后继者是否可能在共享模式下等待,如果传播 > 0 或设置了 PROPAGATE 状态,则传播。
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //这里可能会将上一节点的传播状态转为独占状态!
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
    //将队列头设置为节点,从而出队。 仅由获取方法调用。 为了 GC 和抑制不必要的信号和遍历,还清空了未使用的字段。
        setHead(node);
        /**
        如果出现以下情况,请尝试向下一个排队节点发出信号: 传播由调用者指示,或被前一个操作记录(在 setHead 之前或之后作为 h.waitStatus)
        (注意:这使用 waitStatus 的符号检查,因为传播状态可能会转换为 SIGNAL。 ) 和下一个节点在共享模式等待,或者我们不知道,因为它出现 null 这两个检查的保守性可能会导致不必要的唤醒,但只有在有多个竞速获取释放时,所以现在最需要信号或者很快。
        */
    //首先如果propagate>0表明必然是共享锁,因为由多个线程持有锁;
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //共享模式的释放动作——表示后继者并确保传播。
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

tryAcquireShared

       protected final int tryAcquireShared(int unused) {
            //获取当前的线程,即尝试获取共享锁的线程
            Thread current = Thread.currentThread();
            int c = getState();
           //下面说明如果是独占锁持有且不是当前独占锁线程想要获取读锁(即不是锁降级)返回-1
           	//exclusiveCount就是独占锁重入的次数
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
           //进入这里可能是独占锁需要降级或持有锁不是本来就是共享锁或者没线程持有锁
           //计算共享锁的重入次数
            int r = sharedCount(c);
           //readerShouldBlock:这里主要是受是否公平锁的策略影响,如果公平锁不允许插队,即队列中有写线程等待时间更长不允许跳过超过它;如果是非公平锁主要是看第一个在等待的是否是写锁,避免写锁长时间等待饿死
           //MAX_COUNT重入的次数是否达到最大、compareAndSetState修改获取以获取锁显然是CAS
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //下面就是共享锁计数,主要是第一个线程的重入计数和非第一个线程的重入计数
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
           //这里表明跳过上面获取失败。所以需要CAS+自旋获取
            return fullTryAcquireShared(current);
        }
API

获取读、写锁以及一些查询队列信息的操作

image-20211205231342735 image-20211205231358073

信号量

基础

Semaphero信号量同样通过AQS的共享锁实现,支持公平和非公平算法;信号量主要

  • 通过AQS的state记录许可证数量,直接使用acquire和tryacquire使用获取
  • 直接通过release归还许可证
  • 支持公平和非公平获取
  • 没有条件队列(显然不需要),只有阻塞队列获取不到共享锁(许可证数量不足)进入共享的阻塞队列;

实现

构造器:显然就是许可证数量和是否使用公平锁(默认非公平锁)

    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

AQS实现

AQS显然只需要实现共享锁的获取和释放即可

abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
            setState(permits);
        }

        final int getPermits() {
            return getState();
        }
		//非公平获取锁
        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
		//释放共享锁
        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }
		//减少许可证数量
        final void reducePermits(int reductions) {
            for (;;) {
                int current = getState();
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))
                    return;
            }
        }
		//获取所有可用许可证
        final int drainPermits() {
            for (;;) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }

原子变量

  • 很显然原子变量是通过volatile+CAS+自旋实现的
  • 对于需要getAndSet操作的都要通过CAS+自旋实现;
  • 对于直接get、set的显然可以通过volatile直接实现原子性(因为volatile+计算机的缓存一致性协议就能实现);
  • 原子变量主要分为:基本数据类型:boolean、int、long及int、long数组和reference字段三大类

原子变量

  • AtomicBoolean:原子更新布尔类型

    AtomicInteger:原子更新整型

    AtomicLong:原子更新长整型

    AtomicReference:原子更新引用类型

  • AtomicIntegerArray:原子更新整型数组的元素

    AtomicLongArray:原子更新长整型数组里的元素

    AtomicReferenceArray:原子更新引用类型数组里的元素

  • AtomicReferenceFieldUpdate:原子更新引用类型中的字段

    AtomicMarkableReference:原子更新带有标记微的引用类型。可以更新一个布尔类型的标记微和引用类型。

    AtomicIntegerFieldUpdate:原子更新整型的字段的更新器

    AtomicLongFieldUpdate:原子更新长整型字段的更新器

    AtomicStampedReference:原子更新带有版本号的类型。该类将整型值与引用关联起来,可用于原子的更新数据和数据版本号,可以解决使用CAS进行原子更新时可能出现ABA问题。

以数组为例子数组可能也是最常用的原子变量了,因为无论是对象的字段还是数组的元素,都不能通过volatile保证可见性,俩种只能保证其指针的可见性(就是引用的可见性)

以int的数组原子变量为,例子:其元素的获取、设置都是通过本地方法xxxIntVolatile保证可见性、原子性和有效性;

public final void set(int i, int newValue) {
        unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
    }

public final int getAndAdd(int i, int delta) {
        return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
    }

    public final int incrementAndGet(int i) {
        return getAndAdd(i, 1) + 1;
    }

API

image-20211206235613355

阻塞队列和常用容器

CopyOnWrite容器

  • CopyOnWriteList
  • CopyOnWriteSet

同步容器(Concurrent)

concurrentHashMap:在容器的时候已经介绍:略;

ConcurrentLinkedQueue

基础
  • 很显然其和ConcurrentHashMap一样为了保证效率使用弱一致性;同样不接受null
  • 通过链表实现(无界)、安全队列(FIFO),最重要的是非阻塞;
  • 具有collection和queue接口的方法、扩展了非阻塞方法
  • 其实现还是通过CAS+自旋实现非阻塞,但是其优化了CAS的使用体现在对于head和tail的CAS操作上
    • 所谓的优化主要是在修改头尾节点上,其移除头元素后并不会立刻删除头节点,同样其追加元素后也不会立刻修改尾节点,由于这两个节点都是volatile修饰的,修改将会导致缓存失效,所以每两跳修改一次,而且;
    • addAll(想创建传入的所有元素的节点链,然后整体插入最后), removeAll, retainAll, containsAll, equals, toArray(创建新ArrayList数组,然后遍历concurrentLinkedQueue,将遍历到的元素的value加入新数组)方法不会保证原子性
    • size获取需要遍历整个集合(),而且其获取到的不一定是实时的结果与大多数集合不同,此方法不是恒定时间操作。 由于这些队列的异步特性,确定当前元素的数量需要 O(n) 遍历。 另外,如果在执行该方法的过程中添加或删除元素,则返回的结果可能不准确。
实现

重要变量和构造器:就是头尾节点、空构造会让head、tail指向一个空节点

	private transient volatile Node<E> head;
    private transient volatile Node<E> tail;


    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
    public ConcurrentLinkedQueue(Collection<? extends E> c) {
        Node<E> h = null, t = null;
        for (E e : c) {
            checkNotNull(e);
            Node<E> newNode = new Node<E>(e);
            if (h == null)
                h = t = newNode;
            else {
                t.lazySetNext(newNode);
                t = newNode;
            }
        }
        if (h == null)
            h = t = new Node<E>(null);
        head = h;
        tail = t;
    }

add、offer都是通过offer实现

  • 闯入的e不能为null,因为需要用到null作为失效节点标记(失效节点不会立刻移除)
  • 自旋+CAS尝试完成数据尾部添加,如果tail已经失效直接指向head(head必然可以到达所有有效节点)
  • 否则检查是否已经俩跳,是的话需要将p移动到tail
public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);
		//t、p指向tail,注意tail不一定是尾节点
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            //这里如果满足,说明上一时刻tail是尾节点
            if (q == null) {
                // 尝试CAS插入
                if (p.casNext(null, newNode)) {
                    //注意这里,间隔更新尾节点的是绝对不会更新尾节点的(p=t)
                    if (p != t) // 一次跳两个节点
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                //cas失败说明有其他线程插入成功只需要继续后移
            }
            //如果p=q说明原来的tail指向的节点已经移除,需要更新
            else if (p == q)
                //将其指向新的tail或则头节点(主要是t赋值期间移除的话就指向head,head可以到达任意有效节点)
                //这里需要注意 t != (t = tail);按照指令,先将t值入栈、然后tail入栈将tail赋值给t再比较;就是说其实还是旧值和新值比较,这里需要对操作数栈有比较号的认识,操作数栈保存的是数不是变量,不会因为t被改变而改变
                p = (t != (t = tail)) ? t : head;
            else
                // 两跳后检查尾部更新。
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

remove和poll

public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
				//同样的原理,俩跳才修改
                if (item != null && p.casItem(item, null)) {
                    // Successful CAS is the linearization point
                    // for item to be removed from this queue.
                    if (p != h) // hop two nodes at a time
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                //到达最后
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                //节点失效
                else if (p == q)
                    continue restartFromHead;//重新进入,使用了goto
                else
                    //p后移
                    p = q;
            }
        }
    }

    public boolean remove(Object o) {
        if (o != null) {
            Node<E> next, pred = null;
            //遍历查找
            for (Node<E> p = first(); p != null; pred = p, p = next) {
                boolean removed = false;
                E item = p.item;
                //节点元素有效
                if (item != null) {
                    //如果和希望移除不相等
                    if (!o.equals(item)) {
                        //获取下一个节点
                        next = succ(p);
                        continue;
                    }
                    //CAS移除,成功返回true
                    removed = p.casItem(item, null);
                }
                //去除无效节点
                next = succ(p);
                if (pred != null && next != null) // unlink
                    //让它指向他自己
                    pred.casNext(p, next);
                //退出
                if (removed)
                    return true;
            }
        }
        return false;
    }

	final Node<E> succ(Node<E> p) {
        Node<E> next = p.next;
        // p = next说明被删除
        return (p == next) ? head : next;
    }

常用api

image-20211207001151875

阻塞队列(BlockQueue)

  • 阻塞队列通过可重入锁+条件队列实现生产者消费者模式创建(两个Condition:分别是full和empty)
  • BlockingQueue方法有四种形式,处理不能立即满足但可能在未来某个时刻满足的操作的方式各不相同:一个抛出异常,第二个返回一个特殊值( null或false ,取决于操作)第三个线程无限期地阻塞当前线程,直到操作成功,第四个阻塞仅给定的最大时间限制,然后放弃
    • image-20211207095238434
  • BlockingQueue不接受null元素
  • 这里阻塞的意思是:如果队列满将阻塞插入线程直到有空间插入,如果队列空将阻塞获取线程直到可以获取
  • 其实现通过可重入锁的ConditionObject条件队列实现阻塞队列,通过同步队列实现进入阻塞队列的线程安全
  • 支持公平和非公平设置(很显然就是可重入锁的公平和非公平设置)
  • 由7个基本的实现;
    • 有界
      • ArrayBlockingQueue:数组实现的阻塞队列,队满会阻塞(容量为指定的容量)
      • LinkedBlockingQueue:链表实现的阻塞队列,队满会阻塞(容量默认为Integer.MAX_VALUE)
      • SynchronousQueue:不存储元素的阻塞队列,所以会按一进一处进行
    • 无界
      • PriorityBlockingQueue:支持优先级设置的阻塞队列
        • 默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则
      • DelayQueue:按(到期时间)优先级排序的阻塞队列,放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走(定时任务就是通过这个实现的)
        • Delayed实现:重点就是如何重写getDelay方法;参考ScheduledFutureTask:1.初始化时使用time记录当前对象延迟到什么时候可以使用;2.getDelay利用time和当前时间返回当前元素还需要延时多长时间,单位是纳秒;3.实现compareTo方法来指定元素的顺序
      • LinkedTransferQueue:链表的阻塞队列
        • 相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
        • tryTransfer:如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法
          时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等
          待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回
        • tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。
      • LinkedBlockingDeque:双向链表的阻塞队列
        • LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法

以ArrayBlockingQueue为例子

put和take实现

  • 都是通过获取可重入锁、ConditionObject的条件队列实现的;检查是否队列满(空),队满多空则阻塞
  • 在enqueue、dequeue使用Condition的await和single实现阻塞和唤醒(因为只有可能队满或队空阻塞所以唤醒必然时合理的阻塞线程)
  • enqueue、dequeue仅仅就是简单的插入或异常队列数据,然后count++或–
public void put(E e) throws InterruptedException {
        checkNotNull(e);
    //获取可重入锁
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

Executor

Executor框架

  • 我们知道线程解在接受CPU调度期间有多种状态(阻塞、就绪、活动……),如果将其维度向类的扩大:线程状态可以分为:初始化、提交执行(执行阶段)、结束可以说Executor框架就是将java的线程划分为下列阶段而准备的;

    • 初始化阶段就是将一个实现Runnable接口、Callable接口的类实例化;
    • 提交执行阶段需要一个执行器专门处理提交执行的实例化的线程类,程序通过execute提交后,执行器异步择机执行;
    • 执行器处理完成后,可能会有处理结果(Callable接口的类),需要通过Future类获取执行结果或者执行状态;
    • 任务相当于初始化后的类(必须要是可执行的无论是Runnable还是Callable接口本质都要通过Callable),是一个可执行的类;被执行任务需要实现的接口:Runnable接口或Callable接口
    • 提交执行一个执行器(可以说时线程池),包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类(接口)实现了ExecutorService接口(ThreadPoolExecutor(线程池异步任务框架)类和ScheduledExecutorService接口(周期任务或定时API接口))。
    • 异步计算的结果就相当于理论结束后的返回值(Futrue接口),包括接口Future和实现Future接口的FutureTask类
  • Executor接口仅仅定义了execute方法,就是提交执行;

    • ExecutorService提供了:获取Futrue的提交方法【(submit:对于一个任务)和(invokeXXX 对于一个Callable集合的任务)】和关闭执行器(shutdown、shutdownNow)(即不再接受新任务【执行器没有正在执行的任务,没有等待执行的任务,也没有新的任务可以提交。 应关闭未使用的ExecutorService以允许回收其资源。】)
    • AbstractExecutorService显然就是ExecutorService方法的默认实现类
    • ThreadPoolExecutor:线程池执行池,它是线程池的实现类;
      • ScheduledThreadPoolExecutor继承自ThreadPoolExecutor实现了ScheduledExecutorService接口。它主要用来在给定的延迟之后运行任务,或者定期执行任务。
  • Future

    • Future表示异步计算的结果。 提供了检查计算是否完成、等待其完成以及检索计算结果的方法。 结果只能在计算完成后使用get方法检索,必要时阻塞,直到它准备好取消由cancel方法执行。 提供了其他方法来确定任务是正常完成还是被取消。 一旦计算完成,就不能取消计算;就5个方法:get俩个、cancel、isCancelled、isDone
    • RunnableFuture:继承Runnbale,主要是为了不提交直接执行获取结果;
    • FutureTask; 此类提供Future的基本实现,具有启动和取消计算、查询以查看计算是否完成以及检索计算结果的方法。 计算完成后才能检索结果; 如果计算尚未完成, get方法将阻塞。 一旦计算完成,就不能重新开始或取消计算(除非使用runAndReset调用计算)
  • 任务:显然就是实现Runnable或则Callable的类的实例(Callable的cell方法和Runnable的run方法一样。但是cell是有返回值的,如果我们希望提交异步任务有返回值就需要用Callable接口

ScheduledExecutorService

image-20211222125053142

简单说明各个API:

  • scheduleRunnable/Callable:任务、long:执行延迟时间、TimeUnit:时间单位;
  • scheduleAtFixedRate:第一个long就是开始时间,后一个long就是周期(就是每隔long执行一次,但是如果一次执行时间过长,在到达周期没执行完则不会并发执行,跳过本次周期)
  • scheduleWithFixedDelay后一个long是相邻两次执行的时间间隔(或者说一次执行结束后、下一次执行开始前的时间间隔)

下面简单看看JUC是怎么实现周期任务和异步任务的

ScheduledThreadPoolExecutor

该类继承ThreadPoolExector、实现ScheduledExecutorService;显然作为JUC包下的,其应该是线程安全的;

  • 为了实现周期任务,首先将任务包装为ScheduledFutureTask(用了实现状态保存,即是否为周期任务以及周期任务的其他信息)
  • 然后保存在具有现有队列性质的DelayedWorkQueue(自身的阻塞队列),通过Delayed获取任务的距离达到执行的时间以确定是否执行)
  • 重写execute和submit方法以保证通过该类实现的都是定时周期任务
  • 所有任务通过delayedExecute真正实现周期任务入队(理论上所有任务都要先入优先队,再由检查到达到执行周期而出队执行),显然由于是优先队列每次只需要检查阻塞队列的首节点即可;

image-20211222132019184

DelayedWorkQueue的offer
        public boolean offer(Runnable x) {
            if (x == null)
                throw new NullPointerException();
            RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                int i = size;
                if (i >= queue.length)
                    grow();
                size = i + 1;
                if (i == 0) {
                    queue[0] = e;
                    setIndex(e, 0);
                } else {
                    siftUp(i, e);
                }
                if (queue[0] == e) {
                    leader = null;
                    available.signal();
                }
            } finally {
                lock.unlock();
            }
            return true;
        }

ThreadPoolExecutor

总结

  • 使用工厂模式+策略模式实现
    • 工厂模式:通过线程工厂创建线程hashset作为worker容器(ReentrantLock可重入锁保证并发安全)、worker自己实现AQS管理线程的状态即方便线程状态管理,同时又是不可重入的,避让执行任务过程中被shutdown中断);
    • 策略模式:拒绝策略可选
  • 执行过程主要包括:任务管理、线程管理和交接三部分
    • 任务管理主要是对外提供的execute,处理提交到执行器的任务(执行流程)
      • image-20211208173408726
      • worker的执行流程:
        • 首先允许中断(是否持有的worker的排他、非可重入锁)
        • 获取可执行的任务
          • 当前线程创建是本身自带任务(并将其置为空,下次将在队列中获取任务)
          • 在阻塞队列中获取到了任务
            • 非核心线程和核心线程的区别主要在获取任务,非核心线程将不能在阻塞队列获取到任务,然后会在这退出循环,线程结束
        • 重新获取锁,再次检查是否发出中断命令是否需要中断执行
        • 获取到执行权利(执行钩子方法beforeExecute)然后执行任务的run方法
        • 执行完成进行后置钩子方法和统计处理以及释放worker的锁
        • 回到步骤2;
      • 交接者:在阻塞队列获取任务(getTask)
        • 根据当前线程数确定当前线程是否为核心线程
          • 如果非核心线程当前线程数大于1或者队列本身为空,让本线程退出执行(返回null任务)
          • 如果为核心线程检查是否允许超时中断,如果允许且超时(即已经超时阻塞获取过一次阻塞队列任务,且获取失败),当前存在其他核心线程或者队列本身为空,允许退出
          • 在阻塞队列中超时等待获取

基础

线程池的状态:

image-20220314161900334
  • Running:允许状态(可接受处理状态);
  • SHUTDOWN:不接收新任务,但能处理已添加的任务;
  • STOP:终止,不接收新任务,且包括中断正在执行任务的线程**(如果线程正在执行任务(由于worker使用自定义的非可重入的AQS,所以会阻塞等待,(worker执行完成后会进行一次释放锁的操作)获取到锁再中断);**
  • TIDYING:即将进入最后终止,当所有的任务已终止,调用terminated进入最后阶段
  • terminated:最后终止,线程池彻底终止,就变成TERMINATED状态。

在开始ThreadPoolExecutor后开始:先介绍器四种使用Executors的默认提供的线程池:换句话说就是帮助我们配置了默认的参数

  • SingleThreadExecutor:单核心线程线程池,没有非核心线程;
  • ScheduledThreadPool:具有延时效应的线程池,后面介绍;
  • CachedThreadPool:只有非核心线程的线程池;
  • FixedThreadPool:固定核心线程的线程池,最大非核心线程和核心线程数目一致,所有非核心线程空闲就销毁;

我们知道ThreadPoolExecutor可以通过传入:1.核心线程数、2.最大线程数、3.非核心线程存活时间、4.时间单位、5.阻塞队列、6.线程工厂、7.拒绝策略 自定义线程池;

  • 核心线程数、最大线程数、非核心线程存活时间和时间单位:就是线程工厂创建后,即时空闲也不会销毁的线程、非核心线程则是最大线程数-核心线程数,其空闲可存活时间就是非核心线程存活时间设置的时间,而核心线程呢?默认其会一直自旋(工作),如果设置true将超过和非核心线程一样的一定空闲时间就阻塞

  • 阻塞队列:就是我们前面提到的7种阻塞队列,一般如果是无界的阻塞队列将不存在拒绝策略(因为其必然接受);

  • 线程工厂:就是实现了ThreadFactory接口的类,提供newThread创建线程;

  • 拒绝策略继承RejectedExecutionHandler接口,实现其方法;在ThreadPoolExecutor提供了四种默认实现的拒绝策略

    • AbortPolicy:抛出异常;

    • CallerRunsPolicy:被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务,除非执行器已经关闭,在这种情况下任务将被丢弃。简单来说就是直接使用调用者线程执行,如果调用者线程已经关闭或者执行器已经关闭则抛弃;

    • DiscardOldestPolicy:很明显就是抛弃队列中头节点,然后入队

    • DiscardPolicy:被拒绝任务的处理程序,它默默地丢弃被拒绝的任务就是说直接抛弃不报异常

    • 重点看一下怎么写拒绝策略:显然就是实现RejectedExecutionHandler,重写

    • public static class CallerRunsPolicy implements RejectedExecutionHandler {
              public CallerRunsPolicy() { }
      		@Override
              public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                  //首先判断执行器是否关闭
                  if (!e.isShutdown()) {
                      //然后直接调用r开线程执行
                      r.run();
                  }
                  //如果需要队列操作最好使用内部类,这样比较方便操作线程
              }
          }
      

线程工厂的实现:我们先看别人是怎么实现的:DefaultThreadFactoryh还是Executors提供的默认ThreadFactory实现

//我们实现自己的线程工厂一定需要 设置合适的线程名、可能需要合适的栈大小    
static class DefaultThreadFactory implements ThreadFactory {
        //线程池号
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        //线程组
        private final ThreadGroup group;
        //线程数
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        //名字前缀
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            //通过这里设置线程真正运行的runnable
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            //非守护
            if (t.isDaemon())
                t.setDaemon(false);
            //设置为默认级别
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

到这里基本实现了线程工厂、阻塞队列和拒绝策略;在进入原理前既然是工厂模式既然有一个worker:Worker

Worker

很显然这个工厂的worker就是线程,特别是核心线程,但是线程有一点点特殊,这里使用AQS进行管理,另外就是实现了Runnable接口(显然是为了使用run方法),使用AQS而不是使用ReentranLock(主要是避免重复获取,而且也方便检查锁状态)

  • Worker 主要维护线程运行任务的中断控制状态简化获取和释放围绕每个任务执行的锁。 这可以防止旨在唤醒等待任务的工作线程的中断,而不是中断正在运行的任务、不希望工作任务在调用诸如 setCorePoolSize 之类的池控制方法时能够重新获取锁。

  • public void run() {
        //可以看到真正运行的是runWorker
                runWorker(this);
            }
    

原理

下面开始解析ThreadPoll Execute的运行逻辑:前面已经说过Execute框架主要就是将线程三个阶段分开实现异步功能(类似中间件),所以为了实现异步功能线程的在ThreadPoll xecute中可以分为三部分:任务管理、线程管理和俩者交接(假设叫交接者)

首先认识核心变量

	//ctl记录线程当前数目、执行器的状态(其中前3位位执行器状态,后面29位位线程数)
//状态包括(5个):运行状态、终止状态(且中断所有在执行的线程任务)、不再接受新任务等待执行完终止状态、即将计入最后终止状态、最后终止状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 省略部分对于ctl修改的原子操作、省略了6个核心构造参数
// 持有 mainLock 时访问才能HashSet<Worker>
    private final ReentrantLock mainLock = new ReentrantLock();
//workers
    private final HashSet<Worker> workers = new HashSet<Worker>();	
//阻塞队列
	private final BlockingQueue<Runnable> workQueue;

在这里插入图片描述
任务管理
  • 首先所有的线程都是通过execute方法(无论是submit提交【仅仅多了生成Futrue的操作】还是execute方法),进入执行器。所以需要execute作为任务接受接口
  • 对进入执行器的方法理论上可以去三个地方:1.交给交接者让其创建线程执行;2.进入阻塞队列;3.执行拒绝策略;
  • 对于进入阻塞队列的任务理论上也只有三种去处:1.被中断;2.被交接者获取执行;3.因为拒绝策略被剔除出队;
线程管理
  • 线程管理理论上可以分为3种:交接者进行线程创建、线程销毁和线程阻塞和唤醒;
  • 线程的创建和销毁具有一般性,线程阻塞和唤醒比较有意思;
交接

首先看execute方法,我们知道所有任务地都在这进入执行器 (这里需要注意的是submit并不是直接提交Runnable的,而是提交FutrueTask进入,我们知道FutrueTask实现了RunnableFutrue实现了Runnable接口,所以最后执行的run方法不再是我们定义的run方法而是加工的run方法【后面重点介绍】)

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
    //计算当前线程数是否小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            //提交交接者创建线程执行任务
            if (addWorker(command, true))
                return;
            //创建失败重新获取线程数和执行器状态字段
            c = ctl.get();
        }
    //检查进行器状态,只有runnable才可以接受任务,否则非阻塞入队
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //再次检查线程是否是运行状态,这里主要避免入队期间线程已经shutdown、并且线程数目已经变为0要变为终止了,但是这时入队成功了,如果这次检查通过说明真入队成功了
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //没有线程则创建线程(这里主要应对核心线程数位0的设置)
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
    //这里是入队不成功,交接者会试图创建非核心线程执行该任务(这里要重点记住)
        else if (!addWorker(command, false))
            reject(command);
    }

到了这里大概知道execute都干了什么:

  • 首先尝试创建核心线程执行任务;
  • 创建失败则尝试进入阻塞队列,等待核心线程执行;
  • 入队还是失败则尝试创建非核心线程执行任务;【到这里可以说其任务执行时不公平的】

然后看addWorker创建线程

//从上面入参就可以猜出一个是任务、一个是是否是核心线程    
private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
    //第一个自旋:
    	//可以说是针对执行器的;
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            //简单来说就是检查执行器状态和执行器对应状态的策略
            	//如果是SHUTDOWN状态:并且workQueue不为空,并且不是先传入任务的话就运行继续
            	//如果是除了运行、SHUTDOWN外的状态直接拒绝
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&firstTask == null && ! workQueue.isEmpty()))
                return false;
			//第二个自自旋
            	//这个自旋就针对获取线程的
            for (;;) {
                int wc = workerCountOf(c);
                //第一个判断新添加worker不能超过最大值,如果线程数目达到最大不允许添加
                //第二个则是不允许超过设置值,特别是其希望创建核心线程;
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //显然就是创建worker
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                //检测是否是执行器状态改变
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
//进入这里绝对是获取到了创建worker权力
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //创建worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                //前面说了worker入hashset需要加锁
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
					//检测执行器状态,略
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        //进入这里基本可以认为可以在hashset添加worker,不过还要检测线程状态
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    //解锁
                    mainLock.unlock();
                }
                //添加成功、执行
                if (workerAdded) {
                    //调用run方法,前面我们知道run方法就是调用runWorker
                    t.start();
                    //提交执行结果
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

到这里基本可以认为线程通过交接者直接执行已经基本实现了,基本步骤如下:

  • 首先获取添加worker权力,主要就是针对执行器和线程数的检查
  • 然后向workers添加新worker,添加成功则执行;影响添加主要是:执行器状态和创建的线程是否无故被杀死

我们知道任务提交还有另外俩种可能:拒绝策略和入队后被出队执行;拒绝策略跳过;主要看怎么经过阻塞队列执行;在这之前显然需要知道worker线程的管理:

  • worker进入的是hashsethashset是要通过可重入锁获取操作权力的;前面我们已经知道worker是实现了AQS的
  • worker是线程,按理来说不需要哈希保存,这里我们直接看看workers都被哪里调用了:【可以发现之所以哈希保存主要是为了中断指定线程和指定线程退出的时候回收

有了上面的基础我们大概可以猜测:所有线程会在runWorker中不断运行直到有人中断他们,这时他们将退出和回收;

到runWorker方法验证:

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        // 提交执行的任务,或者说第一次使用worker时提交的任务
        Runnable task = w.firstTask;
        //任务置为空
        w.firstTask = null;
        //注意这里允许中断(由于workerAQS非可重入);
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            //获取任务(一般),如果是第一次构造可能是首次要求的任务
            while (task != null || (task = getTask()) != null) {
                w.lock();
                //检查是否可以执行:主要还是执行器是否终止,线程是发被中断:
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                //进入这里理论上有执行的权力,除非自定义beforeExecute还检查是否需要中断等
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //这里直接使用他的run方法就是将线程类当普通类调用
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        //执行后处理
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    //线程完成工作数统计
                    w.completedTasks++;
                    w.unlock();
                }
            }
            //线程正常退出标记
            completedAbruptly = false;
        } finally {
            //线程退出
            processWorkerExit(w, completedAbruptly);
        }
    }

​ 到这里为止我们注意到工作线程运行确实入我们所想一直执行,线程的交接者仅仅做了一件事获取任务getTask,好像缺点啥,先看getTask

/**
执行阻塞或定时等待任务,具体取决于当前配置设置,或者如果由于以下任何原因而必须退出此工作程序,则返回 null: 
	1. 有超过 maximumPoolSize 的工作程序(由于调用 setMaximumPoolSize)。 
	2. 池停止。 
	3. 池关闭,队列为空。 
	4. 这个worker等待任务超时,超时的worker在定时等待前后都会被终止(即allowCoreThreadTimeOut || workerCount > corePoolSize ),如果队列不为空,这个worker不是池中的最后一个线程。
*/
private Runnable getTask() {
        //是否超时
        boolean timedOut = false; 
        //自旋获取任务
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
            // 先检查执行器、在看阻塞队列是否还有任务
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                //任务数-1
                decrementWorkerCount();
                return null;
            }
            //线程数;
            int wc = workerCountOf(c);
//如果为 false(默认),核心线程即使在空闲时也保持活动状态。 如果为 true,可以认为核心线程和非核心线程区别不大,仅仅是创建时机不同(就是一个是直接创建,一个是队满后才能创建)        
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
			//检查是否需要终止线程:
      //非核心线程 timed将会为true,如果wc也大于0说明有线程活跃,本线程可以退出执行
      //核心线程如果设置了allowCoreThreadTimeOut将在超时等待后,允许核心线程退出执行
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                //尝试ctl-1,成功则会在后弦正常退出线程
                if (compareAndDecrementWorkerCount(c))
                    return null;
                //失败则继续
                continue;
            }
			//到这里没有终止,说明队列不为空,而且该线程应该核心线程且具有执行的权利
            try {
                //timed是否需要阻塞获取或计时获取
                Runnable r = timed ? 
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
                //如果为空说明是计时的核心线程(或者没有核心线程时只有一个非核心线程的情况下,非核心线程也可以处理阻塞队列的任务)获取失败(没有获取到任务),直接设置timeOut为true
                if (r != null)
                    //获取到返回
                    return r;
                //核心线程超时等待
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

到这里总算直到交接者对于线程能否接受任务的管理了

  • 总结以下前面发现:其实交接者对于阻塞队列的线程交接也很简单
    • 通过线程数知道其是核心线程还是非核心线程
    • 通过阻塞阻塞时间获取判断是否有资格
    • 通过阻塞队列阻塞等待的线程
  • 然后我们回顾执行,我们发现worker终止执行仅仅只是判断是否可以执行(执行器、本线程是否中断)worker退出也仅仅和终止执行或者没有任务但是以及空闲时间已经到达有关

总结

基本工作流程:

image-20211208173408726

执行器功能划分:(下图有问题,主要体现在corePool才能获取阻塞队列任务上)

image-20211208173525108

Futrue

基础

前面说过Future保存异步计算结果,显然异步计算还没运行前不会有结果,所以这个Futrue也是一个执行器的入口(h),主要看FutureTask,其实Futrue的实现类,前面我们已经知道FutrueTask会包装我们提交的任务,就是将器run方法写入Future的run方法中:

  • 任务同样有三种基本状态:还未执行、运行中和已完成
    • 还未执行状态:0:NEW【新任务】、5:INTERRUPTING【终止】、6:INTERRUPTED【已经终止】、4:CANCELLED【取消】
    • 运行中: 1:COMPLETING:执行完成在设置结果
    • 已完成:2:NORMAL【正常停止】、3:EXCEPTIONAL【异常停止】
  • FutrueTask通过重写run方法实现对于任务执行和执行状态的控制
  • Futrue通过state标记状态、通过object保存结果,通过CAS修改状态+treiber栈
    • 当前设计中的同步控制依赖于通过 CAS 更新的“状态”字段来跟踪完成,以及一个简单的 Treiber 堆栈来保存等待线程。而之前使用的是AQS,AQS有一个问题就是:当多个线程对同一个FutureTask执行cancel的时候,FutureTask会把中断状态保留起来,让调用方感到奇怪。

实现

主要变量和构造器

//任务    
	private Callable<V> callable;
//结果
    private Object outcome; 
//状态
    private volatile int state;
//运行线程、主要是为了终止
    private volatile Thread runner;
//等待线程的 Treiber 堆栈,后面重点介绍;简单拉私活就是一个无限的后进先出的数据结构,通过CAS来进出栈
	//到这里不妨大胆猜着大致猜到实现类似于任意的链表:一个头节点不保存有用数据,然后就是每次通过CAS再器后面插入,获取当然也是CAS获取
    private volatile WaitNode waiters;



	public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

run方法:

    public void run() {
        //很显然通过CAS可以让只有一个线程执行run
        if (state != NEW ||
          	//如果runner空显然可以获取到执行权
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //真正的run、call
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    //异常
                    setException(ex);
                }
                if (ran)
                    //执行完成设置结果
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            //确保来自可能的 cancel(true) 的任何中断仅在运行或 runAndReset 时传递给任务。
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

**get阻塞执行就跳过比较简单 **

Treber栈

image-20211209103445108

Treiber Stack Algorithm是一个可扩展的无锁栈,利用细粒度的并发原语CAS来实现的

    static final class WaitNode {
        //线程
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

入栈

    private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        //自旋入栈
        for (;;) {
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }
//相当于future是否还有效判断
            int s = state;
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                //显然就是入栈
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

Fork/Jion框架

  • Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
    • 首先是创建一个可以分割的任务:ForkJionTask接口定义了可以分割任务的基本方法
    • 最后是确定怎么合并任务:执行时和结果保存通过一个双端队列实现的,显然由于其调度的特殊性需要通过特点线程池ForkJionPool调度
  • 工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行;Fork/Jion框架默认使用工作窃取;

基础

​ Frok/Jion:

  • ForkJionPool(执行器)用于运行ForkJoinTask的ExecutorService ,换句话说还是ExecutorService、还是再Executor框架上实现的,那么可以猜测:首先具有异步执行的特性、使用工厂模式、同样是三部分任务管理、交接者、线程管理这部分显然就是执行器
  • ForkJionTask(任务接口):和Execute框架的其他task最大区别就是客人可以分割,不过同样实现了Futrue,说明还是具有异步操作提交的任务的能力;
  • WorkQueue(阻塞队列,不是真正意义上的阻塞队列):在实现阻塞队列的时候,由于任务的分解时认为有提交后就可以确定被分解成什么样(或则说任务分解时确定的,只是还没分解前不能知道,或则说无论提交多少次粪污分解都是该结果),所以其设置只需要像操作数栈一样即可;任务的阻塞队列ForkJionPool中是通过数组实现的(前面我们知道),另外worker管理也是通过数组实现;这点和ThreadPoolExecute有很大不同

PS:后续所有开源学习笔记同步到gitee,有需要去拉取 https://gitee.com/wusport/open-source-notes

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,您想了解关于Java并发编程中的CountDownLatch吗? CountDownLatch是Java并发编程中非常重要的一个工具类,它可以让某个线程等待其他线程成后再继续执行。它的作用类似于倒计时门闩,当count值减为0时,所有等待者都会被唤醒。 CountDownLatch是一次性的,也就是说,它的计数器只能使用一次。如果需要重复使用,可以考虑使用CyclicBarrier。 在使用CountDownLatch时,我们需要先创建一个CountDownLatch对象,并指定计数器的初始值。然后在需要等待的线程中调用await()方法进行等待,同时在其他线程中调用countDown()方法进行计数器的减1操作。 举个例子,假设我们有一个需求:主线程需要等待两个子线程成后再执行。那么可以这样编写代码: ```java import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行开始"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行毕"); countDownLatch.countDown(); }, "线程1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "执行开始"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行毕"); countDownLatch.countDown(); }, "线程2").start(); System.out.println(Thread.currentThread().getName() + "等待子线程执行毕"); countDownLatch.await(); System.out.println(Thread.currentThread().getName() + "所有子线程执行毕,继续执行主线程"); } } ``` 在上面的例子中,我们首先创建了一个计数器初始值为2的CountDownLatch对象,然后创建了两个线程分别进行一些操作,并在操作结束后调用countDown()方法进行计数器减1操作。在主线程中,我们调用await()方法进行等待,直到计数器减为0时,主线程才会继续执行。 希望能够对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舔猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值