并发三要素
可见性,原子性,有序性
并发问题产生的原因
可见性:CPU多级缓存的一致性问题
原子性:线程调度(CPU 时间片,上下文切换)
有序性:指令重排序
缓存一致性
高速缓存Cache
缓存行
根据空间局部性原理(位置相邻的数据常常会在相近的时间内被访问),为提高缓存命中率将缓存行作为 Cache 与内存交互的最小单位,在 x86 架构中为 64 个字节。
总线锁&缓存锁
处理器发出 LOCK 指令锁定总线,使其他处理器的请求阻塞,该处理器独占共享内存。缺点是 CPU 利用率低下
缓存锁对CPU的缓存中的缓存行进行锁定,通过缓存一致性协议保证确保修改操作的一致性
缓存一致性协议
图解 MESI:https://zhuanlan.zhihu.com/p/467782159
指令重排序
as-if-serial 语义
保证单线程下指令重排序后执行结果不变
重排序类型
编译器重排序:编译器对代码进行优化的过程中可能会打乱代码顺序
指令集并行(ILP):指令之间不存在相关时,它们在流水线中可以重叠起来并行执行。这种指令序列中存在潜在并行性称为指令级并行
内存重排:store buffer 延迟写入,在多线程下无法保证可见性
解决并发问题:JMM
并发编程需要解决的两个问题
同步:不同线程操作发生顺序的控制
通信:线程间如何交换信息
并发编程的两个模型
内存共享:JMM
消息传递:Actor
[译] 你想知道的关于 actor 模型但可能不敢问的所有信息 | 敖小剑的博客
JMM是什么
是一种抽象的模型。主要用来定义多线程中变量的访问规则,用来解决变量的可见性、有序性和原子性问题,确保在并发环境中安全地访问共享变量。
JMM 如何保证线程安全
定义 happens-before 规则
- 程序顺序规则
- volatile 变量规则:volatile 域的写发生于 volatile 域的读之前
- monitor 锁规则:解锁发生在加锁前
- start & join 规则:启动操作发生于被启动线程的所有操作之前;线程中的所有操作都发生于对此线程的终止检测之前
- 传递性
volatile
功能
- 防止重排序
- 保证可见性
原理
volatile 通过内存屏障保证可见性和有序性
CPU 内存一致性模型
不同 CPU 为提升性能对一致性要求不同,常见架构有 SC, TSO, PSO, RMO
Java 内存屏障
Java 为屏蔽不同 CPU 一致性模型的差异,抽象出了四种内存屏障:
LoadLoad Barriers
LoadStore Barriers
StoreLoad Barriers
StroreStore Barriers
什么时候插入哪种内存屏障遵循 JSR 133规范:
第一步 / 第二步 | 读 | 写 | volatile 读 | volatile 写 |
读 | LoadStore | |||
写 | StoreStore | |||
volatile 读 | LoadLoad | StoreLoad | LoadLoad | LoadStore |
volatile 写 | StoreLoad | StoreStore |
总结为以下三大原则:
- volatile 读写之间相互序列化
- volatile 读之后不能乱序
- volatile 写之前不能乱序
深入理解java内存屏障(volatile实现原理)_java内存屏障原理-CSDN博客
Synchronized
功能
对象锁:
- 代码块,指定锁对象为 this 或自定义对象
- 普通方法锁(this)
类锁:
- 代码块,指定锁对象为 xxx.class
- 静态方法锁(Class)
原理
Monitor(管程)
Java 使用抽象程度更高的管程来实现并发编程的同步和互斥,它简化了信号量复杂的 PV 操作配对,AQS 和 ObjectMonitor 是对管程机制的不同实现。
https://www.bilibili.com/video/BV1cWUDYgERn/
信号量 VS 管程
https://www.cnblogs.com/binarylei/p/12544002.html
ObjectMonitor
- ObjectMonitor 关键属性
_recursions = 0; // synchronized是一个重入锁,这个变量记录锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 调用wait阻塞的线程:等待线程组成的双向循环链表,_WaitSet是第一个节点
_cxq = NULL ; // 有线程在执行,新进入的线程会进入这个队列:多线程竞争锁会先存到这个单向链表中 (FILO栈结构:非公平!)
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
- ObjectMonitor 等待唤醒机制
Monitor对象全解析 - 张高峰的博客 | Peakiz Blog
Java并发基石——所谓“阻塞”:Object Monitor和AQS(1)_on object monitor-CSDN博客
Java 对象结构
Mark Word
内置锁状态 | 57bit | 4bit | 1bit | 2bit | ||
---|---|---|---|---|---|---|
biased_lock | lock | |||||
无锁 | unused(25bit) | indentity_hashCode(31bit) | unused(1bit) | age | 0 | 01 |
轻量级锁 | ptr_to_lock_record 指向方法栈帧中锁记录的指针 (62bit) | 00 | ||||
重量级锁 | ptr_to_heavyweight_monitor 指向重量级锁监视器的指针 (62bit) | 10 | ||||
GC 标记 | 空 (62bit) | 11 | ||||
偏向锁 | 线程 ID(54bit) | epoch(2bit) | unused(1bit) | age | 1 | 01 |
lock:锁标志位
biased:是否启用偏向锁
age:对象分代年龄。在 GC 中,每次被复制到 survivor 区年龄都 + 1,达到阈值 15 进入老年代(4bit 最大 15)
identity_hashcode:对象标识 HashCode。当调用 Object.hashCode() 方法或 System.identityHashCode() 方法时将计算结果写入对象头。
优化 Synchronized
锁消除
JVM 通过逃逸分析省略同步,如果检测到某段代码不被共享或参与竞争就去除同步锁。
锁粗化
一连串操作对同一个对象加锁解锁时,JVM 将锁的范围扩展到这一连串操作。
锁升级
无锁 👉 偏向锁 👉 轻量级锁 👉 重量级锁
偏向锁:无竞争就不同步
同一个线程多次获取锁和释放锁是资源的浪费,如果一个线程长时间持有该锁,只需要获取一次即可。
获取偏向锁流程
轻量级锁:错时竞争就是无竞争
如果持锁线程进入同步块的时间短,无需阻塞等待锁的线程,通过自旋等待锁的释放。
注:自旋——消耗 CPU,让线程循环等待
重量级锁:mutex
线程切换需要保存上下文,需要进入内核态,内核态与用户态的转换非常耗时。
锁
Java
AQS
数据结构
技术依赖关系
重要变量和方法
AQS 有两种资源共享模式,Exclusive(独占)或者 Share;
compareAndSetState方法,在独占模式下进行锁请求时经常使用——因为是独占模式所以肯定不允许多个线程同时更改State的状态。
private volatile int state; // 管理共享资源状态或计数,具体含义由同步器实现
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
Node 内部类
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
Node nextWaiter;
ReentrantLock
特点是公平与非公平锁,可重入锁
内部类
构造函数
默认创建非公平锁,转入参数 fair 决定 new 公平锁还是非公平锁
final void lock()
lock会调用内部类 sync.lock(),无论是 FairSync 还是 NonfairSync 都是调用父类 AQS 的 acquire 方法;其中 tryAcquire(arg) 由 reentrantLock 实现,若当前线程成功获取独占操作权则直接返回;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire 失败后,将该线程加入阻塞队列 addWaiter(Node.EXCLUSIVE),Node.EXCULSIVE 表示该节点在独占模式下等待;
加入队列后还要设置它的状态——acquireQueued(final Node node, int arg):
shouldParkAfterFailedAcquire(pred, node) 用于判断是否阻塞当前节点。只有当前节点的前驱节点状态是 SIGNAL 才阻塞当前节点的线程,因此队列中头节点不是 SIGNAL 表示当前节点的线程还未完成阻塞。
lock.unlock()
public void unlock() {
sync.release(1);
}
调用 sync 实现的 tryRelease( arg ),成功且头节点的后继节点被阻塞则释放后继节点:
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantLock 作为可重入锁,只有释放全部的锁才能退出独占操作:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
与 Synchronized 对比
- 需要手动释放锁,编程麻烦但更灵活,还可以通过 Condition 选择获得锁的线程
- 可以实现公平与非公平锁
BlockingQueue
方法
抛出异常 | 返回特定值 | 阻塞 | 限时阻塞 | |
入队 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
出队 | remove(o) | poll() | take() | poll(o, timeout, timeunit) |
检查 | element() | peek() |
接口实现类使用场景
ArrayBlockingQueue
- 数组实现,容量固定,内存占用确定
- 存取共用一把锁,存取操作无法并行
不适用于要求高并发和吞吐量的场景,可以处理生产者-消费者问题
LinkedBlockingQueue
- 链表实现,数据新增移除比数组快,队列容量默认Integer.MAX_VALUE,存储线程基本不会阻塞,但是消费线程速度慢会造成内存占用过多
- 存取操作分别拥有独立的锁,可实现存取并行
适用于消费者和生产者速度相当的核心业务,如订单完成的短信/邮件通知
FutureTask
类的关系
实现了 runnable,callable 和 future
使用方法
线程池提交
FutureTask<Integer> futureTask = new FutureTask<>(()->{
Thread.sleep(3000);
return 11111;
});
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(futureTask); // 线程池启动法
线程启动
new Thread(futureTask).start(); // 线程启动法
构造函数
参数为 callable 的构造函数
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Callable}.
*
* @param callable the callable task
* @throws NullPointerException if the callable is null
*/
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
参数为 runnable 的构造函数,采用适配器模式将 runnable 和 result 组合成 callable
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
RunnableAdapter 是适配器
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
RunnableAdapter 持有一个 Runnable 接口 task 和泛型 result,通过构造函数进行赋值;调用 call() 方法实际是调用 task.run() 并返回 result。
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
核心成员
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
/** The underlying callable; nulled out after running */
private Callable<V> callable;
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
/** The thread running the callable; CASed during run() */
private volatile Thread runner;
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
outcome:get() 方法返回的结果或异常原因
runner:运行 callable 任务的线程
waiters:Treiber栈,存放等待线程,通过 CAS 保证线程安全(如多个线程同时调用 get() 方法被阻塞需要同时入栈),底层是单链表
WaitNode 持有线程和指向下一个 node 的 next 指针:
/**
* Simple linked list nodes to record waiting threads in a Treiber
* stack. See other classes such as Phaser and SynchronousQueue
* for more detailed explanation.
*/
static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() { thread = Thread.currentThread(); }
}
FutureTask 状态转换
- NEW:新建的任务
- COMPLETING:运行中的任务(异常原因或运行结果还没有到 outcome)
- INTERRUPTING:正在响应用户调用的 cancel(boolean mayInterruptIfRunning) 方法
- NORMAL:任务执行完成,结果在 outcome
- EXCEPTIONAL:任务执行异常,异常原因在 outcome
- CANCELLED:用户调用了 cancel(false) 方法取消任务且不中断任务执行线程
- INTERRUPTED:用户调用了 cancel(true) ,该方法使用 runner.interrupt() 中断任务执行线程,线程已中断
线程安全
volatile & UNSAFE
ThreadPoolExecutor
核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:阻塞队列中线程空闲的最长时间
unit:keepAliveTime 的单位
workQueue:阻塞队列,可选 BlockingQueue 接口的各种实现类
threadFactory:默认 DefaultThreadFactory,自定义工厂可以继承 ThreadFactory 接口,在构造函数中传入自定义字符串修改 namePrefix
handler:拒绝策略,线程池提供了四种策略——
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:提交任务的线程自己运行
- DiscardOldestPolicy:丢弃队列中等待最久的线程
- DiscardPolicy:直接丢弃任务
线程池执行流程
检验一下你是否看懂了
1. 10 个线程在核心运行,20个任务在队列等待
2. 第 1001 个任务触发拒绝策略
为什么不允许用 Executors 创建线程池?
通过ThreadPoolExecutor的方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
newFixedThreadPool & newSingleThreadExecutor:LinkedBlockingQueue 是无界队列,处理任务过慢会导致任务无限堆积,造成 OOM
newCachedThreadPool & newScheduledThreadPool:最大线程数是 Integer.MAX_VALUE,任务过多会创建过多线程,造成 OOM
配置线程池的考虑
ThreadLocal
方法
.withInitial()
ThreadLocal 设置初始值的方法
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
Set(T value)
获取当前运行线程的 ThreadLocalMap,不为空则插入,为空则新建一个 map
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
createMap:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap
构造函数
ThreadLocalMap 构造函数,新建一个 Entry 数组,并插入初始值;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap 本质上是 HashMap。
内存泄露问题
Key 是弱引用,value 是强引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
key 弱引用是为了防止内存泄露。线程不结束运行,ThreadLocal 静态变量不能被释放,但是创建过多线程从而增加的 ThreadLocal 中不再使用的却没有被释放。将 key 设为弱引用,发生 GC 时自动回收 key,而 ThreadLocalMap 的 expungeStaleEntry、replaceStaleEntry、cleanSomeSlots 和 expungeStaleEntries 方法会清理 key 为 null 的元素。
开放地址法
ThreadLocalMap 使用开放地址法解决 Hash 冲突
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
线性探测:di = i
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}