文章目录
线程
1. 创建线程
继承 Thread 类
public class SubThread extends Thread {
//重写 run()方法
public void run() {
//输出...
}
}
public class TestThread {
public static void main(String[] args) {
SubThread st = new SubThread();
//调用start()方法运行线程
st.start();
}
}
实现 Runnable 接口
public class PrintNum1 implements Runnable {
public void run() {
...
}
}
public class TestThread1 {
public static void main(String[] args) {
PrintNum1 p = new PrintNum1();
Thread t1 = new Thread(p);
t1.start();
}
}
实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
2. 线程方法
2.1. 调度
- Thread.sleep(millisec):睡眠
- Thread.yield():调用此方法的线程释放当前 cpu 的执行权(可能会再次调用该线程)
声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。 - join():等其他线程调用结束
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。
例:A 线程中调用 B 线程的 join,当执行到 join 方法时 A 停止,直到 B 执行完,A 再接着 join 之后的代码执行
2.2. 优先级
Api:
- getPriority():返回线程优先值
- setPriority(int newPriority):改变线程的优先级
概述:
优先级高只能是说明,它获得时间片的概率大,但不是一定会执行它
线程创建时继承父线程的优先级
1(MIN_PRIORITY)5(NORMAL_PRIORITY)10(MAX_PRIORITY)
2.3. 中断
-
void interrupt():向线程发送中断请求。线程的中断状态将被设置为 true。如果该线程被 sleep 调用阻塞、限期等待或者无限期等待状态,抛出 InterruptedException 异常。
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { try { Thread.sleep(2000); } catch(InterruptedException e) { e.printStackTrace(); } }).start(); // 线程在 sleep 时中断,抛出异常 thread.interrupt(); System.out.println("Main run"); }
-
static boolean interrupted():测试当前线程是否被中断,并将当前线程的中断状态重置为 false
-
boolean isInterrupted():测试线程是否被终止,不改变线程的中断状态
3. 线程的生命周期
新建、就绪、运行、阻塞、死亡
线程池
【细谈 Java 并发】谈谈线程池:ThreadPoolExecutor
1. ThreadPoolExecutor
线程数计算公式: 线程数 = CPU 核数 * 期望 CPU 使用率 0~1 *(1 + 等待时间 / 计算时间)
1.1. 七大参数
ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
- corePoolSize:核心线程数。保留在线程池中的线程数,即使它们处于空闲状态,除非设置了
allowCoreThreadTimeOut
。 - maximumPoolSize:最大线程数。线程池中允许的最大线程数,在阻塞队列满了之后加入的任务会创建非核心线程进行处理。
- keepAliveTime:当线程数大于
corePoolSize
时,非核心线程在终止之前等待新任务的最大时间。 - unit:
keepAliveTime
参数的时间单位。 - workQueue:阻塞队列。在执行任务之前用于保存任务的队列。 这个队列将只保存
execute
方法提交的Runnable
任务。提交的线程后若发现线程总数超过corePoolSize
但是不超过keepAliveTime
的情况下。 - threadFactory:用来执行的时候创建线程的线程工厂,可用于线程命名。
- handler:在执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量。
- AbortPolicy:直接抛出异常,这是默认策略。
- CallerRunsPolicy:用调用者所在的线程来执行任务。
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务。
- DiscardPolicy:直接丢弃任务。
随着任务不断增加
- 如果核心线程还没创建: 创建并执行任务。核心线程不会被销毁,会一直存在。
- 如果所有的核心线程都在执行任务: 把当前任务加入阻塞队列。
- 如果阻塞队列满了且线程数小于最大线程数: 创建非核心线程去执行。
- 如果阻塞队列满了且线程数满了,也都在执行任务: 进行拒绝策略。
1.2. API
-
execute(Runnable runnable):开启线程
-
shutdown():会等待线程都执行完毕之后再关闭
-
shutdownNow():相当于调用每个线程的 interrupt() 方法
-
submit():如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
Future<?> future = executorService.submit(() -> { // ... }); future.cancel(true);
1.3. 线程池状态
-
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
-
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown() 方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用 shutdown() 方法进入该状态)
-
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态
-
TIDYING:如果所有的任务都已终止了,workerCount(有效线程数)为 0,线程池进入该状态后会调用 terminated() 方法进入 TERMINATED 状态
-
TERMINATED:在 terminated() 方法执行完后进入该状态,默认 terminated() 方法中什么也没有做
1.4. 源码
ctl:
高 3 位表示线程池状态,低 29 位表示 worker 数量
// 1. 状态|工作数的一个 32 bit 的值
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 2. 29 bit 代表线程数
private static final int COUNT_BITS = Integer.SIZE - 3;
// 3. 线程池允许的最大线程数。1 左移 29 位,然后减 1,即为 2^29 - 1
// 0001-1111-1111-1111-1111-1111-1111-1111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 4. 线程池有 5 种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED
// 1110-0000-0000-0000-0000-0000-0000-0000
private static final int RUNNING = -1 << COUNT_BITS;
// 0000-0000-0000-0000-0000-0000-0000-0000
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 0010-0000-0000-0000-0000-0000-0000-0000
private static final int STOP = 1 << COUNT_BITS;
// 0100-0000-0000-0000-0000-0000-0000-0000
private static final int TIDYING = 2 << COUNT_BITS;
// 0110-0000-0000-0000-0000-0000-0000-0000
private static final int TERMINATED = 3 << COUNT_BITS;
// 5. 获取线程池状态,通过按位与操作,低 29 位将全部变成 0
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 6. 获取线程池 worker 数量,通过按位与操作,高 3 位将全部变成 0
private static int workerCountOf(int c) { return c & CAPACITY; }
// 7. 根据线程池状态和线程池 worker 数量,生成 ctl 值
private static int ctlOf(int rs, int wc) { return rs | wc; }
// 8. 线程池状态小于 xx
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
// 9. 线程池状态大于等于 xx
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
execute(Runnable command) 方法:
- worker 数量 < 核心线程数 -> 直接创建 worker 执行任务
- worker 数量 >= 核心线程数 && 线程池是运行状态 -> 任务直接进入队列
- 双重检查状态: 入队后再次检查状态,若线程池状态不是 RUNNING 状态,说明执行过 shutdown 命令,需要对新加入的任务执行 reject() 操作。
- 核心线程数为 0 的情况: 若是 RUNNING 状态,且当前线程数为 0。该任务因核心线程数已满才加入阻塞队列,表明核心线程数为 0(在线程池构造方法中,核心线程数允许为 0)。这种情况会创建非核心线程去执行该任务
- 如果线程池不是运行状态,或者任务进入队列失败(例如队列满了),则尝试创建 worker 执行任务。如果创建 worker 失败,说明线程池 shutdown 或者饱和了,这时会执行拒绝。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1.worker 数量 < 核心线程数 -> 直接创建 worker 执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
// 没有成功 addWorker(),再次获取 c(凡是需要再次用 ctl 做判断时,都会再次调用ctl.get())
c = ctl.get();
}
// 2.worker 数量超过核心线程数 && 线程池是运行状态 -> 任务直接进入队列
if (isRunning(c) && workQueue.offer(command)) {
// 任务入队列前后,线程池的状态可能会发生变化。
int recheck = ctl.get();
// 线程池状态不是 RUNNING 状态,说明执行过 shutdown 命令,需要对新加入的任务执行 reject() 操作。
if (! isRunning(recheck) && remove(command))
reject(command);
// 在线程池构造方法中,核心线程数允许为 0
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建 worker 执行任务。
// 1. 线程池不是运行状态时,addWorker 内部会判断线程池状态
// 2. addWorker 第 2 个参数表示是否创建核心线程
// 3. addWorker 返回 false,则说明任务执行失败,需要执行 reject 操作
else if (!addWorker(command, false))
reject(command);
}
addworker(Runnable firstTask, boolean core) 方法:
- 增加 worker 数量:使用两层自旋,第一层用于判断线程池状态,第二层使用 CAS 增加 worker 数量(CAS 自旋)
- 外层自旋:判断线程池状态。
- 状态 == RUNNING,通过
- 状态 == SHUTDOWN,任务为空且队列不为空,通过
- 其余情况返回 false
- 内层自旋:CAS 自旋
- worker 数量超过容量,直接返回 false
- 使用 CAS 的方式增加 worker 数量。若成功则直接跳出整个外层自旋
- 否则重新获取状态,若状态发生变化,重新执行外层循环普判断状态
- 若状态没发生变化,意味着还可以继续竞争 CAS,直接继续内层循环
- 外层自旋:判断线程池状态。
- 创建 worker 并执行
- 创建 Worker 对象 worker
- 加锁,把 worker 加入 workers(HashSet)
- 如果添加成功则调用 worker 内部线程的 start() 方法执行任务。实际上会执行
ThreadPoolExecutor#runWorker
方法
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 外层自旋
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 这个条件和下面的条件等价
// (rs > SHUTDOWN) ||
// (rs == SHUTDOWN && firstTask != null) ||
// (rs == SHUTDOWN && workQueue.isEmpty())
// 1. 线程池状态大于 SHUTDOWN 时,直接返回 false
// 2. 线程池状态等于 SHUTDOWN,且 firstTask 不为 null,直接返回 false
// 3. 线程池状态等于 SHUTDOWN,且队列为空,直接返回 false
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 内层 CAS 自旋
for (;;) {
int wc = workerCountOf(c);
// worker 数量超过容量,直接返回 false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 使用 CAS 的方式增加 worker 数量。
// 若增加成功,则直接跳出外层循环进入到第二部分
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 线程池状态发生变化,对外层循环进行自旋
if (runStateOf(c) != rs)
continue retry;
// 其他情况,直接内层循环进行自旋即可
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
// worker 的添加必须是串行的,因此需要加锁
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
// 这儿需要重新检查线程池状态
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// worker 已经调用过了 start() 方法,则不再创建 worker
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// worker 创建并添加到 workers 成功
workers.add(w);
// 更新`largestPoolSize`变量
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 启动 worker 线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// worker 线程启动失败,说明线程池状态发生了变化(关闭操作被执行),需要进行shutdown 相关操作
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
Worker 类:
既是一个同步队列也是一个 Runnable
-
自身维护了一个
Thread thread
(任务执行器)和一个Runnable firstTask
(任务) -
在构造器中会把传入的任务赋值给
firstTask
,然后把当前自己传入thread
-
当调用
thread.start()
时会执行这个 worker 的 run() 方法,最终调用ThreadPoolExecutor#runWorker
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
final Thread thread;
Runnable firstTask;
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 这儿是 Worker 的关键所在,使用了线程工厂创建了一个线程。传入的参数为当前worker
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
...
}
runworker(Worker w):核心线程执行逻辑
- 如果 firstTask 不为 null,则执行 firstTask
- 如果 firstTask 为 null,则调用 getTask() 从队列获取任务执行
- 加锁
w.lock();
。Worker 实现了 AQS,所以自己也是一把锁,并重写了 tryAcquire 方法(非重入) - 判断写线程池状态,如果线程池正在停止,则对当前线程进行中断操作
- 执行任务
task.run();
- 已完成任务数加一
w.completedTasks++;
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 调用 unlock() 是为了让外部可以中断
w.unlock(); // allow interrupts
// 这个变量用于判断是否进入过自旋(while 循环)
boolean completedAbruptly = true;
try {
// 这儿是自旋
// 1. 如果 firstTask 不为 null,则执行 firstTask;
// 2. 如果 firstTask 为 null,则调用 getTask() 从队列获取任务。
// 3. 阻塞队列的特性就是:当队列为空时,当前线程会被阻塞等待
while (task != null || (task = getTask()) != null) {
// 这儿对 worker 进行加锁,是为了达到下面的目的
// 1. 降低锁范围,提升性能
// 2. 保证每个 worker 执行的任务是串行的
// 开始运行,不允许中断
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
// 如果线程池正在停止,则对当前线程进行中断操作
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
// 执行任务,且在执行前后通过 beforeExecute() 和 afterExecute() 来扩展其功能。
// 这两个方法在当前类里面为空实现。
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
// 帮助 gc
task = null;
// 已完成任务数加一
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 自旋操作被退出,说明线程池正在结束
processWorkerExit(w, completedAbruptly);
}
}
2. ForkJoinPool
概述:
ForkJoin 使用 ForkJoinPool 来启动,它是一个特殊的线程池,线程数量取决于 CPU 核数
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算
原理:
-
ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线程都维护了一个双端队列,用来存储需要执行的任务。工作窃取算法允许空闲的线程从其它线程的双端队列中窃取一个任务来执行。
-
窃取的任务必须是最晚的任务,避免和队列所属线程发生竞争
2.1. RecursiveAction
不带返回值
public class AddTask extends RecursiveAction {
int start, end;
AddTask(int s, int e) {
start = s;
end = e;
}
@Override
protected void compute() {
if (end - start <= MAX_NUM) {
long sum = 0L;
for (int i = start; i < end; i++) {
sum += nums[i];
}
System.out.println("from:" + start + " to:" + end + " = " + sum);
} else {
int middle = start + (end - start) / 2;
AddTask subTask1 = new AddTask(start, middle);
AddTask subTask2 = new AddTask(middle, end);
subTask1.fork();
subTask2.fork();
}
}
public static void main(String[] args) {
ForkJoinPool fjp = new ForkJoinPool();
AddTask task = new AddTask(0, nums.length);
fjp.execute(task);
}
}
2.2. RecursiveTask
带返回值
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任务足够小则直接计算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
// forkJoinPool.execute(example);
// long result = example.join();
// System.out.println(result);
}
}
3. Executors
-
CachedThreadPool: 一个任务创建一个线程
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { executorService.execute(new MyRunnable()); } executorService.shutdown(); }
-
FixedThreadPool: 所有任务只能使用固定大小的线程
-
SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool
-
WorkStealingPool:工作窃取线程池
public static ExecutorService newWorkStealingPool() { return new ForkJoinPool (Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }
线程安全场景
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为
1. 不可变
不可变(Immutable)的对象一定是线程安全的。
-
final 关键字修饰的基本数据类型
-
String
-
枚举类型
-
Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的
-
**不可变集合:**可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。对集合进行修改的方法都直接抛出异常。
public class ImmutableExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); Map<String, Integer> unmodifiableMap = Collections.unmodifiableMap(map); unmodifiableMap.put("a", 1); } } /* Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) at ImmutableExample.main(ImmutableExample.java:9) */
2. 同步
-
阻塞同步:悲观锁:synchronized 和 ReentrantLock
-
非阻塞同步
- CAS(Compare-and-Swap,CAS)乐观锁
- AtomicInteger 等原子类
3. 无同步
如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性
-
栈封闭:
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的
-
ThreadLocal:
把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题
-
可重入代码:
这种代码也叫做 纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
synchronized
synchronized 发生异常会释放锁
不能拿 String、Integer、Long 等基础类型做锁
synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。但是同步块里的非原子操作依旧可能发生指令重排
1. Api
- wait():令当前资源挂起并放弃 CPU、同步资源,不再抢资源,除非唤醒
- notify():当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 唤醒正在排队等待同步资源的下城中优先级最高者结束等待
- notifyAll():当其他线程的运行使得这个条件满足时,其它线程会调用 notifyAll() 唤醒正在排队等待资源的所有线程结束等待
这三个方法只有在 synchrozied 方法或 synchrozied 代码块中才能使用
2. 使用
同步方法:
锁为当前对象,即 this,所以不能用在继承方法的线程上
public class Bank {
private double[] accounts;
public synchronized void transfer(int from, int to, int amount) throws InterruptedException {
while (accounts[from] < amount) {
wait();
}
accounts[from] -= amount;
accounts[to] += amount;
notifyAll();
}
}
同步阻塞:
public class Window2 implements Runnable {
// 共享数据
int ticket = 100;
// 锁(同步监视器)
Object obj = new Object();
// 所有线程必须共用同一把锁
public void run() {
while (true) {
// 若是静态方法可用当前类.class
synchronized (obj) {
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
}
catch (InterrupteException e) {
e.printStackTrace();
}
System.out.println(...);
}
}
}
}
}
3. 用户态与内核态
synchronized 加锁需要程序由用户态切换到内核态,效率低
Intel x86 架构的 CPU 来说一共有 0~3 四个特权级,0 级最高,3 级最低。
硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查。相关的概念有 CPL、DPL 和 RPL。
Linux 中当程序运行在 3 级特权级上时称之运行在用户态,运行在 0 级特权级上时称之运行在内核态。
用户态通过申请外部资源(申请堆内存、读写磁盘文件。。。)切换至内核态
用户态切换到内核态的 3 种方式:
- 系统调用
- 中断
- 异常
具体的切换操作: 以 open 函数调用为例
open 函数调用时,会通过中断陷入内核,从而调用 sys_open 函数。
-
系统调用触发 0x80 中断: 并且将系统调用号存储在 eax 寄存器中,然后陷入内核,内核开始执行中断处理程序,在系统调用表中查找系统调用号对应的系统内核函数并且调用,执行完成后又将返回值通过 eax 寄存器返回给用户空间
-
中断机制: 中断处理程序(内核 )
计算机处于执行期间,系统内发生了非寻常或非预期的急需处理事件,CPU 暂时中断当前正在执行的程序而转去执行相应的事件处理程序,处理完毕后返回原来被中断处继续执行
处理中断源的程序称为中断处理程序。中断的实现由软件和硬件综合完成,硬件部分叫做硬件装置,软件部分称为软件处理程序。
4. 锁升级
对象头 markword 记录了
锁升级步骤:
4.1. 偏向锁
偏向锁假定将来只有第一个申请锁的线程会使用锁。因此,只需要在 Markword 中 CAS 记录 当前线程指针,如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁。
以后当前线程记录的这个线程就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁
源码: fast_enter 方法中在 safepoint 的时候上锁,失败则调用 slow_enter 方法升级为自旋锁。
在明确知道会有多个线程竞争的情况下,偏向锁会涉及锁撤销,比自旋锁效率低,所以这时不会启用偏向锁
例如:JVM 启动过程会有很多线程竞争,所以默认情况启动时不打开偏向锁,过一段时间才会打开
-XX:BiasedLockingStartupDelay=4(默认 4 秒),刚开始未偏向任何一个线程,所以称为匿名偏向
4.2. 自旋锁
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步。详情查看自旋锁。
原理:
每个线程在其线程栈内部生成一个 LockRecord,拿到锁后记录在 markword 中。即演化为多个 LockRecord 使用 CAS 竞争写入 markword 的场景。
对应 slow_enter 方法,首先进入自旋,自旋锁不行则调用 inflate 方法膨胀为重量级锁。
升级:
- 1.6 之前,有线程超过 10 次自旋(-XX:PreBlockSpin),或者自旋线程数超过 CPU 核数的一半。则升级为重量级锁。
- 1.6 之后,加入自适应自旋 Adapative Self Spinning,由 JVM 自己控制何时升级成重量级锁。
4.3. 重量级锁
synchronized 编译为字节码后代码由 monitorenter
和 monitorexit
包围,表示上锁和释放锁。
synchronized 加锁需要程序由用户态切换到内核态,效率低。
内核态 ObjectMonitor 对象中有 WaitSet 队列,抢锁的线程都会进入等待队列,不需要消耗 CPU 资源,由操作系统进程调度
4.4. 可重入锁
重入次数必须记录,因为加锁次数必须和解锁次数对应
- 偏向锁/自旋锁:记录在线程栈中,每重入一次,LockRecord + 1。LockRecord 指向备份的 markword(displace head),里面记录了 HashCode,重入的 LockRecord 指向 null
- 重量级锁:记录在 ObjectMonitor 一个字段上
5. 锁优化
5.1. 锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除
如下 concatString 方法:
每个 append() 方法中都有一个 synchronized 同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它,因此可以进行消除
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
原理: 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除
5.2. 锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗
虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,则会把加锁的范围扩展(粗化)到整个操作序列的外部。
6. Monitor
-
Monitor 是一种用来实现同步的工具
-
与每个 java 对象相关联,所有的 Java 对象是天生携带 monitor
-
Monitor 是实现 Sychronized(内置锁)的基础
-
Monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现
ObjectMonitor() {
// 用来记录该对象被线程获取锁的次数
_count = 0;
_waiters = 0;
// 锁的重入次数
_recursions = 0;
// 指向持有 ObjectMonitor 对象的线程
_owner = NULL;
// 处于 wait 状态的线程,会被加入到 _WaitSet
_WaitSet = NULL;
_WaitSetLock = 0 ;
// 处于等待锁 block 状态的线程,会被加入到该列表
_EntryList = NULL ;
}
ReentrantLock
- boolean tryLock(long time, TimeUnit unit): 尝试获得锁,阻塞时间不会超过给定的值;如果成功返回 true
- void lockInterruptibly(): 获得锁,但是会不确定地发生阻塞。如果线程被中断,抛出一个 InterruptedException 异常
1. 条件对象
作用: 条件对象用来管理那些已经进入被保护的代码段但还不能运行的线程。一个 Condition 对象为一个队列。
方法:
ReentrantLock <==> synchronized
- await() <==> wait(): 将该线程放到条件的等待集中
- signalAll() <==> notifyAll(): 解除该条件的等待集中的所有线程的阻塞状态
- signal() <==> notify(): 从该条件的等待集中随机地选择一个线程,解除其阻塞状态
Condition sufficientFunds = bankLock.newCondition();
// 当前线程不满足条件,进入该条件的等待集,放弃锁
while (!(ok to proceed)) {
sufficientFunds.await();
}
// do something
// 其他线程的操作应使该线程重新判断条件。
// 重新激活因为这一条件而等待的所有线程。
sufficientFunds.signalAll();
可以创建多个不同的 Condition,实现不同的等待队列
Condition producer = lock.newCondition();
Condition comsumer = lock.newCondition();
while (isProducer) {
comsumer.await();
}
producer.signalAll();
2. 读写锁
- Lock readLock(): 得到一个可以被多个读操作共用的读锁,但会排斥所有写操作
- Lock writeLock(): 得到一个写锁,排斥所有其他的读操作和写操作
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
/**
* 对所有的获取方法加读锁
*/
public doule getTotalBalance() {
readLock.lock();
try {
//...
} finally {
readLock.unlock();
}
}
/**
* 对所有的修改方法加写锁
*/
public void transfer() {
writeLock.lock();
try {
//...
} finally {
writeLock.unlock();
}
}
3. 比较
- 锁的实现: synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的
- 性能: 新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同
- 等待可中断: 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情
ReentrantLock 可中断,而 synchronized 不行 - 公平锁: 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的 - 锁绑定多个条件: 一个 ReentrantLock 可以同时绑定多个 Condition 对象
4. CAS
并发策略:
-
乐观并发策略(乐观锁): 乐观地认为本次操作没有其他线程竞争。
先尝试进行操作,如果没有其它线程争用共享数据,那操作就成功了。否则采取补偿措施(不断地重试)直到成功为止。
-
悲观并发策略(悲观锁): 悲观地认为本次操作有其他线程竞争。
直接使用互斥量进行加锁阻塞。
原理: CAS 指令是原子操作,有 3 个操作数。分别是内存地址 V、旧的预期值 A 和新值 B。
当执行操作时,只有当 V 的值等于 A(即认为未被其他线程修改过),才将 V 的值更新为 B。
若失败了则重新获取这三个值再次进行判断,直到操作成功。
乐观锁适用于多读的应用类型,这样可以提高吞吐量
1. ABA 问题
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
解决: J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。
改用传统的互斥同步可能会比原子类更高效。
2. Atomic 类
LongAdder:
思想:分段锁 CAS,分片执行,结果汇总
例:1000 个线程分为 4 个任务,每个任务 250 个线程,执行完后汇总
3. Unsafe
J.U.C 包里面的 AtomicInteger 等原子类的方法调用了 Unsafe 类的 CAS 操作。
AtomicInteger:
-
内部调用
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
本地方法,compareAndSwapInt 定义在 jdk8u: unsafe.cpp 中// AtomicInteger.class public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
-
compareAndSwapInt 调用 cmpxchg(compare and exchange)方法
// jdk8u: unsafe.cpp UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END
-
cmpxchg 方法执行逻辑
以下是 cmpxchg 在 JDK 8,Linux 操作系统,X86 处理器环境下的实现:
// jdk8u: atomic_linux_x86.inline.hpp inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value) : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) : "cc", "memory"); return exchange_value; }
其详细执行逻辑如下:
-
is_MP(Multi Processor)方法判断 是否为多个处理器,保存到变量 mp 中。
// jdk8u: os.hpp static inline bool is_MP() { return (_processor_count != 1) || AssumeMP; }
-
LOCK_IF_MP
// jdk8u: atomic_linux_x86.inline.hpp #define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
-
如果是在多处理器上运行就为 cmpxchg 指令加上
lock
前缀(lock cmpxchg)。这就使 cmpxchg 指令变成原子操作 -
如果是在单处理器上运行就省略
lock
前缀
-
-
执行 cmpxchgl:比较并交换,操作成功返回比较值(旧值),操作失败返回目标地址中的值
-
“cmpxchgl %1,(%3)”
- “=a” (exchange_value)
- “r” (exchange_value), “a” (compare_value), “r” (dest), “r” (mp)
- “cc”, “memory”
-
输入 exchange_value(交换值,即更新值,%1)、compare_value(比较值,即期待值,%2)、dest(目标地址值,%3)、mp(是否多核,%4) 四个值
-
输出 exchange_value(%0)
cmpxchgl %1,(%3)
即表示cmpxchgl exchange_value,(dest)
- 输入:
"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
,代表把 compare_value 存入 eax 寄存器,把 exchange_value、dest、mp 的值存入任意的通用寄存器 - cmpxchgl 有个隐含操作数 eax,会先比较 eax 的值(也就是 compare_value)和 dest 地址所存的值是否相等
- 相等则把 exchange_value 的值写入 dest 指向的地址
- 不相等则把 dest 地址所存的值更新到 eax 中(因为最终输出 eax 中的值,写入 dest 的值代表更新失败)
- 输出:
"=a" (exchange_value)
,把 eax 中存的值写入 exchange_value 变量中。 - 如果输出的是比较值(说明操作成功),Unsafe_CompareAndSwapInt 方法中
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
会返回ture
,即表示 CAS 成功!否则表示 CAS失败。
-
-
最终实现 cmpxchg = cas 修改变量值
lock 指令在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)
lock cmpxchg 指令
自旋锁
自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态
缺点: 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗 CPU。使用不当会造成 CPU 使用率极高。
1. 实现
非公平不可重入:
当第一个线程 A 获取锁的时候,能够成功获取到,不会进入 while 循环,如果此时线程 A 没有释放锁,另一个线程 B 又来获取锁,此时由于不满足 CAS,所以就会进入 while 循环,不断判断是否满足 CAS,直到 A 线程调用 unlock 方法释放了该锁。
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用 CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
2. 可重入
为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。
public class ReentrantSpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
private int count;
public void lock() {
Thread current = Thread.currentThread();
if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
count++;
return;
}
// 如果没获取到锁,则通过CAS自旋
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur = Thread.currentThread();
if (cur == cas.get()) {
if (count > 0) {
// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
count--;
} else {
// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
cas.compareAndSet(cur, null);
}
}
}
}
3. TicketLock
TicketLock 主要解决的是公平性的问题
public class TicketLock {
/**
* 服务号
*/
private AtomicInteger serviceNum = new AtomicInteger();
/**
* 排队号
*/
private AtomicInteger ticketNum = new AtomicInteger();
/**
* lock:获取锁,如果获取成功,返回当前线程的排队号,获取排队号用于释放锁. <br/>
*/
public int lock() {
int currentTicketNum = ticketNum.incrementAndGet();
while (currentTicketNum != serviceNum.get()) {
// Do nothing
}
return currentTicketNum;
}
/**
* unlock:释放锁,传入当前持有锁的线程的排队号 <br/>
*/
public void unlock(int ticketnum) {
serviceNum.compareAndSet(ticketnum, ticketnum + 1);
}
}
上面的实现方式是,线程获取锁之后,将它的排队号返回,等该线程释放锁的时候,需要将该排队号传入。但这样是有风险的,因为这个排队号是可以被修改的,一旦排队号被不小心修改了,那么锁将不能被正确释放。一种更好的实现方式如下:
缺点:
多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量 serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能
public class TicketLockV2 {
/**
* 服务号
*/
private AtomicInteger serviceNum = new AtomicInteger();
/**
* 排队号
*/
private AtomicInteger ticketNum = new AtomicInteger();
/**
* 新增一个ThreadLocal,用于存储每个线程的排队号
*/
private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>();
public void lock() {
int currentTicketNum = ticketNum.incrementAndGet();
// 获取锁的时候,将当前线程的排队号保存起来
ticketNumHolder.set(currentTicketNum);
while (currentTicketNum != serviceNum.get()) {
// Do nothing
}
}
public void unlock() {
// 释放锁,从ThreadLocal中获取当前线程的排队号
Integer currentTickNum = ticketNumHolder.get();
serviceNum.compareAndSet(currentTickNum, currentTickNum + 1);
}
}
4. CLHLock
CLH 锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋,获得锁。
/**
* CLH的发明人是:Craig,Landin and Hagersten。
* 代码来源:http://ifeve.com/java_lock_see2/
*/
public class CLHLock {
/**
* 定义一个节点,默认的lock状态为true
*/
public static class CLHNode {
private volatile boolean isLocked = true;
}
/**
* 尾部节点,只用一个节点即可
*/
private volatile CLHNode tail;
private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>();
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER =
AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");
public void lock() {
// 新建节点并将节点与当前线程保存起来
CLHNode node = new CLHNode();
LOCAL.set(node);
// 将新建的节点设置为尾部节点,并返回旧的节点(原子操作)
// 这里旧的节点实际上就是当前节点的前驱节点
CLHNode preNode = UPDATER.getAndSet(this, node);
if (preNode != null) {
// 前驱节点不为null表示当锁被其他线程占用
// 通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁
while (preNode.isLocked) {
}
preNode = null;
LOCAL.set(node);
}
// 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁
}
public void unlock() {
// 获取当前线程对应的节点
CLHNode node = LOCAL.get();
// 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态置为false,表示当前线程释放了锁
if (!UPDATER.compareAndSet(this, node, null)) {
node.isLocked = false;
}
node = null;
}
}
5. MCSLock
MCSLock 则是对本地变量的节点进行循环。
/**
* MCS:发明人名字John Mellor-Crummey和Michael Scott
* 代码来源:http://ifeve.com/java_lock_see2/
*/
public class MCSLock {
/**
* 节点,记录当前节点的锁状态以及后驱节点
*/
public static class MCSNode {
volatile MCSNode next;
volatile boolean isLocked = true;
}
private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();
// 队列
@SuppressWarnings("unused")
private volatile MCSNode queue;
// queue更新器
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER =
AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");
public void lock() {
// 创建节点并保存到 ThreadLocal 中
MCSNode currentNode = new MCSNode();
NODE.set(currentNode);
// 将queue设置为当前节点,并且返回之前的节点
MCSNode preNode = UPDATER.getAndSet(this, currentNode);
if (preNode != null) {
// 如果之前节点不为null,表示锁已经被其他线程持有
preNode.next = currentNode;
// 循环判断,直到当前节点的锁标志位为false
while (currentNode.isLocked) {
}
}
}
public void unlock() {
MCSNode currentNode = NODE.get();
// next为null表示没有正在等待获取锁的线程
if (currentNode.next == null) {
// 更新状态并设置queue为null
if (UPDATER.compareAndSet(this, currentNode, null)) {
// 如果成功了,表示queue==currentNode,即当前节点后面没有节点了
return;
} else {
// 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待
// 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)
while (currentNode.next == null) {
}
}
} else {
// 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null
currentNode.next.isLocked = false;
currentNode.next = null;
}
}
}
6. CLHLock 和 MCSLock 比较
- 都是基于链表,不同的是 CLHLock 是基于隐式链表,没有真正的后续节点属性,MCSLock 是显示链表,有一个指向后续节点的属性
- 将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了 TicketLock 多处理器缓存同步的问题
volatile
处理器如何实现原子操作
处理器提供 总线锁定 和 缓存锁 定两个机制来保证复杂内存操作的原子性。
-
使用总线锁保证原子性
所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
-
使用缓存锁保证原子性
底层用 lock 实现,如果是多核添加 lock 指令。
lock 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效
另外还提供了有序的指令无法越过这个内存屏障的作用
volatile 变量自身具有以下特性:
- [可见性](#1. 保证线程可见性):对一个 volatile 变量的读,总是能看到 (任意线程) 对这个 volatile 变量最后的写入。
- [禁止指令重排](#2. 禁止指令重排)
想了解以上特性的原理,需先了解 [处理器缓存](#0. 处理器缓存)。
0. 处理器缓存
CPU 缓存(Cache Memory)是位于 CPU 与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。
L1(一级缓存)分为 数据缓存 和 指令缓存,L2(二级缓存)和 L3(三级缓存)只有 数据缓存
作用:
缓存的出现主要是为了解决 CPU 运算速度与内存读写速度不匹配的矛盾,因为 CPU 运算速度要比内存读写速度快很多,这样会使 CPU 花费很长时间等待数据到来或把数据写入内存。
读缓存: CPU 依次从一级缓存、二级缓存、三级缓存中获取数据,若未命中则到内存中获取,再去更新缓存
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是 2 的整数幂个连续字节,一般为 32-256 个字节。最常见的缓存行大小是 64 个字节。
因此当 CPU 在执行一条读内存指令时,它是会将内存地址所在的缓存行大小的内容都加载进缓存中的。也就是说,一次加载一整个缓存行。
写操作: 两种模式
-
直写(write-through):**更新内存数据再更新缓存(或丢弃)。**保证该数据在缓存与内存中一致
透过本级缓存,直接把数据写到下一级缓存(或直接到内存)中。
如果对应的段被缓存了,会同时更新缓存中的内容(甚至直接丢弃)。
-
回写(write-back):**先更新缓存,再由缓存回写至内存。**缓存暂时与内存不一致,但最终会写回内存。
仅修改本级缓存中的数据,并且把对应的缓存段标记为 “脏” 段。
脏段会触发回写,也就是把里面的内容写到对应的内存或下一级缓存中。
1. 保证线程可见性
对一个 volatile 变量的读,总是能看到 (任意线程) 对这个 volatile 变量最后的写入。
1.1. 缓存一致性协议
在多核处理器系统中,每个处理器核心都有它们自己的一级缓存、二级缓存等。
这样一来当多个处理器核心在对共享的数据进行写操作时,就需要 保证该缓存数据在所有处理器核心中的可见性 / 一致性。
1.2. 窥探技术
所有内存传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线。
- 同一个指令周期中,只有一个缓存可以读写内存,会通知其他处理器
- 缓存不断地窥探总线上发生的数据交换。只要某个处理器写内存,其他处理器马上就知道这块内存在它们自己的缓存中对应的缓存行已经失效。
1.3. MESI 协议
MESI 是缓存行四种状态的首字母缩写,任何多核系统中的缓存行都处于这四种状态之一。
-
失效(Invalid): 该处理器缓存中无该缓存行,或缓存中的缓存行已经失效了。
-
共享(Shared): 多组缓存都可以拥有指向同一内存地址的缓存行。且缓存行只能被读取,不能被写入。
该状态下缓存行数据是主内存的一份拷贝,其数据与主内存数据保持一致。
-
独占(Exclusive): 如果一个处理器持有了某个「独占」状态的缓存行,其他处理器中的同一缓存行会变成「失效」状态。
缓存行数据是主内存的一份拷贝,其数据与主内存数据保持一致。
-
已修改(Modified): 属于脏段,表示该缓存行已经被所属的处理器修改了。如果一个缓存行处于「已修改」状态,那么它在其他处理器缓存中的拷贝马上会变成「失效」状态。
已修改缓存行如果被丢弃或标记为「失效」状态,那么先要把它的内容回写到内存中,即需保证已经修改的数据一定要回写至内存。
写操作过程:
只有当缓存行处于「独占」状态或「已修改」状态时处理器才能对其进行写操作
当处理器想对某个缓存段进行写操作时,如果它没有独占权
- 会先发送一条申请独占权的请求给总线,这会通知其他处理器
- 其他处理器把它们拥有的同一缓存行的拷贝改为「失效」状态
- 修改数据,更改状态为「已修改」状态
读操作过程:
当处理器想对某个缓存段进行读操作时
-
若缓存行处于「独占」状态或「已修改」状态时,直接读
-
若其他处理器中有同一缓存行的拷贝且处于「独占」状态或「已修改」状态时(由于窥探总线技术,所以也会知道)需把状态改为「共享」状态才能进行读操作
其他处理器中的缓存行若为「已修改」状态需先把它的内容回写到内存中
操作系统通过内存屏障保证缓存间的可见性,JVM 通过给 volatile 变量加入内存屏障保证线程之间的可见性。
其实,volatile 对于可见性的实现,内存屏障也起着至关重要的作用。因为内存屏障相当于一个数据同步点,他要保证在这个同步点之后的读写操作必须在这个点之前的读写操作都执行完之后才可以执行。并且在遇到内存屏障的时候,缓存数据会和主存进行同步,或者把缓存数据写入主存、或者从主存把数据读取到缓存。
已经有了缓存一致性协议,为什么还需要 volatile?
-
并不是所有的硬件架构都提供了相同的一致性保证,Java 作为一门跨平台语言,JVM 需要提供一个统一的语义。
-
操作系统中的缓存和 JVM 中线程的本地内存并不是一回事,通常我们可以认为:MESI 可以解决缓存层面的可见性问题。使用 volatile 关键字,可以解决 JVM 层面的可见性问题。
-
缓存可见性问题的延伸:由于传统的 MESI 协议的执行成本比较大。所以 CPU 通过 Store Buffer 和 Invalidate Queue 组件来解决,但是由于这两个组件的引入,也导致缓存和主存之间的通信并不是实时的。也就是说,缓存一致性模型只能保证缓存变更可以保证其他缓存也跟着改变,但是不能保证立刻、马上执行。
2. 禁止指令重排
DCL 单例:
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized(Singleton.class) {
if (singleton == null) {
// 若 singleton 没有加 volatile 会出现指令重排问题
singleton = new Singleton();
}
}
}
return singleton;
}
}
创建对象步骤: T t = new T();
对应指令
0 new #2 <com/test/jvm/volatile/T>
3 dup
4 invokespecial #3 <com/test/jvm/volatile/T.<init>>
7 astore_1
8 return
- 申请内存:new 命令
- 初始化成员变量:invokespecial 命令
- 赋值:astore_1 将对象引用返回给 t
由于指令重排,2 和 3 可能会互换位置。这时变量可能会先拿到一个尚未初始化成员变量的对象,若刚好此时有线程进入 DCL 会直接拿到该变量去使用
读写屏障
- loadfence
- storefence
AQS
1. CountDownLatch
概述: 用来控制一个线程等待多个线程
原理:
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒
API:
- await(): 调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
- await(long timeout, TimeUnit unit): 和 await() 类似,只不过等待一定的时间后 count 值还没变为 0 的话就会继续执行
- countDown(): 将 count 值减 1
实例:
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
// run..run..run..run..run..run..run..run..run..run..end
场景:
-
启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行
-
实现多个线程开始执行任务的最大并行性
CountDownLatch(1),多个线程挂起,当主线程调用 countDown() 时,多个线程同时被唤醒
不足:
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
2. CyclicBarrier
概述: 用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行
原理:
线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行
方法:
- await()
- await(long timeout, TimeUnit unit): 设置超时时间,超过该时间仍然还有线程还没到达屏障则忽略这些线程,将等待的线程全部释放
区别: CyclicBarrier 和 CountdownLatch 的区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以 循环使用,所以它才叫做循环屏障
构造器: CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会选择一个线程执行一次
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
实例:
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
// before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
重用:
public class Test {
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for (int i = 0; i < N; i++) {
new Writer(barrier).start();
}
try {
Thread.sleep(25000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("CyclicBarrier重用");
for (int i = 0; i < N; i++) {
new Writer(barrier).start();
}
}
static class Writer extends Thread {
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据...");
try {
//以睡眠来模拟写入数据操作
Thread.sleep(5000);
System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch(BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "所有线程写入完毕,继续处理其他任务...");
}
}
}
3. Phaser
main:
public static void main(String[] args) {
phaser.bulkRegister(7);
for (int i = 0; i < 5; i++) {
new Thread(new Person("p" + i)).start();
}
new Thread(new Person("新郎")).start();
new Thread(new Person("新娘")).start();
}
Phaser:指定什么阶段做什么事
public ass MarriagePhaser extends Phaser {
// phase: 阶段编号: registeredParties: 此阶段注册人数
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
// ...
return false;
case 1:
// ...
return false;
case 2:
// ...
return false;
case 3:
// ...
return true;
default:
return true;
}
}
}
run:如何到达阶段逻辑
public class Person implements Runnable {
@Override
public void run() {
// doing something
// 等待所有注册的线程全部到达后执行
phaser.arriveAndAwaitAdvance();
if (name.equals("新郎") || name.equals("新娘")) {
System.out.printf("%s 洞房!\n", name);
phaser.arriveAndAwaitAdvance();
}
// 指定注销
else {
phaser.arriveAndDeregister();
//phaser.register()
}
}
}
4. ReadWriteLock
StampedLock
- 共享锁
- 排它锁
ReadWriteLock readWriteLock = new ReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
5. Semaphore
概述: Semaphore 类似于操作系统中的信号量,可以 控制对互斥资源的访问线程数
原理:
acquire() 获取一个许可,如果没有就等待
release() 释放一个许可
构造器:
- Semaphore(int permits): 参数 permits 表示许可数目,即同时可以允许多少线程进行访问
- Semaphore(int permits, boolean fair): 这个多了一个参数 fair 表示是否是公平的,即等待时间越久的越先获取许可
API:
- acquire(): 获取一个许可
- acquire(int permits): 获取 permits 个许可
- release(): 释放一个许可
- release(int permits): 释放 permits 个许可
实例:
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
// 2 1 2 2 2 2 2 1 2 2
6. Exchanger
线程交换
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
String s1 = "T1";
try {
// 阻塞等待交换,交换后才能继续执行
s1 = exchanger.exchange(s1);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + s1);
}, "t1").start();
new Thread(() -> {
String s2 = "T2";
try {
s2 = exchanger.exchange(s2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + s2);
}, "t2").start();
// t1:T2
// t2:T1
7. LockSupport
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(i);
if (i == 5) {
// 停止当前线程
LockSupport.park();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// 继续执行
LockSupport.unpark(t);
8. AQS 源码
内部维护一个 state 和一个双向线程链表
- ReentrantLock#lock
-
AbstractQueuedSynchronizer#compareAndSetState:CAS 把 state 从 0 变为 1,若成功则代表拿到锁
-
AbstractOwnableSynchronizer#setExclusiveOwnerThread:若抢到锁,则设置当前线程为独占线程
-
AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
ReentrantLock.NonfairSync#tryAcquire
- ReentrantLock.Sync#nonfairTryAcquire:
- state 为 0,则继续调用 compareAndSetState 抢锁(尝试把 state 变为 1,成功则接着调用 setExclusiveOwnerThread)
- state 为 > 0,且当前线程是独占访问的那个线程(说明锁重入),则 state++(此时 state 代表重入线程数)
- 否则 TryAcquire 失败
- ReentrantLock.Sync#nonfairTryAcquire:
-
TryAcquire 失败则调用 AbstractQueuedSynchronizer#addWaiter:使用 CAS 加入链表队列
// jdk 8 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { // jdk 9 使用 VarHandler.set(this, pred)。 // VarHandler 内部有 CAS 的方法 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
jdk 9 使用
VarHandler.set(this, pred)
代替node.prev = pred;
,其调用 native 实现(相当于直接操纵二进制码),效率比反射高VarHandler 指向一个变量
// 指定某类下某名某类型的变量 VarHandler handle = MethodHandles.lookup().findVarHandle(TestClass.class, "test", int.class); // 使用 TestClass testClass = new TestClass(); handle.set(testClass, 9); // 把该对象中的 test 属性变为 9 handle.compareAndSet(testClass, 9, 10); // 通过 CAS 更改值 handle.getAndAdd(testClass, 10); // 原子性添加值 => x+=10 的原子版本
- AbstractQueuedSynchronizer#acquireQueued:加入队列后不断监听前一个节点,若前节点为头结点(已拿到锁),则试图去抢锁,成功则返回 false(不中断)
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
-
-
CompletableFuture
管理多个线程运行后返回的结果
- allOf():所有任务全部完成才结束
- anyOf():任意一个任务完成才结束
CompletableFuture<Double> futureTM = CompletableFuture.supplyAsync(() -> priceOfTM())
// 对结果进行处理
.thenApply(String::valueOf)
.thenApply(str -> "price " + str)
.thenAccept(System.out::println);
CompletableFuture<Double> futureTB = CompletableFuture.supplyAsync(() -> priceOfTB());
CompletableFuture<Double> futureJD = CompletableFuture.supplyAsync(() -> priceOfJD());
CompletableFuture.allOf(futureTM, futureTB, futureJD).join();
ThreadLocal
把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题
使用:
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
threadLocal.remove();
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
threadLocal.remove();
});
thread1.start();
thread2.start();
}
}
// 1
原理:
ThreadLocal#set:把值存到当前线程的 ThreadLocalMap 中
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
// 线程中的 ThreadLocalMap 类型字段
return t.threadLocals;
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
内存泄露:
ThreadLocal 对象 new 后有强引用,而当前线程中的 ThreadLocalMap 对象的键也有 ThreadLocal 对象的弱引用,所以当 ThreadLocal 对象失去强引用时 ThreadLocalMap 中对应的键也会变为 null,防止了内存泄露。
虽然 ThreadLocalMap 的键为 null 了,但是其 value 值还存在所以依然会有内存泄露,所以需要执行 ThreadLocal#remove 方法。
进程、线程、纤程(协程)
线程在内核态
纤程在用户态,可以启动的数据比线程多