跳至主要內容

多线程

soulballad总结文字总结记忆文字总结约 5392 字大约 18 分钟

1.线程状态

1.1 线程六种状态

img

1.2 线程启动终止

  • 启动: start
  • 终止:
    • stop: stop方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态
    • interrupt:优雅的去中断一个线程,在线程中提供了一个 interrupt方法。
      • 中断: Thread.interrupt()
      • 判断是否中断: Thread.isInterrupted()
  • 复位:
    • 主动复位: Thread.interrupted()
    • 被动复位: 抛出InterruptedException

2.线程安全-锁

2.1 synchronized关键字

2.1.1 synchronized使用

  1. 修饰实例方法,给当前实例加锁;
  2. 静态方法,给类加锁;
  3. 修饰代码块:
    1. 如果锁是 this 或 object 对象,作用域是当前对象
    2. 如果锁是 类名.class,作用域是全局的
      img

2.1.2 synchronized原理

每一个 JAVA 对象都会与一个监视器 monitor 关联,我们 可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先 获取到 synchronized 修饰的对象对应的 monitor。 monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程 可以尝试去获得这个监视器。

monitor 依赖操作系统的 MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

img

2.1.3 wait/notify

  • wait:表示持有对象锁的线程 A 准备释放对象锁权限,释放 cpu 资源并进入等待状态。
  • notify:表示持有对象锁的线程 A 准备释放对象锁权限,通知 jvm 唤醒某个竞争该对象锁的线程 X。线程 A synchronized 代码执行结束并且释放了锁之后,线程 X 直接获得对象锁权限,其他竞争线程继续等待(即使线程 X 同步完毕,释放对象锁,其他竞争线程仍然等待,直至有新的 notify,notifyAll 被调用)。
  • notifyAll:notifyall 和 notify 的区别在于,notifyAll 会唤醒所有竞争同一个对象锁的所有线程,当已经获得锁的线程A 释放锁之后,所有被唤醒的线程都有可能获得对象锁权限。

img

调用wait方法,首先会​$\color{red}{获取监视器锁​}$,获得成功以后,会让当前线程$\color{red}{进入等待状态进入等待队列}$​​并且$\color{red}{​释放锁}$​;

然后 当其他线程调用​$\color{red}{notify或者notifyall}$​以后,会选择从​$\color{red}{等待队列}$中​唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要​​$\color{red}{等到当前的线程执行完按monitorexit指令以后}$,也就是​锁被释​放以后,处于$\color{red}{等待队列中的线程就可以开始竞争锁}$​​了。

2.2 锁的存储

在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域: 对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

​$\color{red}{头标元实齐,哈分同偏线}$​

img

Mark word 记录了对象和锁有关的信息,当某个对象被 synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。Mark Word里面存储的数据会随着锁标志位的变化而变化,Mark Word 可能变化为存储以下 5 种情况。

img

img

2.3 锁的升级

2.3.1 锁的类型

2.3.1.1 无锁

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

2.3.1.2 偏向锁

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

2.3.1.3 轻量级锁

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

2.3.1.4 重量级锁

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源。

2.3.2 锁升级过程

  • 无锁到偏向锁
    • 初次执行到synchronized代码块的时候,锁对象变成偏向锁,通过CAS修改对象头里的锁标志位。
  • 偏向锁到轻量级锁
    • 轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁
  • 轻量级锁到重量级锁
    • 如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁,依然是CAS修改锁标志位,但不修改持有锁的线程ID。

img

3.可见性和有序性

3.1 可见性

volatile

volatile 可以使得在多处理器环境下保证了共享变量的可见性

3.2 有序性

2. AQS队列

head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点

2.1 参数说明

2.1.1 state

  1. 当 state=0 时,表示无锁状态;
  2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0,其他线程才有资格获得锁。

img

2.1.2 waitStatus

Node 有 5 中状态,分别是:CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0)

  • CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状态后的结点将不会再变化;
  • SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程;
  • CONDITION: 和 Condition 有关系,后续会讲解;
  • PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态;
  • 0:初始状态

2.2 Reentrant#lock

重入锁加锁流程(非公平锁)

  1. 直接通过cas获取锁,获取成功设置当前线程独占锁;
  2. cas获取失败,通过acquire(1)方法获取;
  3. 逻辑判断 boolean t=!tryAcquire(1)&&acquireQueued(addWaiter(Node.EXCLUSIVE), 1)
    1. 调用tryAcquire方法,接着调用nonfairTryAcquire方法;
      1. 判断方式,同步标记state,state==0?(cas获取锁成功?true:false):(当前线程持有锁?重入->true:false);
    2. 通过acquireQueued竞争锁
      1. addWaiter封装为exclusive类型节点node,然后加入到双向链表尾部
      2. 获取node前一个节点p,如果p为head并且tryAcquire成功,设置node为head
      3. 如果上一步失败,判断当前线程是否应该中断并中断
        1. 判断逻辑:节点p的状态ws:如果ws=-1,则需要中断;ws>0,跳过p判断它前一个,直到找到一个ws<=0的节点,然后丢弃中间的节点;否则,cas将当前节点状态设为-1,不中断;
        2. 中断方式: LockSupport.park(this)
  4. 如果 t==true表示尝试之后也没有获取到锁, 中断当前线程,Thread.currentThread().interrupt();

2.3 Reentrant#unlock

  1. 调用release(1)来释放锁,实际调用tryRelease(1)
  2. tryRelease: 获取state,重入次数减1,如果结果为0,则将排它锁线程设置为null,返回true
  3. 如果第2步返回true,则使用unparkSuccessor唤醒下一个waitStatus<0的节点;
  4. 在调用之前需判断head节点存在,且waitStatus!=0

2.4 Condition#await

  1. 将当前线程封装为condition类型节点,加到等待队列中(单向链表)
  2. 完全释放当前线程持有的锁,并唤醒AQS队列中的一个线程
  3. 如果当前线程没有在AQS队列上,即没有被signal,将当前线程阻塞
  4. 当这个线程醒来, 通过acquireQueued获取锁, 当返回false表示拿到了锁
  5. 如果等待队列中下一个节点不是 null, 则清理等待队列上状态不是condition的节点
  6. 如果线程被中断了,需要抛出异常或者什么都不做

2.5 Condition#signal

  1. 先判断当前线程是否获取了锁
  2. 从等待队列中头部开始找到第一个condition状态的节点t,执行transferForSignal操作
  3. 更新节点t的状态为0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
  4. 调用 enq,把节点t添加到 AQS 队列。并且返回前一个节点p,也就是原 tail 节点
  5. 如果p的状态是取消了, 或者尝试设置上一个节点的状态为 SIGNAL 失败,唤醒节点t的线程

2.6 阻塞队列操作

  • add(e) :添加元素到队列中,如果队列满了,继续插入元素会报错,IllegalStateException;
  • offer(e) : 添加元素到队列,返回是否插入成功,如果成功则返回 true;如果满了返回false;
  • put(e) :当阻塞队列满了以后,生产者继续通过 put添加元素,队列会一直阻塞生产者线程,直到队列可用;
  • poll(): 当队列中存在元素,则从队列中取出一个元素,如果队列为空,则直接返回 null;
  • remove():当队列为空时,调用 remove 会返回 false,如果元素移除成功,则返回 true;
  • take():基于阻塞的方式获取队列中的元素,如果队列为空,则 take 方法会一直阻塞,直到队列中有新的数据可以消费;

3. 线程池

3.1 执行原理

$\color{red}{核最时位阻场拒}$

img

3.2 类型

  1. newFixedThreadPool:固定线程数量的线程池,线程数不变。当有任务提交时,若线程池中空闲则立即执行,若没有则会被暂缓在一个任务队列中,等待有空闲的线程去执行;
  2. newSingleThreadExecutor: 单线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中;
  3. newCachedThreadPool:线程数量可调整的线程池,不限制最大线程数量,若有空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收;
  4. newScheduledThreadPool: 可以指定线程的数量的线程池,这个线程池还带有延迟和周期性执行任务的功能,类似定时器。

3.3 说明

  1. newFixedThreadPool
    1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务;
    2. 线程数等于核心线程数后,将任务加入阻塞队列;
    3. 由于队列容量非常大,可以一直添加;
    4. 执行完任务的线程反复去队列中取任务执行。
  2. newCachedThreadPool
    1. 没有核心线程,直接向 SynchronousQueue 中提交任务;
    2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个;
    3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收;
  3. newSingleThreadExecutor
    1. 只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序

3.4 拒绝策略

  1. AbortPolicy:直接抛出异常,默认策略;
  2. CallerRunsPolicy:用调用者所在的线程来执行任务;
  3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4. DiscardPolicy:直接丢弃任务。

4. 总结

归纳

基阻池安CA工(基础+阻塞队列+线程池+安全+CAS+AQS+工具/容器)
区隔稳性粒
创TRC池C返抛
生NRBWTT(NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED)
方S3YJI3(start/stop/sleep+yield+join+interrupt/isInterrupted/interrupted)

方抛返阻超 aopo+rptp+ep(插 add/offer/put/offer、删 remove/poll/take/poll、获 element/peek)
类ALDPS
P锁满是fw否入es
t锁空是ew否出fs

核核最时位阻场拒
状RSPDT(RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED)
流核任非拒(核心线程->任务队列->非核心线程->拒绝策略)
复WRHGTPK(Worker/runWorker/While/getTask/timed/poll?take)
关SNA(shutdown shutdownNow awaitTermination)
类C(Sc0)F(Lcm)G(Lcm1)S cached/fixed/single/scheduled
提SE参返异(submit/execute 区别: 参数+返回值+异常处理)
钩BAT(beforeExecute afterExecute terminated)
拒ADOCR(AbortPolicy/DiscardPolicy/DiscardOldestPolicy/CallerRunsPolicy/RejectedExecutionHandler)
好降响管数I2C1(好处:降低资源消耗+提高响应速度+提高线程的管理性, 线程数: IO密集=核心数*2 CPU密集=核心数+1)

三原可序MESI
顺外传开教监
S实静代(实例方法 静态方法 代码块)
头标元实齐 哈分同偏线
升 无锁->偏向锁->轻量级锁->重量级锁 

AswCSDP0(state: state=0-无锁,state>0多次获取了锁;waitStatus: CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0))
抢ataeasp(①acquire(arg)->②tryAcquire(arg)->③addWaiter->④自旋入队:enq->⑤自旋抢占: acquireQueued()->⑥shouldParkAfterFailedAcquire()->⑦parkAndCheckInterrupt())
放rtu(release()-> 钩子实现: tryRelease()-> 唤醒后继: unparkSuccessor())
a封加释判是旋否阻(封装节点->加入WAIT队列->释放锁(完全)->判断是否在CLH队列:是->自旋获取锁,否->阻塞自己,等待被唤醒并加入CLH队列竞争锁)
s判移加设换(判断是否持有锁->从WAIT队列移除当前头结点->加入到CLH队列->设置CLH原尾结点Signal失败->唤醒线程)

线程基础

  1. 线程与进程的区别 区隔稳性粒
  2. 线程的创建 Thread Runnable Callable 线程池 Runnable&Callable对比 Callable允许返回值、抛出异常,Runnable不允许返回值、不能抛出异常
  3. 线程的生命周期 NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED 状态流转
  4. 线程的基本操作 start stop yield sleep join interrupt isInterrupted interrupted

阻塞队列

  1. 操作方法: 插入、删除、获取: 抛返阻超(插 add/offer/put/offer、删 remove/poll/take/poll、获 element/peek)
  2. 实现类 ArrayBlockingQueue(大小不变) LinkedBlockingQueue(先进先出) DelayQueue(Delayed接口,大小无限制) PriorityBlockingQueue(优先级,Comparator) SynchronousQueue(每个 put 必须等待一个 take,反之亦然)
  3. put流程 获取lock锁-> 判断队列是否已满:已满->notFull.await,等待唤醒;未满->调用enqueue入队-> notEmpty.signal
  4. take流程 获取lock锁-> 判断队列是否已空:已空->notEmpty.await,等待唤醒;未空->调用dequeue出队-> notFull.signal

线程池

  1. 线程池的参数 corePoolSize maximumPoolSize keepAliveTime unit workQueue threadFactory handler
  2. 线程池的状态 RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
  3. 线程池任务流程 核心线程->任务队列->非核心线程->拒绝策略
  4. 线程池中线程如何复用 工作线程worker使用while循环从队列获取任务,take方法获取不到就阻塞,poll方法获取不到就回收,核心线程使用take,非核心使用poll,allowCoreThreadTimeOut=true时,核心也是用poll; Worker->runWork->while->getTask->boolean timed=allowCoreThreadTimeOut || wc>corePoolSize->poll&take
  5. 线程池的关闭 shutdown shutdownNow awaitTermination
  6. 线程池类型 newCachedThreadPool(SynchronousQueue,c=0) newFixedThreadPool(LinkedBlockingQueue,c=m) newSingleThreadExecutor(LinkedBlockingQueue,c=m=1) newScheduledThreadPool(DelayQueue)
  7. 任务提交 submit&execute,二者区别: 参数、返回值、异常
  8. 钩子方法 beforeExecute afterExecute terminated
  9. 拒绝策略 AbortPolicy(默认,拒绝并抛异常) DiscardPolicy(丢弃任务) DiscardOldestPolicy(丢弃最老) CallerRunsPolicy(调用者执行) 自定义(RejectedExecutionHandler)
  10. 使用线程池的好处 降低资源消耗 提高响应速度 提高线程的管理性
  11. 线程池线程数确定 IO密集型任务(核心线程数 = CPU核数 * 2) CPU密集型任务(核心线程数 = CPU核数 + 1) 混合型任务

ThreadLocal

线程安全

  1. 线程安全三要素 原子性、可见性、有序性
  2. 硬件层面的可见性 MESI协议 Store Bufferes
  3. 线程安全问题产生原因: 可见性(JMM内存模型和Java内存区域)和有序性(重排序: 编译器+指令+内存)
  4. happens-before规则 顺序、传递、volatile、start、join、监视器
  5. synchronized使用 实例方法 静态方法 代码块
  6. synchronized存储 头标元实齐 哈分同偏线
  7. 偏向锁 原理 判断对象头和栈帧中的锁记录偏向的线程id, cas
  8. 轻量级锁 原理 普通自旋和自适应自旋
  9. 重量级锁 EntryList WaitSet monitorenter monitorexit https://blog.csdn.net/qq_43783527/article/details/114669174
  10. 锁升级 无锁->偏向锁->轻量级锁->重量级锁
    • 偏向锁标识==1&锁标志位==01->判断是否可偏向,如可则判断锁记录线程ID==抢锁线程ID->获得偏向锁,执行临界区代码;
    • 锁记录线程ID!=抢锁线程ID,通过CAS竞争:
      • 竞争成功: 设置参数(偏向锁标识=1、锁标志位=01、锁记录线程ID=抢锁线程ID)->获得偏向锁,执行临界区代码;
      • 竞争失败: 说明发生了竞争,撤销偏向锁,升级为轻量级锁;
        • 使用CAS将锁对象的MarkWord替换为抢锁线程的锁记录指针:
          • 替换成功: 获得锁,执行代码;
          • 替换失败: 使用自旋锁尝试,如果成功依然是轻量级锁;自旋失败升级为重量级锁,线程阻塞;
  11. wait/notify原理,wait和sleep区别(指定时间、释放cpu、同步块中使用)

CAS

  1. CAS原理 乐观锁机制 V(var)、E(exp)、N(new)
  2. CAS问题 ABA问题(AtomicStampedReference、AtomicMarkableReference)、只能操作一个共享变量(锁、AtomicReference)
  3. LongAdder原理

AQS

  1. AQS核心参数
    state: state=0-无锁,state>0多次获取了锁;
    waitStatus: CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0)
  2. CLH线程同步队列(双向队列) 、WAIT等待线程队列(单向队列)
  3. AQS中钩子方法 tryAcquire、 tryRelease、 tryAcquireShared、 tryReleaseShared、 isHeldExclusively
  4. AQS抢占锁流程 ①AQS模板方法: acquire(arg) -> ②钩子实现: tryAcquire(arg) -> ③直接入队: addWaiter -> ④自旋入队: enq -> ⑤自旋抢占: acquireQueued() -> ⑥挂起预判: shouldParkAfterFailedAcquire() -> ⑦线程挂起: parkAndCheckInterrupt()
  5. AQS释放锁流程 AQS模板方法: release()-> 钩子实现: tryRelease()-> 唤醒后继: unparkSuccessor()
  6. Condition#await 封装节点->加入WAIT队列->释放锁(完全)->判断是否在CLH队列:是->自旋获取锁,否->阻塞自己,等待被唤醒并加入CLH队列竞争锁
  7. Condition#signal 判断是否持有锁->从WAIT队列移除当前头结点->加入到CLH队列->设置CLH原尾结点Signal失败->唤醒线程

并发工具及容器

  1. 不同类型锁 ReentrantLock ReentrantReadWriteLock StampedLock
  2. 通信工具类 Semaphore Exchanger CountDownLatch CyclicBarrier Phaser
  3. 并发容器 CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentSkipListSet ConcurrentHashMap ConcurrentSkipListMap 各种BlockingQueue
  4. Fork/Join 工作窃取算法 每个工作线程维护一个工作队列
  5. 异步回调 FutureTask、FutureCallback&ListenableFuture(Guava)、GenericFutureListener&ChannelFuture(Netty)
  6. CompletableFuture thenRun()&thenApply()、thenCombine()&runAfterBoth()、runAfterEither()
上次编辑于:
贡献者: soulballad