Java并发总结

线程和进程的区别

  • 进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,是系统进行资源分配和调度的基本单位
  • 线程则是进程的一个执行路径(实体),一个进程中至少有一个线程,进程中的多个线程共享进程的资源
  • 操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位
  • 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域
  • 线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反
  • 虚拟机栈、本地方法栈和程序计数器都是线程私有的,堆和方法区是所有线程共享的;
  • 程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置,所以是线程私有的;是为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。

线程的生命周期

Java 线程在运⾏的⽣命周期中的指定时刻只可能处于下⾯ 6 种不同状态的其中⼀个状态

在这里插入图片描述面试题:为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法?
调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏,会把 run()⽅法当成⼀个 main 线程下的普通⽅法去执⾏。

并发和并行的区别

  • 并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束
  • 并行是说在单位时间内多个任务同时在执行
  • 并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行

wait和sleep的区别

  • wait方法会释放该共享对象获得的锁,该调用线程会被阻塞挂起,直到发生调用了该共享对象的 notify() 或者 notifyAll() 方法或者调用了该共享对象的 notify() 或者 notifyAll() 方法
  • wait方法为什么要释放锁:如果不释放,由于其他生产者线程和所有消费者线程都已经被阻塞挂起,而这就处于了死锁状态
  • 当一个执行中的线程调用了 Thread 的 sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源
  • 即wait方法会释放锁,而sleep会抱着锁睡
  • **虚假唤醒:**一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用 notify()、 notifyAll() 方法进行通知,或者被中断,或者等待超时
  • 如何避免虚假唤醒: 做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用 wait() 方法进行防范

yield和join的区别

  • join方法:当线程中调用另一一个线程的join方法,会将该线程挂起,而不是忙等待,直到目标线程结束
  • yield方法:当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。
  • 操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了 Thread 类的静态方法 yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度

线程死锁

  • 死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去
    在这里插入图片描述
  • 死锁产生的四个条件
  1. **互斥条件 :**指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。
  2. 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  3. 不可剥夺条件 :指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  4. 环路等待条件 :指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0, T1, T2,…, Tn} 中的 T0 正在等待一个 T1 占用的资源, T1 正在等待 T2 占用的资源,……Tn 正在等待已被 T0 占用的资源。
  • 如何避免死锁:破坏请求并持有和环路等待条件。即保证线程资源申请的有序性即可。

守护线程与用户线程

  • 用户线程:平时用到的普通线程均是用户线程
  • 守护线程:指在程序运行的时候在后台提供一种通用服务的线程,守护线程是为用户线程服务的,当有用户线程在运行,那么守护线程同样需要工作,当所有的用户线程都结束时,守护线程也就会停止
  • 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。

线程同步

线程安全问题: 线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题,这个时候需要用到线程同步。
线程同步: 多个线程并发访问资源时,保证共享资源发生在同一时刻只被一个线程。同步是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,前面的使用完毕,下一个线程再使用,线程同步执行条件:队列 + 锁
解决线程同步的手段有两种:互斥同步和非堵塞同步(悲观与乐观)

并发编程的三个重要特性

  1. 原⼦性 : ⼀个的操作或者多次操作,要么所有的操作全部都得到执⾏并且不会收到任何因素的⼲扰⽽中断,要么所有的操作都执⾏,要么都不执⾏。 synchronized 可以保证代码⽚段的原⼦性。
  2. **可⻅性 :**当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新值。 volatile 关键字可以保证共享变量的可⻅性。synchronized和final同样可以保证可见性。
  3. **有序性 :**在本线程内观察所有操作都是有序的(线程内表现为串行的语义),另外一个线程观察此线程所有操作都是无序的(指令重排序) 。 volatile 关键字可以禁⽌指令进⾏重排序优化。

synchronized 关键字

  • synchronized 块是 Java 提供的一种原子性内置锁, Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的 wait 系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁

  • **使用synchronized带来的问题:**由于 Java 中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而 synchronized 的使用就会导致上下文切换,并带来线程调度开销。并且一个线程持有锁会导致其他所有需要此锁的线程挂起。如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致性能倒置,引起性能问题。

  • synchronized 的一个内存语义:这个内存语义就可以解决共享变量内存可见性问题。进入 synchronized 块的内存语义是把在 synchronized 块内使用到的变量从线程的工作内存中清除,这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出 synchronized 块的内存语义是把在 synchronized 块内对共享变量的修改刷新到主内存

讲⼀下 synchronized 关键字的底层原理

  • synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
    当执⾏monitorenter 指令时,线程试图获取锁也就是获取 **monitor(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)**的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏monitorexit 指令后,将锁计数器设为0,表明锁被释放。
  • synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法, JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。

JDK1.6之后synchronized 关键字底层做了哪些优化:
JDK1.6 对锁的实现引⼊了⼤量的优化,如偏向锁、轻量级锁、⾃旋锁、适应性⾃旋锁、锁消除、锁粗化等技术来减少锁操作的开销。锁主要存在四种状态,依次是:⽆锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈⽽逐渐升级。注意锁可以升级不可降级,这种策略是为了提⾼获得锁和释放锁的效率。

理解上下文切换

在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当前线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

  • 线程上下文切换时机有 : 当前线程的 CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。

单例模式

public class Singleton {
	private volatile static Singleton uniqueInstance;
	private Singleton() {
	}
	public synchronized static Singleton getUniqueInstance() {
	//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
	if (uniqueInstance WX null) {
	//类对象加锁
	synchronized (Singleton.class) {
		if (uniqueInstance WX null) {
			uniqueInstance = new Singleton();
		}
	}
	}
	return uniqueInstance;
}
}
  1. 为什么需要两次判断if(singleTon==null)?
  • **第一次校验:**由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。

  • **第二次校验:**如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

  1. 为什么要用voitilote修饰?

Instance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton()
这段代码其实是分为三步执⾏:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址
    但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1i>3i>2。指令重排在单线程环境下不会出
    现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和
    3,此时 T2 调⽤ getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回
    uniqueInstance,但此时 uniqueInstance 还未被初始化。
    使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏

ReentrantLock

从JDK5.0开始,Java提供了通过显示定义同步锁对象来实现同步,并通过ReentrantLock类实现了Lock,它拥有与synchronized想通的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

二者的区别

  1. Synchronized 内置的Java关键字, Lock 是一个Java类
  2. Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
  3. Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
  4. Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
  5. Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);
  6. Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

非堵塞同步

采用锁的方式解决同步问题也称为互斥同步或堵塞同步,属于一种悲观的并发策略
这里还有另外一种选择:非堵塞同步 ,基于冲突检测的乐观并发策略

对共享资源进行操作,如果没有其他线程争用数据,那操作就成功了;
如果共享数据有争用,产生了冲突,那就采用其他的补偿策略(自旋:不断重试,直到成功),这种并发策略不需要挂起线程。
常用的指令为CAS

生成者消费组问题

这是一个线程同步问题,生产者和消费者共享一个资源,并且生产者和消费者之间互为依赖,互为条件

  1. 对于生产者,没有生产产品前,通知消费者等待,生产产品后,通知消费者消费
  2. 对于消费者,消费之后要通知生产者已经结束消费,需要生产新的产品提供消费
  3. 在生产者消费者问题中,仅有synchronized是不够的
    - synchronized可阻止并发更新同一个共享资源,实现了同步
    - synchronized不能用来实现线程之间的通讯

ThreadLocal

  • 多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步。
  • ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
  • ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
  • 举例: Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

ThreadLocal实现原理

在这里插入图片描述Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap,其中 key 为我们定义的 ThreadLocal 变量的 this 引用, value 则为我们使用 set 方法设置的值。在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用 ThreadLocal 的 set 或者 get 方法时才会创建它们。其实每个线程的本地变量不是存放在 ThreadLocal 实例里面,而是存放在调用线程的 threadLocals 变量里面。也就是说, ThreadLocal 类型的本地变量存放在具体的线程内存空间中。 ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面并存放起来,当调用线程调用它的 get 方法时,再从当前线程的 threadLocals 变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的 threadLocals 变量里面,可能会造成内存溢出,因
此使用完毕后要记得调用 ThreadLocal 的 remove 方法删除对应线程的 threadLocals 中的本地变量。


 public void set(T value) {
 		//(1)获取当前线程
        Thread t = Thread.currentThread();
        //(2)将当前线程作为key,去查找对应的线程变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
        //(3)第一次调用就创建当前线程对应的HashMap
            createMap(t, value);
    }
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

public T get() {
	//(4) 获取当前线程
	Thread t = Thread.currentThread();
	//(5)获取当前线程的threadLocals变量
	ThreadLocalMap map = getMap(t);
	//(6)如果threadLocals不为null,则返回对应本地变量的值
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
		@SuppressWarnings("unchecked")
		T result = (T)e.value;
		return result;
	}
}
	//(7)threadLocals为空则初始化当前线程的threadLocals成员变量
	return setInitialValue();
}
  • ThreadLocal 不支持继承性:同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。
  • InheritableThreadLocal可以让子线程可以访问在父线程中设置的本地变量。使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
private void test() {    
final ThreadLocal threadLocal = new InheritableThreadLocal();       
threadLocal.set("xx");    
Thread t = new Thread() {        
    @Override        
    public void run() {            
      super.run();            
      Log.i( threadLocal.get());        
    }    
  };          
  t.start(); 
}
  • 如果线程的inheritThreadLocals变量不为空,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。

  • ThreadLocalMap没有实现Map接口,而且他的Entry是继承**WeakReference(弱引用)**的,也没有看到HashMap中的next,所以不存在链表了

  • **弱引用:**只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

  • 如果发生Hash碰撞问题,即key不等于entry,(开放地址法)那就找下一个空位置,直到为空为止

  • ThreadLocal是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

  • 内存泄漏: ThreadLocal在没有外部强引用时,发生GC时会被回收(key是弱引用,被直接回收 ),如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。(解决方法remove)

内存可见性问题

问题描述

  • Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。
    在这里插入图片描述- JMM是抽象的概念,在实际内存结构中,CPU每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 都共享的二级缓存。那么 Java 内存模型里面的工作内存,就对应这里的 L1 或者 L2 缓存或者 CPU 的寄存器。
    在这里插入图片描述内存不可见问题:引发原因,Cache的存在,线程首先会从一级/二级缓存中查看是否存在需要读取的数值,如果不存在才会去主内存中读取,如果一个线程对变量进行了修改,另外一个线程中这个值并为从主内存中读取,而缓存中该指大小是以及被修改了的,因此则会产生内存不可见问题
  1. 线程 A 首先获取共享变量 X 的值,由于两级 Cache 都没有命中,所以加载主内存中 X 的值,假如为 0。然后把 X=0 的值缓存到两级缓存,线程 A 修改 X 的值为 1,然后将其写入两级 Cache,并且刷新到主内存。线程 A 操作完毕后,线程 A 所在的CPU 的两级 Cache 内和主内存里面的 X 的值都是 1。
  2. 线程 B 获取 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X= 1 ;到这里一切都是正常的,因为这时候主内存中也是 X=1。然后线程 B 修改 X 的值为 2,并将其存放到线程 2 所在的一级 Cache 和共享二级 Cache 中,最后更新主内存中 X 的值为 2 ;到这里一切都是好的
  3. 线程 A 这次又需要修改 X 的值,获取时一级缓存命中,并且 X=1,到这里问题就出现了,明明线程 B 已经把 X 的值修改为了 2,为何线程 A 获取的还是 1 呢?这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见。

volatile

https://ifeve.com/java-volatile%e5%85%b3%e9%94%ae%e5%ad%97/#more-45241

  • synchronized可以解决内存不可见问题,但synchronized是重量级锁,会为线程带来上下文的切换开销
  • volatile是java虚拟机提供的最轻量级的同步机制,该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
  • volatile 虽然提供了可见性保证,但并不保证操作的原子性
  • 使用volatile的场景:
    • 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取—计算—写入三步操作,这三步操作不是原子性的,而 volatile 不保证原子性。
    • 读写变量值时没有加锁。

指令重排

Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。

  • 指令重排是指CPU采用允许将多条指令不按程序的顺序分开发送给各相应电路处理单元

  • volatile 的另外一语义是禁止指令重排优化

  • 写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile读之前。

  • 如将代码编译成JIT的汇编代码,可以看到用volatile修饰的变量,赋值后语句后会多执行一个"lock…"操作,找个操作就相当于一个内存屏障,重排序时不能把后面的指令重排序到内存屏障之前。同时lock指令,也意味着所以之前的操作都已经执行完成。

happens-before

参考:https://www.cnblogs.com/chenssy/p/6393321.html
从JDK 5 开始,JMM就使用happens-before的概念来阐述多线程之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。 happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题。
happens-before原则定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

下面是happens-before原则规则:

  • 程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。
  • 锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
  • volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

CAS

CAS 即 Compare and Swap,其是 JDK 提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性。JDK 里面的 Unsafe 类提供了一系列的compareAndSwap* 方法

  • CAS 有四个操作数,分别为 :对象内存位置对象中的变量的偏移量变量预期值和新的值
  • 操作含义是,如果对象 obj 中内存偏移量为 valueOffset 的变量值为 expect,则使用新的值 update 替换旧的值 expect。
  • ABA问题:假如线程 I 使用 CAS 修改初始值为 A 的变量 X,那么线程 I 会首先去获取当前变量 X 的值(为 A),然后使用 CAS 操作尝试修改 X 的值为 B,如果使用 CAS 操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程 I 获取变量 X 的值 A 后,在执行 CAS 前,线程 II 使用 CAS 修改了变量 X 的值为 B,然后又使用 CAS 修改了变量 X 的值为 A。所以虽然线程 I 执行 CAS时 X 的值是 A,但是这个 A 已经不是线程 I 获取时的 A 了
  • 为了解决ABA问题:JDK 中的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

Unsafe类

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作, Unsafe 类中的方法都是native 方法,它们使用 JNI 的方式访问本地 C++ 实现库。
一些代表性的api

compareAndSwapLong(Object obj, long offset, long expect, long update)
void park(boolean isAbsolute, long time) 方法 : 阻塞当前线程,
void unpark(Object thread) 方法 :唤醒调用 park 后阻塞的线程
ong getAndSetLong(Object obj, long offset, long update) 方法 :获取对象 obj 中偏移
量为 offset 的变量 volatile 语义的当前值,并设置变量 volatile 语义的值为 update。
  • Unsafe 类是 rt.jar 包提供的, rt.jar 包里面的类是使用 Bootstrap 类加载器加载的,而我们的启动 main 函数所在的类是使用AppClassLoader 加载的,所以在 main 函数里面加载 Unsafe 类时,根据委托机制,会委托给 Bootstrap 去加载 Unsafe 类。

伪共享

伪共享的非标准定义为:当 CPU 访问某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把该变量所在内存区域的一个 Cache 行大小的内存复制到 Cache 中。由于存放到 Cache 行的是内存块而不是单个变量,所以可能会把多个变量存放到一个 Cache 行中当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享
在这里插入图片描述如何避免伪共享
在 JDK 8 之前一般都是通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中,

public final static class FilledLong {
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6;
}

假如缓存行为 64 字节,那么我们在 FilledLong 类里面填充了 6 个 long 类型的变量,每个 long 类型变量占用 8 字节,加上 value 变量的 8 字节总共 56 字节。另外,这里FilledLong 是一个类对象,而类对象的字节码的对象头占用 8 字节,所以一个 FilledLong对象实际会占用 64 字节的内存,这正好可以放入一个缓存行
JDK 8 提供了一个 sun.misc.Contended 注解,用来解决伪共享问题。将上面代码修改为如下。

@sun.misc.Contended
public final static class FilledLong {
public volatile long value = 0L;
}

  • 悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态
  • 乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。
  • 根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁
  • 公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁
  • 非公平锁则在运行时闯入,也就是先来不一定先得
  • ReentrantLock 提供了公平和非公平锁的实现;ReentrantLock pairLock = new ReentrantLock(false)为非公平锁。 如果构造函数不传递参数,则默认是非公平锁
  • 在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。
  • 根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁
  • 独占锁保证任何时候都只有一个线程能得到锁, ReentrantLock 就是以独占方式实现的,独占锁是一种悲观锁
  • 共享锁则可以同时由多个线程持有,例如 ReadWriteLock 读写锁,它允许一个资源可以被多线程同时进行读操作,共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。
  • **可重入锁:**当一个线程再次获取它自己已经获取的锁时,如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数地进入被该锁锁住的代码。
  • synchronized 内部锁是可重入锁
  • 可重入锁的原理:锁在内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为 0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成 1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加 +1,当释放锁后计数器值 -1。当计数器值为 0 时,锁里面的线程标示被重置为 null,这时候被阻塞的线程会被唤醒来竞争获取该锁。
  • 自旋锁: 当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可以使用 -XX:PreBlockSpinsh 参数设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起
  • 为什么要自旋锁: 由于 Java 中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。

java原子变量操作类

1.AtomicLong

JUC 包提供了一系列的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的,其内
部使用 Unsafe 来实现,synchronized等锁实现原子性操作在性能上会有些损耗。
以AtomicLong为例,AtomicLong 是原子性递增或者递减类,其内部使用 Unsafe 来实现:
代码如下:

  • AtomicLong类也是在 rt.jar 包下面的, AtomicLong 类就是通过 BootStarp 类加载器进行加载的
public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;

	// ( 1)获取Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //( 2)存放变量value的偏移量
    private static final long valueOffset;
    //( 3)判断JVM是否支持Long类型无锁CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

    private static native boolean VMSupportsCS8();

    static {
        try {
        //( 4)获取value在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	//( 5)实际变量值
    private volatile long value;
	/**
	*递增和递减操作代码
	*/
	//调用unsafe方法,原子性设置value值为原始值+1,返回值为原始值
	  public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
	//调用unsafe方法,原子性设置value值为原始值-1,返回值为原始值
    public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }

    /**
     * 调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值
     */
    public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }

  • Unsafe 的 getAndAddLong是个原子性操作,这里第一个参数是 AtomicLong 实例的引用,第二个参数是 value 变量在 AtomicLong 中的偏移值,第三个参数是要设置的第二个变量的值。
   public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
        //拿到当前变量的当前值,由于 value 是 volatile 变量,所以这里拿到的是最新的值
            var6 = this.getLongVolatile(var1, var2);
        //然后在工作内存中对其进行增 1 操作,而后使用 CAS 修改变量的值。如果设置失败,则循环继续尝试,直到设置成功。
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

**unsafe.compareAndSwapLong:**如果原子变量中的 value 值等于 expect,则使用 update 值更新该值并返回 true, 否则返回 false。

2.LongAdder

  • AtomicLong 通过 CAS 提供了非阻塞的原子性操作,但使用 AtomicLong 时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS 操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS 的操作,而这会白白浪费 CPU 资源。
  • JDK 8 新增了一个原子性递增或者递减类 LongAdder 用来克服在高并发下使用AtomicLong 的缺点
  • 使用 LongAdder 时,则是在内部维护多个 Cell 变量,每个 Cell 里面有一个初始值为 0 的 long 型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了争夺共享资源的并发量。另外,多个线程在争夺同一个 Cell 原子变量时如果失败了,它并不是在当前 Cell 变量上一直自旋 CAS 重试,而是尝试在其他 Cell 的变量上进行 CAS 尝试,这个改变增加了当前线程重试 CAS 成功的可能性。最后,在获取 LongAdder 当前值时,是把所有 Cell 变量的 value 值累加后再加上 base 返回的。
    在这里插入图片描述

JUC并发集合

并发List

  • 发包中的并发 List 只有 CopyOnWriteArrayList。 CopyOnWriteArrayList 是一个线程安全的 ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略
    -每 个 CopyOnWriteArrayList 对 象 里 面 有 一 个array 数组对象用来存放具体元素, ReentrantLock 独占锁对象用来保证同时只有一个线程对 array 进行修改
  1. 添加方法
    (1) 获得独占锁ReentrantLock ,一个线程获取到锁后,就保证了在该线程添加元素的过程中其他线程不会对array 进行修改。
    (2) 复制 array 到一个新数组,并将原来数组大小增加1
    (3) 把新增的元素添加到新数组,并使用新数组替换原数组
    (4) 释放锁
   public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
  1. 获取元素get(int index)
public E get(int index) {
	return get(getArray(), index);
}
final Object[] getArray() {
	return array;
}
private E get(Object[] a, int index) {
	return (E) a[index];
}

如果在get时候有个线程删除数组元素,那么 remove 操作首先会获取独占锁,然后进行写时复制操作,也就是复制一份当前 array 数组,然后在复制的数组里面删除线程 x 通过 get 方法要访问的元素 1,之后让 array 指向复制的数组。而这时候array 之前指向的数组的引用计数为 1 而不是 0(get方法获得的数组)。因此,get方法会获得删除前的元素,这就是写时复制策略产生的弱一致性问题

  1. 修改指定元素
  • 获得独占锁,获得当前数组,并get到当前index上的元素
  • 如果指定位置的元素值与新值不一致则创建新数组并复制元素
  • 然后在新数组上修改指定位置的元素值并设置新数组到 array
  • 如果指定位置的元素值与新值一样,则为了保证 volatile 语义,还是需要重新设置 array
    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
  1. 删除元素
    与上述方法很接近,
  • 获取独占锁,获取数组和指定位置元素
  • 如果要删除的是最后一个元素,则直接将前面数组复制到新数组中
  • 如果删除中间数据,创建新数组,分两次复制删除后剩余的元素到新数组(待删除元素前后)
  • 使用新数组代替老数组
  • 释放锁
 public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

  1. 弱一致性的迭代器
    弱一致性是指返回迭代器后,其他线程对 list 的增删改对迭代器是不可见的(在迭代器遍历过程中,其他线程对其进行增删改等操作无法生效)
  • 获取独占锁
  • checkForComodification()方法判断是否有其他线程对List进行修改,如果当前大小和预期不一样,则抛出ConcurrentModificationException
  • 返回一个COWSubListIterator对象,COWSubListIterator所维持的数组是一个final对象
  • 释放锁
   public Iterator<E> iterator() {
            final ReentrantLock lock = l.lock;
            lock.lock();
            try {
                checkForComodification();
                return new COWSubListIterator<E>(l, 0, offset, size);
            } finally {
                lock.unlock();
            }
        }
     private void checkForComodification() {
            if (l.getArray() != expectedArray)
                throw new ConcurrentModificationException();
        }

AQS

AQS 的全称为( AbstractQueuedSynchronizer),它是实现同步器的基础组件,并发包中锁的底层就是使用 AQS 实现的
AQS的核心思想:
AQS 核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线
程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中,AQS 是将每条请求共享资源的线程封装成⼀个CLH 锁队列的⼀个结点(Node)来实现锁的分配
LockSupport 工具类:

  • JDK 中的 rt.jar 包里面的 LockSupport 是个工具类,它的主要作用是挂起和唤醒线程,在AQS中被大量地使用。
  • LockSupport 类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport 类的方法的线程是不持有许可证的
  • park() :调用 park 方法的线程已经拿到了与 LockSupport 关联的许可证,则调用LockSupport.park() 时会马上返回,否则调用线程会被禁止参与线程的调度
  • unpark(Thread thread): 一个线程调用 unpark 时,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有。如果 thread 之前因调用 park() 而被挂起,则调用unpark 后,该线程会被唤醒。
    在这里插入图片描述
  • AQS 是一个 FIFO 的双向队列,其内部通过节点 head 和 tail 记录队首和队尾元素,队列元素的类型为 Node。其中 Node 中的 thread 变量用来存放进入 AQS队列里面的线程
  • 在 AQS 中 维 持 了 一 个 单 一 的 状 态 信 息 state, 可 以 通 过 getState、 setState、compareAndSetState 函数修改其值。
  • 对于 ReentrantLock 的实现来说, state 可以用来表示当前线程获取锁的可重入次数 ;
  • 对于读写锁 ReentrantReadWriteLock 来说, state 的高 16位表示读状态,也就是获取该读锁的次数,低 16 位表示获取到写锁的线程的可重入次数;
  • AQS 有个内部类 ConditionObject,用来结合锁实现线程同步。ConditionObject 是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程
  • 对于 AQS 来说,线程同步的关键是对状态值 state 进行操作。根据 state 是否属于一个线程,操作 state 的方式分为独占方式共享方式

**独占方式:**只有⼀个线程能执⾏,如 ReentrantLock 。就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作 state 获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。

  • 独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,在 AQS 内部会首先使用 CAS 操作把 state 状态值从 0变为 1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从 1 变为 2,也就是设置可重入次数,而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。
  • 独占方式下,获取与释放资源的方法为 acquire(int arg)release(int arg)
    (1) 当一个线程调用 acquire(int arg) 方法获取独占资源时,会首先使用 tryAcquire 方法尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回,失败则将当前线程封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞队列的尾部,并调LockSupport.park(this) 方法挂起自己。
    (2) 当一个线程调用 release(int arg) 方法时会尝试使用 tryRelease 操作释放资源,这里是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread) 方法激活 AQS 队列里面被阻塞的一个线程 (thread)。被激活的线程则使用 tryAcquire 尝试,看当前状态变量 state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起。

**共享方式:**当多个线程去请求资源时通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取即可

  • 比如 Semaphore 信号量,当一个线程通过 acquire() 方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。
  • 在共享方式下,获取与释放资源的方法是acquireShared(int arg)releaseShared(int arg)
    (1) 当线程调用 acquireShared(int arg) 获取共享资源时,会首先使用 tryAcquireShared尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回,失败则将当前线程封装为类型为 Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列的尾部,并使用LockSupport.park(this) 方法挂起自己。
    (2) 当一个线程调用 releaseShared(int arg) 时会尝试使用 tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 **LockSupport.unpark( thread)**激活 AQS 队列里面被阻塞的一个线程 (thread)。被激活的线程则使用 tryReleaseShared 查看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入 AQS 队列并被挂起

ReentrantLock

  • ReentrantLock 是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的 AQS 阻塞队列里面。
  • ReentrantLock 默认是非公平锁
  • AQS 的 state 状态值表示线程获取该锁的可重入次数,在默认情况下, state的值为 0 表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用 CAS设置 state 的值为 1,如果 CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为 2,这就是可重入次数。在该线程释放该锁时,会尝试使用 CAS 让状态值减 1,如果减 1 后状态值为 0,则当前线程释放该锁。
  • ReentrantLock 的 lock() 委托给了 sync 类,根据创建 ReentrantLock 构造函数选择 sync 的实现是 NonfairSync 还是 FairSync,这个锁是一个非公平锁或者公平锁
    在这里插入图片描述
    lock():
    当线程获取到该锁时,会将AQS 的状态值设为 1,然后直接返回,如果当前线程之前已经获取过该锁,则这次只是简单地把 AQS 的状态值加 1 后返回。 如果该锁已经被其他线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起
final void lock() {
	//( 1) CAS设置状态值
if (compareAndSetState(0, 1))
	//设置该锁持有者是当前线程
	setExclusiveOwnerThread(Thread.currentThread());
else
	//( 2)调用AQS的acquire方法
	acquire(1);
}

当没获取到锁时,则调用AQS的acquire方法,再次尝试获取锁,如果未获取则加入堵塞队列

public final void acquire(int arg) {
	//(3)调用ReentrantLock重写的tryAcquire方法
	if (!tryAcquire(arg) &&
	// tryAcquiref返回false会把当前线程放入AQS阻塞队列
	acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            	// 取出队列的前驱结点 
                final Node p = node.predecessor();
                // 如果前驱结点是头节点,则尝试获取  
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果唤醒失败,则将节点状态改为signal(-1)(等待unpark)
                if (shouldParkAfterFailedAcquire(p, node) &&
                //LockSupport.park(this);堵塞当前线程
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
       final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

unlock方法:
尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的 AQS状态值减 1,如果减去 1 后当前状态值为 0,则当前线程会释放该锁,否则仅仅减 1 而已。如果当前线程没有持有该锁而调用了该方法则会抛出 IllegalMonitorStateException 异常

 public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        //状态设置成功,尝试将队列中靠近头节点且waitStatus小于0的节点唤醒
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
//(11)如果不是锁持有者调用UNlock则抛出异常
	int c = getState() - releases;
	if (Thread.currentThread() != getExclusiveOwnerThread())
	throw new IllegalMonitorStateException();
	boolean free = false;
	//(12)如果当前可重入次数为0,则清空锁持有线程
	if (c == 0) {
	free = true;
	setExclusiveOwnerThread(null);
	}
	//(13)设置可重入次数为原始值-1
	setState(c);
	return free;
}
    private void unparkSuccessor(Node node) {
		
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            //随后从队尾往前找节点状态需要<0,并离头节点最近的节点进行唤醒
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
       		 //唤醒节点	
            LockSupport.unpark(s.thread);
    }


总结ReentrantLock:

  • 假如线程 Thread1、Thread2 和 Thread3 同 时 尝 试 获 取 独 占 锁ReentrantLock,假设Thread1 获取到了,则 Thread2 和 Thread3 就会被转换为 Node 节点并被放入 ReentrantLock 对应的 AQS 阻塞队列,而后被阻塞挂起。
  • 假设 Thread1 获取锁后调用了对应的锁创建的条件变量 1,那么Thread1 就会释放获取到的锁,然后当前线程就会被转换为 Node 节点插入条件变量 1 的条件队列。由于 Thread1 释放了锁,所以阻塞到 AQS 队列里面的 Thread2 和 Thread3 就有机会获取到该锁,假如使用的是公平策略,那么这时候 Thread2 会获取到该锁,从而从AQS 队列里面移除 Thread2 对应的 Node 节点。
    在这里插入图片描述图源java3Y

ReentrantReadWriteLock

介绍ReentrantReadWriteLock :
ReentrantLock 是独占锁,某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然 ReentrantLock 满足不了这个需求,所以 ReentrantReadWriteLock 应运而生。它的底层是使用 AQS 实现的。ReentrantReadWriteLock 巧妙地使用 AQS 的状态值的高 16 位表示获取到读锁的个数,低16 位表示获取写锁的线程的可重入次数,并通过 CAS 对其进行操作实现了读写分离,这在读多写少的场景下比较适用。

  • 读锁:如果当前线程获取读锁过程中,发现有其他线程获取到了写锁,则通过AQS将当前线程加入堵塞队列;如果当前要获取读锁的线程已经持有了写锁,则也可以获取读锁。但是需要注意,当一个线程先获取了写锁,然后获取了读锁处理事情完毕后,要记得把读锁和写锁都释放掉,不能只释放写锁。

StampedLock

StampedLock 是并发包里面 JDK8 版本新增的一个锁,该锁提供了三种模式的读写控制, 当调用获取锁的系列函数时,会返回一个 long 型的变量,我们称之为戳记(stamp),这个戳记代表了锁的状态。其中 try 系列获取锁的函数,当获取锁失败后会返回为 0 的stamp 值。 当调用释放锁和转换锁的方法时需要传入获取锁时返回的 stamp 值。StampedLock 的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。

  • 写锁:是一个排它锁或者独占锁,某时只有一个线程可以获取该锁,当一个线程获取该锁后,其他请求读锁和写锁的线程必须等待,这类似于ReentrantReadWriteLock 的写锁(不同的是这里的写锁是不可重入锁
  • 悲观读锁 readLock :是一个共享、不可重入锁。在没有线程获取独占写锁的情况下,多个线程可以同时获取该锁。如果已经有线程持有写锁,则其他线程请求获取该读锁会被阻塞,这类似于 ReentrantReadWriteLock 的读锁。悲观是指在具体操作数据前其会悲观地认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑
  • 乐观读锁 tryOptimisticRead :它是相对于悲观锁来说的,在操作数据前并没有通过CAS 设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非 0 的 stamp 版本信息。获取该 stamp 后在具体操作数据前还需要调用validate 方法验证该 stamp 是否已经不可用,也就是看当调用 tryOptimisticRead 返回stamp 后到当前时间期间是否有其他线程持有了写锁,如果是则 validate 会返回 0,否则就可以使用该 stamp 版本的锁对数据进行操作。由于 tryOptimisticRead 并没有使用 CAS 设置锁状态,所以不需要显式地释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用位操作进行检验,不涉及 CAS 操作

CountDownLatch 、CyclicBarrier

Java3y公众号:

https://mp.weixin.qq.com/s/Z3u7qUU4R4GZ-TXRM_OXsw

线程池

线程池的优势

  • 一是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接 new 一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程
  • 二是线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。

线程池可以通过Executors工具类内部的静态方法创建不同的线程池

线程池类型:

  • newFixedThreadPool :创建一个核心线程个数和最大线程个数都为 nThreads 的线程池,并且阻塞队列长度为 Integer.MAX_VALUE。 keeyAliveTime=0 说明只要线程个数比核心线程个数多并且当前空闲则回收
  • newSingleThreadExecutor: 创建一个核心线程个数和最大线程个数都为 1 的线程池,并且阻塞队列长度为 Integer.MAX_VALUE。 keeyAliveTime=0 说明只要线程个数比核心线程个数多并且当前空闲则回收。
  • **newCachedThreadPool :**创建一个按需创建线程的线程池,初始线程个数为 0,最多线程个数为 Integer.MAX_VALUE,并且阻塞队列为同步队列。 keeyAliveTime=60说明只要当前线程在 60s 内空闲则回收。这个类型的特殊之处在于,加入同步队列的任务会被马上执行,同步队列里面最多只有一个任务。
public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
      public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
        public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
       public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

阿里java规范:线程池不建议使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors各个方法的弊端:

  1. newFixedThreadPoolnewSingleThreadExecutor:
      主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  2. newCachedThreadPoolnewScheduledThreadPool:
      主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

ThreadPoolExecutor

ThreadPoolExecutor类图如下,ThreadPoolExecutor 继承了AbstractExecutorService成员变量 ctl 是一个 Integer 的原子变量,用来记录线程池状态和线程池中线程个数
在这里插入图片描述
ctl变量:
Integer 类型是 32 位二进制表示,则其中高 3 位用来表示线程池状态,后面29 位用来记录线程池线程个数

	//默认是RUNNING状态,线程个数为0
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //线程个数掩码位数,Integer的二进制位数-3后的剩余位数所表示的数才是线程的个数
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //线程最大个数(低29位)00011111111111111111111111111111
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    //(高3位): 11100000000000000000000000000000
    private static final int RUNNING    = -1 << COUNT_BITS;
    //(高3位): 00000000000000000000000000000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //(高3位): 00100000000000000000000000000000
    private static final int STOP       =  1 << COUNT_BITS;
    //(高3位): 01000000000000000000000000000000
    private static final int TIDYING    =  2 << COUNT_BITS;
    //(高3位): 01100000000000000000000000000000
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    // 获取高3位(运行状态)
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    //获取低29位(线程个数)
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    //计算ctl新值(线程状态与线程个数)
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    /**
     * 通过CAS增加ctl的值,线程个数
     */
    private boolean compareAndIncrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect + 1);
    }

    /**
     * 通过CAS减少ctl的值,线程个数
     */
    private boolean compareAndDecrementWorkerCount(int expect) {
        return ctl.compareAndSet(expect, expect - 1);
    }
   

线程池状态:

  • RUNNING : 接受新任务并且处理阻塞队列里的任务。
  • SHUTDOWN: 拒绝新任务但是处理阻塞队列里的任务
  • **STOP :**拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。
  • **TIDYING :**所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为 0,将要调用 terminated 方法
  • **TERMINATED :**终止状态。 terminated 方法调用完成以后的状态。

线程池状态转换:

  • **RUNNING -> SHUTDOWN :**显式调用 shutdown() 方法,或者隐式调用了 finalize()方法里面的 shutdown() 方法。
  • **RUNNING 或 SHUTDOWN-> STOP :**显式调用 shutdownNow() 方法时。
  • **SHUTDOWN -> TIDYING :**当线程池和任务队列都为空时。
  • **STOP -> TIDYING :**当线程池为空时。
  • TIDYING -> TERMINATED :terminated() 方法执行完成时。

线程池参数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • **corePoolSize :**线程池核心线程个数。
  • workQueue :用于保存等待执行的任务的阻塞队列,比 如 基 于数组的有界ArrayBlockingQueue、基于链表的无界 LinkedBlockingQueue、最多只有一个元素的同步队列 SynchronousQueue 及优先级队列 PriorityBlockingQueue 等。
  • maximunPoolSize :线程池最大线程数量。
  • **ThreadFactory :**创建线程的工厂。
  • RejectedExecutionHandler饱和策略,当队列满并且线程个数达到 maximunPoolSize后采取的策略,比如 AbortPolicy(抛出异常)、 CallerRunsPolicy(使用调用者所在线程来运行任务)、 DiscardOldestPolicy(调用 poll 丢弃一个任务,执行当前任务)及DiscardPolicy(默默丢弃 , 不抛出异常)
  • keeyAliveTime :存活时间。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则这些闲置的线程能存活的最大时间。
  • TimeUnit :存活时间的时间单位。

ThreadPoolExecuor实现原理:

  • ThreadPoolExecutor 的实现实际是一个生产消费模型,当用户添加任务到线程池时相当于生产者生产元素, workers 线程工作集中的线程直接执行任务或者从任务队列里面获取任务时则相当于消费者消费元素。
  • Worker 继承 AQS 和 Runnable 接口,是具体承载任务的对象。 Worker 继承了 AQS,自己实现了简单不可重入独占锁,其中 state=0 表示锁未被获取状态, state=1 表示锁已经被获取的状态, state=–1 是创建 Worker 时默认的状态
  • Worker线程在ThreadPoolExecuor中被封装到一个HashSet中,由mainLock进行控制;
  Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

在这里插入图片描述

execute方法源码:

  1. 获取当前线程池的状态,当前线程池中线程个数是否小于corePoolSize,小于则开启新线程运行addWorker
  2. 如果线程池处于RUNNING状态,则添加任务到阻塞队列,非 RUNNING 状态下是要抛弃新任务的
  3. 如果向任务队列添加任务成功,则对线程池状态进行二次校验,这是因为添加任务到任务队列后,执行代码前有可能线程池的状态已经变化了。这里进行二次校验,如果当前线程池状态不是 RUNNING 了则把任务从任务队列移除,移除后执行拒绝策略;如果二次校验通过,则执行代码重新判断当前线程池里面是否还有线程,如果没有则新增一个线程。
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
         //获取当前线程池的状态+线程个数变量的组合值
        int c = ctl.get();
        //当前线程池中线程个数是否小于corePoolSize,小于则开启新线程运行
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果线程池处于RUNNING状态,则添加任务到阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //如果当前线程池状态不是RUNNING则从队列中删除任务,并执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
                //否则如果当前线程池为空,则添加一个线程
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果队列满,则新增线程,新增失败则执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

添加线程操作,主要分两个部分:

  1. 第一部分双重循环的目的是通过 CAS 操作增加线程数
  2. 第二部分主要是把并发安全的任务添加到 workers 里面,并且启动任务执行
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 当前线程池状态为 STOP、 TIDYING 或 TERMINATED。
            //当前线程池状态为 SHUTDOWN 并且已经有了第一个任务
            //当前线程池状态为 SHUTDOWN 并且任务队列为空。
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                    //使用 CAS 操作增加线程数
                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
            }
        }
		//使用 CAS 成功地增加了线程个数
		/使用全局的独占锁来控制把新增的 Worker 添加到工作集 workers 中
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        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) {
                //如果新增工作线程成功,则启动工作线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

执行Worker线程:
在构造函数内首先设置 Worker 的状态为 –1,这是为了避免当前 Worker 在调用runWorker 方法前被中断(当其他线程调用了线程池的 shutdownNow 时,如果 Worker 状态 >=0 则会中断该线程)

 public void run() {
            runWorker(this);
        }
  • 这里在执行具体任务期间加锁,是为了避免在任务运行期间,其他线程调用了shutdown 后正在执行的任务被中断(shutdown 只会中断当前被阻塞挂起的线程)
  • 执行任务前,会首先将state设置为0,允许中断
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        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();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        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);
        }
    }

**shutdown:**调用 shutdown 方法后,线程池就不会再接受新的任务了,但是工作队列里面的任务还是要执行的。该方法会立刻返回,并不等待队列任务完成再返回

**shutdownNow :**调用 shutdownNow 方法后,线程池就不会再接受新的任务了,并且会丢弃工作队列里面的任务,正在执行的任务会被中断,该方法会立刻返回,并不等待激活的任务执行完成。返回值为这时候队列里面被丢弃的任务列表。

**awaitTermination:**当线程调用 awaitTermination 方法后,当前线程会被阻塞,直到线程池状态变为TERMINATED 才返回,或者等待时间超时才返回。

Future和CompletableFuture

参考熬丙公众号

ForkJoin

大神博客
参考链接

ForkJoin思想:分治思想,fork就说把大问题拆分成小问题,大任务拆成多个子任务,并行执行子任务。Join就是把任务的结果按顺序合并起来。
普通的线程池:

  1. 所有任务均丢到一个AQS同步队列中处理,所有并行任务共用一个堵塞队列
  2. 如果任务队列中的任务存在依赖,其他依赖线程只能堵塞

ForkJoin中采用Work-Stealing解决上述问题:

  • 每个线程均拥有自己的任务队列,基于数组实现的双端队列,相对仅维护一个队列的线程池,有更高的并行度
  • 线程操作自己的任务队列是**LIFO(Last in First out)**模式,从Top中pop出task
  • 线程还可以偷取别的线程任务队列中的任务,模式为FIFO(First in First out),从base中poll/steal
  • 双端任务队列将所属的自己线程的push/pop操作 和 其他线程的steal操作通过不同的模式区分开。这样只有当Base==Top-1时,pop操作和steal操作才会有冲突。
  • steal/poll: steal操作即多个线程从当前线程中pop出Base位置的task(未激活),需要考虑多个线程之间的同步关系。即通过volatile保证Base的可见性,多次读判断 + CAS原子修改的乐观思想来控制并发,保证原子性。
  • push: 添加任务,是任务队列所属的线程才能操作,天生线程安全:不需要通过CAS或锁来保证同步,只需要原子的修改top处任务 和 top向前移一位 就可以了。
  • pop: 考虑pop操作,虽然任务队列所属的线程才能操作,但是当任务队列只有一个任务时,存在steal操作和pop操作的任务竞争。原理就和steal操作一致了,当CAS修改top-1处任务为空 成功时,再更新top值为top-1。
    1.

ForkJoin提交过程:

  • 任务提交到任务队列,包括invoke等所有任务提交方法最终都会调用ForkJoinPool.externalPush方法。
  • ForkJoinPool中双端任务队列是用数组**(volatile WorkQueue[] workQueues)**实现的,其中奇数下标存放的是可激活的任务队列,偶数下标存放的是不可激活的任务队列。激活指的是这个队列是否是某个ForkJoin线程的任务队列。ForkJoinPool.externalPush只能将任务提交到不可激活任务队列。
  • 当提交的任务是pool的第一个任务时,会初始化workQueues,ForkJoinWorkerThread等资源,通过hash算法选择一个偶数下标的workQueue,在TOP处放入任务。同时唤醒ForkJoinWorkerThread开始拉取任务工作。
  • 当提交的任务不是第一个任务,此时workQueues等资源已初始化好。同样需要选择一个偶数下标的workQueue存放任务,如果选中的workQueue只有这一个任务,说明之前线程资源大概率是闲置的状态,会尝试 唤醒(signalWork方法) 一个空闲的ForkJoinWorkerThread开始拉取任务工作。
  • ForkJoinWorkerThread的运行可激活的workQueue自己所属ForkJoinWorkerThread的任务模式是LIFO(Last In First Out),不可激活的workQueue的任务模式是FIFO(First In First Out)
  • ForkJoinWorkerThread刚开始运行时会调用ForkJoinWorkerThread.scan方法随机选取一个队列从Base处捞取任务.捞取到任务会调用WorkQueue.runTask方法执行任务,最终对于RecursiveTask任务执行的是RecursiveTask.exec方法。里面调用的就是我们一开始定义SumTask的实现方法:compute方法。
  • fork所做的事情就是将我们切分的子任务添加到当前ForkJoinWorkerThread自己的workQueue中
  • join所做的事情就是等待子任务的返回结果
  • join时执行任务的判断
    1. 任务未被偷,将执行完的子任务取出来执行即可
    1. 任务被偷,此时自己的任务队列为空,可以帮助小偷执行它未完成的任务假设求和 1-10任务被Thread1执行,fork出两个子任务:1-5 和 6-10。6-10已成功执行完成,join返回了结果。但此时发现1-5被Thread2偷走了,自己的任务队列中已经没有任务可以执行了。此时Thread1可以找到小偷Thread2,并偷取Thread2的10-20任务来帮助它执行。
    1. 任务被偷,此时自己的任务队列不为空,挂起等待被偷走的任务执行结束,并尝试唤醒空闲线程或者创建新的线程替代自己执行队列中的任务

常见题

  1. 利用多线程技术实现生产者-消费者模型

  2. 利用多线程交替打印0-100

参考

参考:《java并发编程之美》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值