多线程
1.线程状态
1.1 线程六种状态

1.2 线程启动终止
- 启动: start
- 终止:
stop: stop方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态- interrupt:优雅的去中断一个线程,在线程中提供了一个 interrupt方法。
- 中断: Thread.interrupt()
- 判断是否中断: Thread.isInterrupted()
- 复位:
- 主动复位: Thread.interrupted()
- 被动复位: 抛出InterruptedException
2.线程安全-锁
2.1 synchronized关键字
2.1.1 synchronized使用
- 修饰实例方法,给当前实例加锁;
- 静态方法,给类加锁;
- 修饰代码块:
- 如果锁是 this 或 object 对象,作用域是当前对象
- 如果锁是 类名.class,作用域是全局的

2.1.2 synchronized原理
每一个 JAVA 对象都会与一个监视器 monitor 关联,我们 可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先 获取到 synchronized 修饰的对象对应的 monitor。 monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程 可以尝试去获得这个监视器。
monitor 依赖操作系统的 MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

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

调用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}{头标元实齐,哈分同偏线}$

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


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。

3.可见性和有序性
3.1 可见性
volatile
volatile 可以使得在多处理器环境下保证了共享变量的可见性
3.2 有序性
2. AQS队列
head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点
2.1 参数说明
2.1.1 state
- 当 state=0 时,表示无锁状态;
- 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0,其他线程才有资格获得锁。

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
重入锁加锁流程(非公平锁)
- 直接通过cas获取锁,获取成功设置当前线程独占锁;
- cas获取失败,通过acquire(1)方法获取;
- 逻辑判断 boolean t=!tryAcquire(1)&&acquireQueued(addWaiter(Node.EXCLUSIVE), 1)
- 调用tryAcquire方法,接着调用nonfairTryAcquire方法;
- 判断方式,同步标记state,state==0?(cas获取锁成功?true:false):(当前线程持有锁?重入->true:false);
- 通过acquireQueued竞争锁
- addWaiter封装为exclusive类型节点node,然后加入到双向链表尾部
- 获取node前一个节点p,如果p为head并且tryAcquire成功,设置node为head
- 如果上一步失败,判断当前线程是否应该中断并中断
- 判断逻辑:节点p的状态ws:如果ws=-1,则需要中断;ws>0,跳过p判断它前一个,直到找到一个ws<=0的节点,然后丢弃中间的节点;否则,cas将当前节点状态设为-1,不中断;
- 中断方式: LockSupport.park(this)
- 调用tryAcquire方法,接着调用nonfairTryAcquire方法;
- 如果 t==true表示尝试之后也没有获取到锁, 中断当前线程,Thread.currentThread().interrupt();
2.3 Reentrant#unlock
- 调用release(1)来释放锁,实际调用tryRelease(1)
- tryRelease: 获取state,重入次数减1,如果结果为0,则将排它锁线程设置为null,返回true
- 如果第2步返回true,则使用unparkSuccessor唤醒下一个waitStatus<0的节点;
- 在调用之前需判断head节点存在,且waitStatus!=0
2.4 Condition#await
- 将当前线程封装为condition类型节点,加到等待队列中(单向链表)
- 完全释放当前线程持有的锁,并唤醒AQS队列中的一个线程
- 如果当前线程没有在AQS队列上,即没有被signal,将当前线程阻塞
- 当这个线程醒来, 通过acquireQueued获取锁, 当返回false表示拿到了锁
- 如果等待队列中下一个节点不是 null, 则清理等待队列上状态不是condition的节点
- 如果线程被中断了,需要抛出异常或者什么都不做
2.5 Condition#signal
- 先判断当前线程是否获取了锁
- 从等待队列中头部开始找到第一个condition状态的节点t,执行transferForSignal操作
- 更新节点t的状态为0,如果更新失败,只有一种可能就是节点被 CANCELLED 了
- 调用 enq,把节点t添加到 AQS 队列。并且返回前一个节点p,也就是原 tail 节点
- 如果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}{核最时位阻场拒}$

3.2 类型
- newFixedThreadPool:固定线程数量的线程池,线程数不变。当有任务提交时,若线程池中空闲则立即执行,若没有则会被暂缓在一个任务队列中,等待有空闲的线程去执行;
- newSingleThreadExecutor: 单线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中;
- newCachedThreadPool:线程数量可调整的线程池,不限制最大线程数量,若有空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收;
- newScheduledThreadPool: 可以指定线程的数量的线程池,这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
3.3 说明
- newFixedThreadPool
- 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务;
- 线程数等于核心线程数后,将任务加入阻塞队列;
- 由于队列容量非常大,可以一直添加;
- 执行完任务的线程反复去队列中取任务执行。
- newCachedThreadPool
- 没有核心线程,直接向 SynchronousQueue 中提交任务;
- 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个;
- 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收;
- newSingleThreadExecutor
- 只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序
3.4 拒绝策略
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- 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失败->唤醒线程)
线程基础
- 线程与进程的区别 区隔稳性粒
- 线程的创建 Thread Runnable Callable 线程池 Runnable&Callable对比 Callable允许返回值、抛出异常,Runnable不允许返回值、不能抛出异常
- 线程的生命周期 NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED 状态流转
- 线程的基本操作 start stop yield sleep join interrupt isInterrupted interrupted
阻塞队列
- 操作方法: 插入、删除、获取: 抛返阻超(插 add/offer/put/offer、删 remove/poll/take/poll、获 element/peek)
- 实现类 ArrayBlockingQueue(大小不变) LinkedBlockingQueue(先进先出) DelayQueue(Delayed接口,大小无限制) PriorityBlockingQueue(优先级,Comparator) SynchronousQueue(每个 put 必须等待一个 take,反之亦然)
- put流程 获取lock锁-> 判断队列是否已满:已满->notFull.await,等待唤醒;未满->调用enqueue入队-> notEmpty.signal
- take流程 获取lock锁-> 判断队列是否已空:已空->notEmpty.await,等待唤醒;未空->调用dequeue出队-> notFull.signal
线程池
- 线程池的参数 corePoolSize maximumPoolSize keepAliveTime unit workQueue threadFactory handler
- 线程池的状态 RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
- 线程池任务流程 核心线程->任务队列->非核心线程->拒绝策略
- 线程池中线程如何复用 工作线程worker使用while循环从队列获取任务,take方法获取不到就阻塞,poll方法获取不到就回收,核心线程使用take,非核心使用poll,allowCoreThreadTimeOut=true时,核心也是用poll; Worker->runWork->while->getTask->boolean timed=allowCoreThreadTimeOut || wc>corePoolSize->poll&take
- 线程池的关闭 shutdown shutdownNow awaitTermination
- 线程池类型 newCachedThreadPool(SynchronousQueue,c=0) newFixedThreadPool(LinkedBlockingQueue,c=m) newSingleThreadExecutor(LinkedBlockingQueue,c=m=1) newScheduledThreadPool(DelayQueue)
- 任务提交 submit&execute,二者区别: 参数、返回值、异常
- 钩子方法 beforeExecute afterExecute terminated
- 拒绝策略 AbortPolicy(默认,拒绝并抛异常) DiscardPolicy(丢弃任务) DiscardOldestPolicy(丢弃最老) CallerRunsPolicy(调用者执行) 自定义(RejectedExecutionHandler)
- 使用线程池的好处 降低资源消耗 提高响应速度 提高线程的管理性
- 线程池线程数确定 IO密集型任务(核心线程数 = CPU核数 * 2) CPU密集型任务(核心线程数 = CPU核数 + 1) 混合型任务
ThreadLocal
线程安全
- 线程安全三要素 原子性、可见性、有序性
- 硬件层面的可见性 MESI协议 Store Bufferes
- 线程安全问题产生原因: 可见性(JMM内存模型和Java内存区域)和有序性(重排序: 编译器+指令+内存)
- happens-before规则 顺序、传递、volatile、start、join、监视器
- synchronized使用 实例方法 静态方法 代码块
- synchronized存储 头标元实齐 哈分同偏线
- 偏向锁 原理 判断对象头和栈帧中的锁记录偏向的线程id, cas
- 轻量级锁 原理 普通自旋和自适应自旋
- 重量级锁 EntryList WaitSet monitorenter monitorexit https://blog.csdn.net/qq_43783527/article/details/114669174
- 锁升级 无锁->偏向锁->轻量级锁->重量级锁
- 偏向锁标识==1&锁标志位==01->判断是否可偏向,如可则判断锁记录线程ID==抢锁线程ID->获得偏向锁,执行临界区代码;
- 锁记录线程ID!=抢锁线程ID,通过CAS竞争:
- 竞争成功: 设置参数(偏向锁标识=1、锁标志位=01、锁记录线程ID=抢锁线程ID)->获得偏向锁,执行临界区代码;
- 竞争失败: 说明发生了竞争,撤销偏向锁,升级为轻量级锁;
- 使用CAS将锁对象的MarkWord替换为抢锁线程的锁记录指针:
- 替换成功: 获得锁,执行代码;
- 替换失败: 使用自旋锁尝试,如果成功依然是轻量级锁;自旋失败升级为重量级锁,线程阻塞;
- 使用CAS将锁对象的MarkWord替换为抢锁线程的锁记录指针:
- wait/notify原理,wait和sleep区别(指定时间、释放cpu、同步块中使用)
CAS
- CAS原理 乐观锁机制 V(var)、E(exp)、N(new)
- CAS问题 ABA问题(AtomicStampedReference、AtomicMarkableReference)、只能操作一个共享变量(锁、AtomicReference)
- LongAdder原理
AQS
- AQS核心参数
state: state=0-无锁,state>0多次获取了锁;
waitStatus: CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0) - CLH线程同步队列(双向队列) 、WAIT等待线程队列(单向队列)
- AQS中钩子方法 tryAcquire、 tryRelease、 tryAcquireShared、 tryReleaseShared、 isHeldExclusively
- AQS抢占锁流程 ①AQS模板方法: acquire(arg) -> ②钩子实现: tryAcquire(arg) -> ③直接入队: addWaiter -> ④自旋入队: enq -> ⑤自旋抢占: acquireQueued() -> ⑥挂起预判: shouldParkAfterFailedAcquire() -> ⑦线程挂起: parkAndCheckInterrupt()
- AQS释放锁流程 AQS模板方法: release()-> 钩子实现: tryRelease()-> 唤醒后继: unparkSuccessor()
- Condition#await 封装节点->加入WAIT队列->释放锁(完全)->判断是否在CLH队列:是->自旋获取锁,否->阻塞自己,等待被唤醒并加入CLH队列竞争锁
- Condition#signal 判断是否持有锁->从WAIT队列移除当前头结点->加入到CLH队列->设置CLH原尾结点Signal失败->唤醒线程
并发工具及容器
- 不同类型锁 ReentrantLock ReentrantReadWriteLock StampedLock
- 通信工具类 Semaphore Exchanger CountDownLatch CyclicBarrier Phaser
- 并发容器 CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentSkipListSet ConcurrentHashMap ConcurrentSkipListMap 各种BlockingQueue
- Fork/Join 工作窃取算法 每个工作线程维护一个工作队列
- 异步回调 FutureTask、FutureCallback&ListenableFuture(Guava)、GenericFutureListener&ChannelFuture(Netty)
- CompletableFuture thenRun()&thenApply()、thenCombine()&runAfterBoth()、runAfterEither()