重点及完成情况

  • 线程池
  • volatile内存屏障,storeload等四种规则
  • final多线程重排序与实现原理
  • DCL
  • ThreadLocalMap源码
  • condition
  • mutex
  • GC
  • ConcurrentHashMap
  • ReentrantLock。。
  • AQS同步器组件
  • 原子类操作,浮点型的原子操作
  • 循环打印1a2b3c….LockSupport;synchronized;condition;transfreeQueue实现
  • 循环打印3次ABC实现。
  • 两种和三种线程顺序执行的区别

JUC

多线程 - 图1

多线程 - 图2

Unsafe

通过unsafe可以直接操作内存区。该类没有办法通过GC回收。
并且unsafe不用使用提供的getUnsafe方法进行实例化,因为对于jvm来说这是不安全的,
会抛出securitException。
但是可以通过反射进行实例化。
sun.misc.Unsafe提供了可以随意查看及修改JVM中运行时的数据结构的方法。

原子类

为了解决操作的原子性问题,如i++,可以分为三个操作,仅仅使用volatile会存在线程安全问题。

  • 悲观锁的解决方式
    直接加锁,同一时间只有一个线程可以操作i,如synchronized
    但是线程的频繁挂起唤醒会造成严重资源消耗。
  • 乐观锁的解决方式
    默认其他线程不会参与竞争修改,在同步时使用给冲突检测机制,不阻塞线程,自旋反复重试。

分类

基本数据类型:AtomicInteger, AtomicLong, AtomicBoolean

数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

引用类型:AtomicReference,AtomicStampedRerence,AtomicMarkableReference

对象属性修改类型:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater

基本数据类型

使用volatile修饰value,将非原子操作分开执行。

如:

  1. int i = 0;
  2. i++;

使用原子类,会先取得i的初始值O,然后,执行+1操作等到新值N,最后通过cas进行赋值把N值赋给i,再次过程中会进行比较初始值与现在i的初始值,一样才会执行赋值操作,否则自旋。

AQS

多线程 - 图3

AbstractQueuedSynchronizer是用来构建同步组件的基础框架。

主要依靠一个int成员变量(state)来表示同步状态以及通过FIFO(先进先出)队列构成等待队列来实现。

他的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法。

除此之外的其他方法主要实现了排队和阻塞机制。

基本原理

AQS通过维护一个CLH队列来实现资源线程的排队工作。

该队列使用双端双向链表实现。

线程获取资源失败后,会被构造成一个node结点添加到CLH队列中,当前线程会被阻塞在队列中(LockSupport的park方法),当持有资源的线程释放同步状态时,会唤醒后继结点,然后后继结点加入同步状态的竞争中。

两种同步方式:

  1. 独占式
    1. ReentrantLock
  2. 共享式
    1. Semaphore
    2. CountDownLatch
  3. 组合式
    1. ReentantWriteLock

同步器的实现基于模板方法模式(除此之外的模板设计模式应用还有servlet与其实现类)

  1. 重写AQS的指定方法(主要是对共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用重写后的模板方法

两种同步方式及其实现

两种同步方式:

  1. 独占式
    1. ReentrantLock
  2. 共享式
    1. Semaphore
    2. CountDownLatch
  3. 组合式
    1. ReentantWriteLock

Node结点

node结点为AQS中的一个静态内部类。

  • 重点属性
    • 等待状态
      volatile修饰,初始为0
    • 前驱结点
      volatile修饰
    • 后继结点
      volatile修饰
    • 与当前结点关联的排队中的线程
      volatile修饰

      几种等待状态

负值表示结点处于有效等待状态,正值表示结点已被取消。

  • CANCELLED(1)
    表示当前节点已经取消调度,当超时或被中断(相应中断标志时),会触发变更为此状态,进入该状态后的结点将不再变化。
  • SIGNAL(-1)
    表示后继结点在等待当前结点唤醒。后继结点在入队时,会将前驱结点的状态更新为singnal
  • CONDITION(-2)
    表示结点等待在condition上,当其他线程调用了condition的signal方法后,condition的状态结点将从等待对垒转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3)
    共享模式下,前驱结点不仅会唤醒其后继节点,同时也可能唤醒后继的后继结点。
  • 0: 新入队的默认状态。

方法

  • 可重写的方法:
    以下方法不需要全部重写,只需要根据需求重写相应的方法,如独占锁,不用实现共享锁的方法。开发者系需要完成ixie简单的资源状态的获取和释放操作,其余的问题交给AQS完成。

    • tryAcquire

      1. protected boolean tryAcquire(int arg)
      2. //独占式获取同步状态。成功返回true,否则返回false
    • tryRelease

      1. protected boolean tryRelease(int arg)
      2. //独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态。
    • tryAcquireShared

      1. protected int tryAcquireShared(int arg)
      2. //共享式获取同步状态,返回值大于0代表成功,反之失败
    • tryReleaseShared

      1. protected boolean tryReleaseShared(int arg);
      2. //共享式释放同步状态,成功为true,失败为false
    • isHeldExclusively

      1. protected boolean isHeldExclusively()
      2. //是否在独占模式下被线程占用。

独占式

acquire

acquire方法是独占模式下获取共享资源的顶层入口,用来获取同步锁

获取到资源,线程直接返回,否则进入等待队列,直到获取到资源位为止。

整个过程中忽略中断的影响。

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  3. selfInterrupt();
  4. }
  1. final修饰,不能被重写。
  2. 开始尝试直接获取资源,成功直接返回
  3. 不成功,将创造一个结点,通过自旋加入到队尾
  4. 通过自旋判断是否前驱结点为队头,是则尝试获取同步状态,
  5. 否则设置前驱结点的状态为singal后,判断当前线程是否可以休息
  6. 可以则休息等待唤醒,不可以则进行自旋。
  7. 获取到同步状态后,将head指向自己。返回整个过程中是否中断过。
  8. 多线程 - 图4

该线程在整个过程中如果被中断是不响应的,只是返回是否被执行了中断,在获取到了资源后在对中断操作进行相应。

addwaiter方法添加新节点到队尾

将获取资源失败的线程创建一个node对象,然后将该node对象加入到队列的尾部。

插入过程中可能会存在多个线程同时插入到尾部的情况,所以使用cas首先尝试直接插入,插入失败后使用cas自旋插入。

enq方法自旋插入队尾

保证能够线程安全的添加到队尾。

acquireQueued方法处理插入队列中的线程

该方法同样使用了自旋,通过自旋的方式不断判断是否为队头,

队头尝试直接插入,插入成功则返回过程中是否出现中断。

不是队头则判断当前线程能否阻塞休息。

shouldParkAfterFailedAcquire方法 判断该线程是否能够休息

首先获取前驱结点的等待状态。

状态为singal,直接返回true,可以休息。

状态>0(说明线程已经处于cannel状态,已经无效),从后往前遍历,找到状态非cannel的结点,将自己设置为该节点的后续。

找到正常状态的节点后,通过cas将该节点的状态设置为singal。

parkAndCheckInterrupt方法使线程进入阻塞状态,并且返回线程是否中断过。

该方法使用park进入等待状态,并执行中断操作等待unpark唤醒。后续将中断状态改为true;

release
  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {//调用使用者重写的tryRelease方法,若成功,唤醒其后继结点,失败则返回false
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);//唤醒后继结点
  6. return true;
  7. }
  8. return false;
  9. }

释放独占式锁的同步状态。

调用同步器重写实现的tryRelease方法来释放锁,成功唤醒后继结点,失败返回false。

自定义同步器对于tryRelease的实现,一般来说直接剪掉相应量的资源即可,state-=arg,该过程不需要考虑线程安全问题,但是注意返回值是boolean,所以应判断state==0.

unparkSuccessor方法

获取当前结点的状态,小于0则置为0

寻找正常的后继结点(不为空并且状态<0),后继结点不为空则执行唤醒操作(unpark)。

共享式

共享式获取同步状态,同一时刻可以有多个线程同时获取到同步状态。

tryAcquireShared

尝试获取同步资源,需要同步器来重写实现。

其返回结果:

  1. 1. 返回值大于0,表明获取同步状态成功,且还有剩余同步状态可用。
  2. 2. 返回值等于0,表明获取同步状态成功,且没有剩余同步状态了
  3. 3. 返回值小于0,表明获取同步状态失败。

acquireShared

共享式获取同步资源方法

  1. public final void acquireShared(int arg) {
  2. if (tryAcquireShared(arg) < 0)//返回值小于0,获取同步状态失败,排队去;获取同步状态成功,直接返回去干自己的事儿。
  3. doAcquireShared(arg);
  4. }

获取资源失败,执行doAcquireShared,进行排队,获取成功直接放回。

doAcquireShared方法cas获取资源

构造一个共享节点,添加到同步队列尾部,与独占模式一样,cas实现

判断前驱结点是否是头节点,是则尝试获取资源,否则判断时候能够休息方式同独占模式相同。

不同的是在获取资源的时候,独占模式返回的结果是true或false

共享模式返回int类型判断是否可以获取到同步。获取成功就当前结点设置为头结点,如果还有可用资源,传播下去,继续唤醒后继结点。

setHeadAndPropagate方法设置头节点并且唤醒后续结点。

取出队列的头节点后,将自己设置为头节点。判断如果可获取的资源>0或者取出的头为空或者状态正常,那么当前节点的后继结点,如果为空或者是共享状态,则执行释放操作。

releaseShared

共享模式释放同步状态。

  1. public final boolean releaseShared(int arg) {
  2. if (tryReleaseShared(arg)) {
  3. doReleaseShared();//释放同步状态
  4. return true;
  5. }
  6. return false;
  7. }

尝试直接释放锁,失败返回false,成功后唤醒后继结点。

doReleaseShared方法唤醒后继节点。

使用cas,如果头节点不为空且不是队尾,尝试将头节点的状态设置为0,如果不成功,则进行自旋,成功的话将唤醒后继节点。

共享与独占的实现区别

自旋的实现

  1. for (;;) {
  2. Node t = tail;
  3. if (t == null) { // Must initialize
  4. if (compareAndSetHead(new Node()))
  5. tail = head;
  6. } else {
  7. node.prev = t;
  8. if (compareAndSetTail(t, node)) {
  9. t.next = node;
  10. return t;
  11. }
  12. }
  13. }

通过不断循环的方式进行自旋。插入不成功就不断取得最后一个结点尝试插入,直到成功。

模板方法设计模式

父类中实现一个适用于所有情况的模板,在其子类中针对各个类的实际情况,实现自己的不同

公平锁与非公平锁

公平锁:按照线程在队列中的顺序获取锁的资源,先到先得。

非公平锁:线程想要获取资源时,无视队列顺序直接获取,抢到就是得到。

非公平方式如:AQS在实现获取资源时首先直接进行获取资源,而不是直接插入队列,直接获取不到后才进行插入队尾操作。

而公平方式:对应的时候插入队列的线程取得资源的方式。

互斥锁

LockSupport

locakSupport是一个线程阻塞工具类,主要为了实现阻塞和唤醒线程使用。

其所有方法都是静态方法。可以让线程在任意位置阻塞,任意位置唤醒。

与此不同wait和notify只能在同步代码块中执行。

多线程 - 图5

park与unpark方法

park方法实现线程阻塞,unpark实现线程唤醒。

他们之间通过二元信号量(0,1)做的阻塞,可以理解为许可证。

初始状态是0

unpark方法会释放许可证,park会消耗一个许可证。

线程在获取资源时如果不存在许可证,线程会进入等待状态,

如果存在许可证,则消耗一个许可证而获取到资源。

无论执行多少次unpark,许可的数量只能是1

unpark可以先于park调用。

与wait/notify的区别

park、unpark比wait、notify要灵活。解耦了之前的线程之间的同步。同样是阻塞和唤醒操作,但是两者没有交集,park阻塞的线程,notify不能唤醒。

  1. park不需要获取对象的monitor。所以不用必须在同步代码块中调用
  2. notify只能随机唤醒一个线程,而且不能准确唤醒。unpark可以准确唤醒。
  3. park/unpark不会引发死锁。
  4. wait方法会强制让出锁,而park不会

Lock接口

Lock提供比Synchronized更加详细灵活的操作。

Condition

Codition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待通知模式。

线程在调用await方法后执行挂起操作。直到线程等待的某个条件为真时才会被唤醒。不同的条件队列,根据不同的条件阻塞、唤醒队列中的线程。
例如:生产者消费者模式中,多线程对同一个阻塞队列进行操作时,可以创建两个条件队列分别对生产者和消费者进行加锁。当阻塞队列满时,唤醒消费者条件队列中的线程来消费,为空时唤醒生产者条件队列来生产。
与wait/notify最大区别可以根据不同的条件精准的唤醒不同类型的线程。

Condition必须与锁一起使用,Condition实例必须与一个lock绑定。所以Condition一般是作为lock的内部实现。

获取一个condition必须要通过lock的newCondition方法实现。返回一个绑定到此lock下的condition对象。

condition是一个接口,其下只有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS是同步锁的实现基础,所以ConditionObject定义为AQS的内部类。

Condition的方法必须要在lock中执行,类似于wait和notify要在synchronized中执行。

  1. Condition接口可以支持多个等待队列,在前面已经提到一个Lock实例可以绑定多个Condition,所以自然可以支持多个等待队列了
  2. Condition接口支持响应中断,前面已经提到过
  3. Condition接口支持当前线程释放锁并进入等待状态到将来的某个时间,也就是支持定时功能

wait/notify与await/signal的比较

Condtion中的await对应Object的wait;

Condition中的signal对应object的notify;

condition中的notifyAll对应object的notifyAll;

多线程 - 图6

ConditionObject类

  1. public class ConditionObject implements Condition, java.io.Serializable {
  2. private static final long serialVersionUID = 1173984872572414699L;
  3. //头节点
  4. private transient Node firstWaiter;
  5. //尾节点
  6. private transient Node lastWaiter;
  7. public ConditionObject() {
  8. }
  9. /** 省略方法 **/
  10. }

每一个ConditionObject象都包含一个FIFO队列。

FIFO队列由多个Node结点组成,每个结点都包含一个线程引用。

await

调用await方法会使当前线程进入等待状态,同时会将当前线程加入到condition等待队列中并且释放锁,当从await方法返回时,当前线程一定是取得了condition相关联的锁。

signal

唤醒被阻塞的线程,唤醒时从对应的condition队列中唤醒队头获取资源。

Lock方法

尝试获取锁。成功返回,否则阻塞当前线程。

lockInterruptibly() 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常。

tryLock() tryLock(long time , TimeUnit unit)

尝试获取锁,成功返回true,否则返回false

规定时间内获取锁,返回true,否则返回false,期间被中断抛出异常。

ReentrantLock

reentrnatLock为可重入互斥锁,能够对共享资源重复加锁。

支持公平锁和非公平锁两种方式。

其内部主要有三个静态内部类实现。

Sync,NonFairSync,FairSync

Sync是公用的同步组件,继承了AQS。

NonFairSync,FairSync都继承自Sync,分别实现了公平与非公平逻辑。

使用公平锁与非公平锁可以通过创建对象时的传参决定。true使用公平锁,false使用非公平锁,默认false。

  • lock方法和lockInterruptibly方法

线程使用lock方法获取锁时,如果该线程被执行了中断操作,该线程依然会获取锁,在取得锁之后,首先判断是否被中断,然后执行其他操作。而lockInterruptibly方法,如果在获取锁的过程中被执行了中断操作,那么该线程将不再参与获取锁的操作。

NonFairSync

非公平可重入锁

  • 获取锁
    1. 获取同步资源的state状态值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;
    2. 若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;
    3. 其他情况下,获取锁失败,
    4. 获取锁失败会返回到AQS中获取锁失败的逻辑,创建一个新的等待结点,判断是否能够进行休息。。。
  • 释放锁
    释放锁的流程,公平锁与非公平锁相同。都是使用的Sync中的方法。两个实现类并没有重写。
    1. 将state状态值减一,如果state==0,将资源持有线程设为null,返回释放成功。AQS中会处理进行唤醒后续结点。
    2. 如果不为0,返回释放失败,

FairSync

公平可重入锁

  • 获取锁
    1. 获取资源状态,如果为0,进行判断是否存在排在自己之前的未在执行线程。没有则尝试获取资源。
    2. state不为0的情况和非公平锁一致。
  • 释放锁

同非公平锁。

循环打印3次ABC

ReentrantReadWriteLock

可重入读写锁。
读写锁其中包括读锁和写锁。
读锁为共享锁,写锁为独占锁。
在读多写少的情况下,读写锁可以很好的提高效率。

Smaphore

信号量是一个共享锁,可以允许多个线程同时访问资源。
同样支持公平与非公平锁。
创建对象时需要传入信号量大小,最多可以允许多少个线程访问同一资源。
acquire方法获取许可证,release方法驶方资源,增加许可证。smaphore主要来维护许可证的数量。
用于限制某种资源的数量。

阻塞队列

  • 阻塞队列的几个方法 | 方法类型 | 抛出异常 | 返回布尔 | 阻塞 | 超时 | | —- | —- | —- | —- | —- | | 插入 | add(E e) | offer(E e) | put(E e) | offer(E e,Time,TimeUnit) | | 取出 | remove() | poll() | take() | poll(Time,TimeUnit) | | 队首 | element() | peek() | 无 | 无 |

offer方法和put方法通过enqueue方法来实现。
区别在于,offer方法插入失败会返回false,put方法在队列为满时插入会调用condition的await方法进入阻塞状态。put方法在插入时调用的加锁方法是可被中断的。
两个方法在插入之前都会进行加锁。使用的是可重入锁,ReentrantLock。
而add方法调用了put方法进行插入,返回false时会抛出异常。

取出操作同理。

阻塞之后的唤醒使用的是condition条件队列的signal方法。该方法是notify的升级版。

三种常用的阻塞队列

  1. SynchronousQueue : 单个元素的阻塞队列,不存储元素,生产一个消费一个
  2. ArrayBlockingQueue :底层是数组,有界阻塞队列
  3. LinkedBlockingQueue :底层是链表,无界阻塞队列

LinkedBlockingQueue 和ArrayBlockingQueue 的区别

  1. ArrayBlockingQueue,初始化时需要传入数组大小,LinkedBlockingQueue,可以传值,默认为int最大值。
  2. ArrayBlock只有一个锁,LinkedBlock有两个锁,分别插入和移除操作。linkedblock性能会高
  3. array是固定内存,linkedblock是动态内存。

同步队列

各种锁对中断操作的响应

各种锁对阻塞唤醒的支持