多线程
关键字
volatile
保证内存可见性 和 防止指令重排
内存屏障 2 个作用: 禁止重排 和 确保缓存与主存一致
- Load 屏障 加载主存
- Store 屏障 回刷主存
LL | volatile读 | LS
SS | volatile写 | SLJMM 内存模型
线程可以将变量保存在本地内存(如 CPU 的寄存器), 而不是主存(共享内存). 产生不一致问题 volatile 防止 JVM 指令重排&&保证变量的可见性
并发 3 特征: 原子性 可见性 有序性
原子性: 多个指令在监视器锁下原子执行
总线
- 总线嗅探: 每个处理器通过嗅探总线上传播的数据检查自己的缓存是否过期, 如果处理器发现自己缓存行对应的内存地址被修改, 会将处理器的缓存行设置为无效状态,并从主内存重新获取数据.
- 总线风暴: 由于 volatile 的 mesi 缓存一致性协议需要不断的从主内存嗅探和 cas 不断循环无效交互导致总线带宽达到峰值 解决办法:部分 volatile 和 cas 使用 synchronized
synchronized
解决多个线程之间访问资源的同步性, 保证它所修饰的方法或者代码块在任意时刻只能有一个线程执行.
1.6 之前是重量级锁, 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态转换需要相对比较长的时间
三种使用方式:
修饰方法: 1.1 实例方法: 相当于对当前对象加锁, 需要获得对象锁 1.2 静态方法: 相当于给当前类对象加锁, 需要获得类锁 同步方法使用 ACC_SYNCHRONIZED 标识, JVM 检测到之后进行同步调用.
修饰代码块: 指定加锁对象, 需要获得相对应的对象锁或者类锁
通过查看字节码信息 javap -c -s -v -l SynchronizedDemo.class
可以看出同步代码块使用 monitorenter(指向开始) 和 monitorexit(指向结束) 指令, 执行 enter 时尝试持有对象监视器 monitor
(HotSpot 中每个对象都内置一个 ObjectMonitor 对象//HotSpot 中,Monitor 是基于 C++ 实现的)
执行 enter 时尝试获取对象锁, 锁计数器为 0 表示可获取, 获取后锁计数器+1
执行 exit 后将锁计数器设为 0, 表示锁已释放.
本质都是对对象监视器 monitor 的获取.
synchronized 锁升级
无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态 锁升级过程:无锁状态,持有偏向锁的线程二次进入也不需要重新获取锁,等出现锁竞争的时候会变成轻量级锁,轻量级锁通过CAS 自旋处理锁竞争,自旋次数到达一定程度以后变成重量级锁,使用操作系统的监视器锁 1.6 后 synchronized 优化 https://www.cnblogs.com/wuqinglong/p/9945618.html
volatile 和 synchronized 的区别
修饰对象不同, 修饰变量 和 修饰方法&代码块
解决变量在多个线程之间的可见性 解决多个线程之间访问共享资源的同步性
线程
线程状态
| 状态 | 说明 |
|---|---|
| NEW | 线程构建后未调用 start() |
| RUNNABLE | 就绪&运行 统称 runnable |
| BLOCKED | 阻塞于锁 |
| WAITING | 等待,等待其他线程通知或中断 |
| TIME_WAITING | 超时等待 |
| TERMINATED | 终止,线程执行完毕 |
状态转移
状态都是跟 Runnable 做转移 Object::wait Thread::join 主线程等待 thread1 join LockSupport.park()
wait() sleep()
- this.wait() 释放对象锁,并进入等待
- Thread.sleep() 不释放锁, 进入等待 wait() notify()只有 syncronized 内部可调用, 只能由 monitor 锁当前持有的对象调用, 否则 IllegalMonitorStateException
线程中断
interrupt 优雅地中断线程是一种艺术。类似一种标志位,让线程自己中断。
- interrupt 中断操作时,非自身打断需要先检测是否有中断权限,这由 jvm 的安全机制配置;
- 如果线程处于 sleep wait join 等状态,那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常;
- 如果线程处于 I/O 阻塞状态,将会抛出 ClosedByInterruptException(IOException 的子类)异常;
- 如果线程在 Selector 上被阻塞,select 方法将立即返回;
- 如果非以上情况,将直接标记 interrupt 状态;
Future::cancel ExecutorService::shutdown
死锁
一组线程被阻塞了,等待一个永远不会为真的条件。每个线程都在等待其他线程执行一个不可能执行的操作。 如何避免:给定所有互斥操作的一个全序,如果每个线程都以一种顺序获得互斥锁并且以相反的顺序释放,就不会死锁。(CSAPP)
产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系。
线程池
- 构成: 核心线程数 最大线程数 阻塞队列 饱和策略
- 构造函数
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //存活时间
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //工厂
RejectedExecutionHandler handler //拒绝策略
);运行机制: 核心线程满-> 阻塞队列满-> 线程池满-> 拒绝策略
另外, 如果核心线程数没满, 新任务会直接创建新的核心线程, 而不是复用核心线程
阻塞队列
- 无界队列
- DelayQueue 元素有有效期
- LinkedBlockingQueue
- LinkedTransferQueue 对于每个生产者是 FIFO
- PriorityBlockingQueue 优先队列 指定初始容量也会扩容
- 有界队列
- ArrayBlockingQueue
- LinkedBlockingQueue
- 同步移交(相当于没有队列) 条件是有线程在等待接收队列中的元素; 如果没有线程等待接收, 那么会创建新的线程处理; 然而又超过了最大的线程数限制那会根据饱和策略丢弃;
- SynchronousQueue
- 无界队列
拒绝策略:
- CallerRunsPolicy: 交由调用方执行
- AbortPolicy: 抛出 RejectedExecutionException 异常
- DiscardPolicy: 丢弃
- DiscardOldestPolicy: 丢弃最早未处理的任务
线程超时会被标记为可回收, 超过核心线程数, 被标记的会被中止.
注意这里的核心线程池里的线程不会被标记, 可以通过设置核心线程可超时处理
线程数设置:
计算密集型的 CPU 核心数+1;+1 为了防止缺页中断等造成任务暂停
线程池运行状态 5 种
| 状态 | 描述 |
|---|---|
| running | 可接任务,也可处理队列 |
| shutdown | 不接任务,仍可处理队列 |
| stop | 中断正在处理的线程 |
| tidying | 所有任务终止,有效线程数 0 |
| terminated | 结束 |
shutdown(): status -> shutdown shutdownNow(): shutdown 并且返回等待队列中的任务列表, 但是对执行中的任务不能保证立刻停止, 例如使用 Thread.interrupt 但是无响应 awaitTermination(): shutdown() 之后判断是否结束
isTerminated() shutdown 后 任务完成返回 true; 必须先调用 shutdown
Runnable Callable 区别
需要返回结果或抛出异常,用 callable.
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
适配器模式 Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object resule)
execute()方法和 submit()方法的区别
Executor::execute 方法用于提交不需要返回值的任务,所以无法判断任务是否执行成功 ExecutorService::submit 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功
与上面的 Callable 配合使用
ThreadLocal
copy value into every thread 设计的初衷是跨线程传递数据; 常用方式:通过这玩意儿透传数据,避免线程间对参数的修改相互影响。
class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
class ThreadLocal {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
// Entry本身持有ThreadLocal对象(弱Weak引用)和Object对象(强引用)
Object value;
}
}
}一个 Thread 独享一个 ThreadLocal.ThreadLocalMap, 一个 map 有多个 entry, Entry 里面的"key"属性是 threadLocal 的弱引用, Entry 的"value"是 Object. ThreadLocal 只是作为一个 key, 它只被 Entry 持有; 操作都以 key 为入口
ThreadLocal 是个泛型类, 虽然它本身作为 key
内存泄漏和线程复用下脏数据情况, ThreadLocal 用完后要调用 remove()
- 内存泄漏: ThreadLocal 对象(key)通常是静态的,即便是弱引用也不会在下一次 YGC 时回收,那就无法使 Value 跟着回收
当 ThreadLocal 被回收后, value 需要在 get/set 方法中, 遍历 slot, key==null 的置为 null
- 脏数据:线程复用情况下,没有调用 remove(),导致 Thread 共享到脏数据
源码
map 结构: 只有数组, 没有链表 扩容: 2 倍 冲突: 线性向后查找空位 清理: 探测式 expungeStaleEntry 和启发式 cleanSomeSlots
InheritableThreadLocal
public class InheritableThreadLocal<T> extends ThreadLocal<T>子线程创建的时候就会拷贝一份 parent 的 threadLocals, 但是线程池场景不合适
TransmittableThreadLocal 阿里巴巴
ThreadLocal 应用
org.slf4j.MDC, UUID 全链路追踪
ReentrantLock
ReentrantLock 原理
state 资源状态计数。state 初始化为 0,表示未锁定状态,A 线程 lock()时,会调用 tryAcquire() 独占锁并将 state+1。 其他线程 tryAcquire() 就会失败,直到 A 线程 unlock() 到 state=0 为止。 在释放锁之前 A 线程可以重复获取这个锁,state 状态会累加,这就是可重入。
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract boolean initialTryLock();// 有公平和非公平2个实现
}synchronized 和 ReentrantLock 的区别
二者都是可重入锁: 自己可以再次获得自己内部锁, 每次锁计数器会自增 1
如果不是可重入的, 重入的时候会死锁
synchronized 依赖jvm, ReentrantLock 依赖API ReentrantLock 需要手动加锁、释放;
通过循环调用 CAS 操作实现加锁。
ReentrantLock 高级功能: 中断等待 可公平 可以选择性唤醒
等待时可中断: 可以中断等待锁的线程. lock.lockInterruptibly()
ReentrantLock.lockInterruptibly() throws InterruptedException
如果锁被其他线程持有, 线程休眠, 直到:
- 锁可获取
- 当前线程被中断
如果当前线程进入这个方法时是中断状态或者获取锁的时候中断, 抛出异常; 相比于 lock(), 对中断的处理更优先
可实现公平锁: 可指定是公平锁还是非公平锁(通过构造方法)
可实现选择性通知: 在一个 Lock 对象中可以创建多个 Condition 实例, 线程可以注册到指定的 Condition 中, 可以有选择性地进行线程通知.
CompletableFuture
功能强大的 Future, 异步计算, 合并结果, 并行运行统一结束等等
class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}AQS
java.util.concurrent.locks.AbstractQueuedSynchronizer;是一个构建锁和同步器的框架,能构造出大量同步器 (ReentrantLock 就是用了这个
内部维护了 state 代表共享资源,一个 FIFO 双向等待队列封装请求资源的线程
private volatile int state;AQS 原理
如果被请求的资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源锁定。 如果被请求的资源被占用,那么需要一套阻塞、等待、以及唤醒时锁分配机制。AQS 通过 CLH 队列锁实现,将暂时获取不到锁的线程加入队列。
CLH(Craig、Landin and Hagersten)队列是单向链表, AQS 中的队列是 CLH 变体的虚拟双向队列(FIFO), AQS 是通过将每条请求资源的线程封装成一个节点来实现锁的分配。
AQS 共享资源的方式
- 独占 Exclusive:只有一个线程能执行,比如 ReetrantLock,又可分为公平锁和非公平锁
- 公平锁:按照线程在队列中的顺序,先到者拿到锁
- 非公平锁:无视队列顺序,两次 CAS 争抢锁
- 共享 Share:多个线程可同时执行 如 CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock
AQS 公平锁非公平锁
非公平锁在调用 lock 后,首先就会 CAS(compareAndSetState) 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了; 公平锁不抢 非公平锁在第 1 次抢锁失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁 但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
AQS 等待队列
怎么实现的 都有哪几个状态。各个状态什么含义 有什么用 通过内部类 Node 封装线程,并且维护 pre、next 和 waitStatus 信息实现双向队列
java8: waitStatus 状态:
- CANCELLED=1 取消状态,表示线程获取锁的请求已经取消了,如节点获取锁超时。
- SIGNAL=-1 表示线程已经准备好了,等到资源释放
- CONDITION=-2 表示节点在等待队列中,节点线程等待唤醒
- PROPAGATE=-3 当前线程处于共享情况下才会使用
- 0 初始化
java17: status 状态:
- WAITING:1
- CANCELLED:0x80000000
- COND:2
AQS 条件队列
是啥 有什么用 单向链表,阻塞队列的暂存
AQS 响应中断 ?
本质是阻塞的线程能够唤醒并执行完毕。 1 线程能从阻塞中唤醒,AQS 使用 LockSupport.park(this)阻塞线程。而此方法是支持中断。 2 线程能正常执行完毕,只有获取同步状态才能正常退出自旋循环。需要退出就需要在中断时抛出异常。
AQS 实现类
Semaphore
允许 x 个线程同时访问, 相当于颁发 x 个许可证
CyclicBarrier
基于 ReentrantLock 和 Condition
跟倒计时器类似,它是让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时被拦截的线程才能继续工作
CountDownLatch
协调多个线程的同步,可以让某线程等待倒计时结束再开始执行 典型用法:
- 等待多个线程结束
- 让多个线程同时开始
CountDownLatch
任务分为 N 个子线程执行,state 初始化为 N,子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS,减一。等到 state=0,会 unpark()主线程,主线程会从 await()返回,继续后续操作。
countDownLatch 使用
预估多个任务费用的时候, 有 10 条线程并行执行, 每条线程执行完任务后 countDown, 最后 await 等待所有线程执行完毕后统一返回结果 可以用 CompletableFuture 改进
原子类
Atomic 类
4 类: 基本类型, Array, Reference, Updater
基本类型
使用原子的方式更新基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。java
final T reference; final int stamp; - AtomicMarkableReference:原子更新带有标记位的引用类型java
final T reference; final boolean mark;
对象的属性修改类型
更新对象的属性必须 public volatile 修饰
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器java
public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass, Class<W> vclass, String fieldName){}
AtomicInteger 原理
利用 CAS, volatile 和 native 方法来保证原子操作 拿期望的值和原本的一个值作比较,如果相同则更新成新的值。 UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法用来拿到“原来的值”的内存地址,返回值是(long) valueOffset。 另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
并发容器
- ConcurrentHashMap
- CopyOnWriteArrayList 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector
- ConcurrentLinkedQueue 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
- ConcurrentSkipListMap 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找
ConcurrentHashMap
1.7 底层是分片数组
有 Segment 分段锁, 继承于 ReentrantLock, 每次只给一段加锁保证并发度, 显然冲突的时候使用 CAS
1.8 底层跟 HashMap 一致, 使用 synchronized, 放弃使用 ReentrantLock; CAS 操作在 桶位创建第一个元素的时候 casTabAt put 方法中有一个循环, 建表/放置头节点之后会进入下一次循环; 会用 sychronized 锁定桶位
其实是减小了锁的粒度, 在哈希冲突不多的情况下, 本也不需要加锁操作, HashMap 的线程不安全主要还是在哈希冲突的时候
CopyOnWriteArrayList
复制与替换, get() 完全不加锁 set() 加锁 copy array
非阻塞队列 ConcurrentLinkedQueue
典型代表 ConcurrentLinkedQueue CAS 实现并发读写, 性能不错
ConcurrentSkipListMap
跳表 实现有序 map 平衡树的锁粒度大, 跳表的锁粒度小
CAS
轻量级 不加互斥锁
- 并发量大时对 CPU 消耗大, 有可能产生忙循环
- ABA 问题, 可以通过添加版本号解决
乐观锁 悲观锁
乐观默认别人不会修改,更新的时候会判断是否被其他操作修改过,如果修改过会重试。版本号、CAS 等 悲观锁默认别人会修改,会先对资源进行加锁处理