JUC入门笔记
JUC入门笔记,参考B站 尚硅谷 (opens new window) 和 狂神 (opens new window) 的教程,后续有机会补充其他内容,这俩教程相似度极高我不做评价......
# 多线程基础
进程:进程就像是在电脑上运行的一个程序,编写一个Java程序,java -jar
运行起来之后就是一个进程
线程:线程是进程中的更小的执行单元,一个进程可以有多个线程
串行:单线程逻辑,依次执行任务,可理解为先喝牛奶在看书
并行:多线程逻辑,同时执行任务,可理解为边喝牛奶边看书
并发:指大量任务同时涌入,可能是一群人来喝牛奶,也可能是一个人不断的喝牛奶,与多线程单线程并没有直接关系
# 创建线程的方法
基础阶段有两种方法,分别是实现Runnable
接口或继承Thread
类,复写其中的run
方法,最后调用start
方法启动,Thread 类也是实现了 Runnable 接口
后续 JUC 中还会提到Callable
带有返回结果的线程,和Executors
线程池工具类,加上面两个共四种方法
# 线程常见方法
线程中常用的静态方法
方法名 | 解释说明 |
---|---|
currentThread() | 获取当前线程对象 |
sleep(long millis) | 使当前线程睡眠 millis 毫秒 |
yield() | 让出CPU执行权,等待下次调度 |
activeCount() | 获取当前线程组中的活动线程数量 |
线程中常用的成员方法
方法名 | 解释说明 |
---|---|
run() | 线程执行的逻辑代码都在 run 方法中,需要手动复写 |
start() | 启动线程,使线程进入就绪状态随时准备接受调度 |
getState() | 获取线程状态,参考Thread.State枚举类,共有六种状态:新建,就绪,阻塞,等待,超时等待,终止 |
getName() | 获取线程名称 |
setName(String name) | 设置线程名称 |
getPriority() | 获取线程优先级,优先级范围1~10,1为最低10为最高 |
setPriority(int newPriority) | 设置线程优先级,只是一种优化不能绝对保证 |
getDaemon() | 获取线程守护状态,默认线程都是用户线程,设置为true代表守护线程 |
setDaemon(boolean on) | 设置守护线程状态,守护线程会随着用户线程的停止而停止 |
join() | 插队方法,常见于 t1 和 t2 并行执行,当 t1 某个流程需要 t2 的结果才可以继续执行时就在 t1 中调用t2.join() 让 t2 执行完成,期间 t1 属于等待状态 |
interrupt() | 中断线程的运行 |
# 线程间通信
线程间通信主要靠这三个方法
方法名 | 解释说明 |
---|---|
wait() | Object 方法,使线程进入等待状态,被唤醒后可继续运行 |
notify() | Object 方法,唤醒指定线程 |
notifyAll() | Object 方法,唤醒所有等待中的线程 |
# 线程生命周期
线程生命周期有七种状态,除开运行状态之外其他都可以在Thread.State
中找到
- NEW 新建:new Thread() 线程对象创建后就进入新建状态
- RUNNABLE 就绪:当调用 start 方法后就进入就绪状态,随时等待 CPU 的调度
- RUNNING 运行:被 CPU 调度并执行时就属于运行状态
- BLOCKED 阻塞:当代码始终获取不到锁无法继续执行时属于阻塞状态
- WAITING 等待:当执行 wait、await,join 等方法时会进入阻塞状态,需要等到其他线程唤醒才可以继续执行
- TIMED_WAITING 超时等待:当执行 sleep 或者带有时间参数的等待方法时会进入等待状态,时间过后会自动唤醒
- TERMINATED 终止:线程运行结束,进入终止状态
# 并发问题
回到一开始的喝牛奶,并发可以理解为多人喝牛奶,人太多了不小心牛奶洒了就是并发问题
解决并发问题使用synchronized
关键字,可以在方法中使用 synchronized 维护一个锁对象解决并发问题,也可以直接把关键字写在方法上更简单方便
- 写在成员方法上:默认锁的是当前对象
- 写在静态方法上:默认锁的是当前类的class
集合中并发问题的处理:
- List 集合推荐使用
CopyOnWriteArrayList
解决并发问题 - Set 集合推荐使用
CopyOnWriteArraySet
解决并发问题 - Map 集合推荐使用
CurrentHashMap
解决并发问题
# Lock锁
Lock 锁指的是java.util.concurrent.locks.Lock
接口类,是 JUC 中的一个重点
方法名 | 解释说明 |
---|---|
lock() | 获取锁,如果锁被占用则进入阻塞状态 |
tryLock() | 非阻塞获取锁,获取到返回true,否则返回false |
tryLock(long time, TimeUnit unit) | 超时阻塞获取锁,获取到返回true,否则返回false |
unlock() | 释放锁 |
newCondition() | 基于该锁创建条件对象,用于线程间通信 |
lockInterruptibly() | 获取锁,允许响应中断,被中断后抛出异常 |
# 各种锁使用
重入锁&递归锁
Lock 接口下最常用的就是 ReentrantLock
实现类,属于可重入锁又叫递归锁,顾名思义它允许同一个线程在持有该锁时,多次进入需要该锁的同步代码块或方法不会被阻塞,synchronized
也属于可重入锁,Java中提供的都是可重入锁,并没有提供非重入锁的实现
public class ReentryLock {
// 创建可重入锁对象
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
// 获取锁
lock.lock();
try {
// 执行业务逻辑...
// 可以在已经获取锁的状态下再次获取锁,这种就叫可重入锁
// 需要注意的是获取几次锁就需要释放几次锁,否则可能会造成死锁状态
} finally {
// 释放锁
lock.unlock();
}
}).start();
}
}
公平锁与非公平锁
ReentrantLock
与syncronized
默认都属非公平锁,可能会出现某个线程大量工作其他线程工作量小甚至没有工作的不公平分配现象,被称为非公平锁,线程分配不到工作的现象又被称为线程饥饿
ReentrantLock 锁提供了个有参的构造,接收一个 bool 类型的值,在创建锁时可以传入 true 实现公平锁
公平锁让每个线程都可以公平的分配任务进行工作,是因为底层维护了一个线程队列记录了线程执行顺序 ,为了保证公平分配任务额外做了一些同步和判断,所以说公平锁对性能有一部分牺牲,一般情况下不建议使用
读写锁 共享锁与独享锁
读写锁对应java.util.concurrent.locks.ReadWriteLock
接口类,通常使用他的实现ReentrantReadWriteLock
可重入读写锁类,接口中分别提供了方法获取读锁和写锁
方法名 | 解释说明 |
---|---|
Lock readLock() | 获取读锁 Lock 对象,后续进行上锁和解锁 |
Lock writeLock() | 获取写锁 Lock 对象,后续进行上锁和解锁 |
读锁:在读取数据前上锁,单纯并发读取操作并不会对数据造成什么影响,所以读锁属于共享锁允许并发读取数据
写锁:在写入数据前上锁,防止多条线程同时修改数据,保证数据的一致性,写锁属于独享锁,又被称为独占锁或排他锁
读读共享,写写独享,读写互斥,利用这个特点对锁进行优化可以提高并发场景下的读写性能
public class ReadWriteLockTest {
public static void main(String[] args) {
Cache cache = new Cache();
for (int i = 0; i < 5; i++) {
final String num = String.valueOf(i + 1);
new Thread(() -> {
cache.put(num, num);
}, "AA").start();
}
for (int i = 0; i < 5; i++) {
final String num = String.valueOf(i + 1);
new Thread(() -> {
Console.log("通过{}读取到的数据为{}", num, cache.get(num));
}, "BB").start();
}
}
private static class Cache {
private volatile Map<String, Object> cache = MapUtil.newHashMap();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
lock.writeLock().lock();
try {
Console.log("{}正在执行{}写操作", Thread.currentThread().getName(), key);
cache.put(key, value);
} finally {
Console.log("{}缓存{}写入完成", Thread.currentThread().getName(), key);
lock.writeLock().unlock();
}
}
public Object get(String key) {
lock.readLock().lock();
try {
Console.log("{}正在执行{}读操作", Thread.currentThread().getName(), key);
return cache.get(key);
} finally {
Console.log("{}缓存{}读取完成", Thread.currentThread().getName(), key);
lock.readLock().unlock();
}
}
}
}
死锁
死锁值两个或多个线程都在等待对方释放锁,或者由于某种原因未能释放锁导致线程一直阻塞无法继续执行,就被成为死锁状态
有些代码看起来像是进入死锁状态,但实际上根据业务逻辑来讲并不会产生死锁,针对这种模糊的情况可以使用jps -l
打印进程信息,找到对应的进程ID使用jstack
命令查看线程信息,可以判断是否出现死锁
# 定制化通信
Lock 接口中提供了newCondition
方法可以创建条件对象,使用条件对象进行等待和唤醒可以实现定制化通信
方法名 | 解释说明 |
---|---|
await() | 使线程进入等待状态 |
awaitUninterruptibly() | 使线程进入等待状态,但不会响应中断 |
signal() | 唤醒一个等待中的线程 |
signalAll() | 唤醒所有等待中的线程 |
awaitNanos(long nanosTimeout) | 超时等待,时间过后自动唤醒,也可以被提前唤醒 |
awaitUntil(Date deadline) | 使线程进入等待状态,到指定的时间自动唤醒,也可以被提前唤醒 |
# 虚假唤醒
线程通信经典操作就是wait/await
等待和notifyi/signal
唤醒,而等待唤醒中有个特点,就是在什么地方等待,就在什么地方唤醒,在并发状态下可能会产生一些问题
- 正常流程:线程 A 在执行代码时通过 if 判断得知当前不满足运行条件时等待,而后线程 B 开始执行代码,当 B 执行对应操作后使得 A 满足了运行条件,那么就需要在 B 中唤醒 A,A 被唤醒后继续向下执行
- 异常流程:线程 A 在执行代码时通过 if 判断得知当前不满足运行条件时等待,而后线程 B 开始执行代码,当 B 执行对应操作后使得 A 满足了运行条件,那么就需要在 B 中唤醒 A,但此时线程 C 又执行了某些代码,使得 A 的条件又不被满足,但是 A 已经被唤醒开始继续执行,于是 A 就在条件并不满足的情况下继续执行,很有可能会出问题,这种就被成为线程的虚假唤醒
对于虚假唤醒的解决方案就是将 if 替换为 while,在线程每次被唤醒时都要进行条件判断,如果被唤醒后条件被改变就循环一次继续等待,直至唤醒后条件满足,走出 while 循环执行后续逻辑
# Lock 锁总结
- synchronized 内置的Java关键字, Lock 是个接口类,有多种实现支持各种锁
- synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- synchronized 会自动释放锁,lock 必须要手动释放锁,否则可能会造成死锁现象
- synchronized 简单方便适合少量的代码同步问题,Lock 锁灵活可控适合大量的代码同步问题
# JUC辅助类
# CountDownLatch
减少计数工具类,创建对象时需要给一个初始大小,核心方法就是await
等待和countDown
减少计数,每次调用 countDown 方法都会对初始值 -1,当值为 0 时 await 等待状态就被唤醒了
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
// 构建线程计数器,初始值为8
CountDownLatch count = new CountDownLatch(8);
// 循环创建8个线程,每个线程都进行countDown
for (int i = 1; i <= 8; i++) {
new Thread(() -> {
// 每执行一个线程,count都-1
System.out.println(Thread.currentThread().getName() + " 号同学离开了教室");
count.countDown();
}, String.valueOf(i)).start();
}
// 主线程进入等待,当count为0时自动唤醒
count.await();
System.out.println("班长最后一个走,然后锁门");
}
}
# CyclicBarry
循环栅栏工具类,创建对象时需要给一个初始大小以及回调方法,当等待线程数量达到初始大小后就会触发回调方法,回调方法执行完成后每个线程的等待状态也全被唤醒,与 CountDownLatch 不同的是每个线程被唤醒后继续执行后面的代码,可以再次进入等待状态,当再次满足条件后回调会继续执行,这就是循环栅栏的概念
相比 CountDownLatch 多了个reset
重置方法,让正在等待的线程抛出异常,重新开始计数
public class CyclicBarrierTest {
public static void main(String[] args) {
System.out.println("欢迎来到英雄联盟\n");
// 提供一个初始值和回调
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("----------------"));
// 循环构建七个线程,当七个线程都使用CyclicBarrier阻塞后,回调就执行了
for (int i = 1; i <= 7; i++) {
final int index = i;
new Thread(() -> {
String name = Thread.currentThread().getName();
try {
System.out.println("第" + index + "位玩家已点击确认");
cyclicBarrier.await();
System.out.println("第" + index + "位玩家已选择召唤师");
cyclicBarrier.await();
System.out.println("第" + index + "位玩家已加载完成");
cyclicBarrier.await();
System.out.println("第" + index + "位玩家已进入游戏!");
} catch (Exception e) {
throw new RuntimeException(e);
}
}, String.valueOf(i)).start();
}
}
}
# Semaphore
信号灯工具类,可以限制某一区域的线程数量,核心方位为acquire
获得许可和release
释放许可
public class SemaphoreTest {
public static void main(String[] args) {
// 模拟停车场,有3个停车位
Semaphore semaphore = new Semaphore(3);
// 创建8个线程模拟8辆车进入停车场的流程
Random random = new Random();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
try {
semaphore.acquire();
int timestamp = (random.nextInt(3) + 1) * 1000;
Console.log("{}抢到了车位,停车{}ms", Thread.currentThread().getName(), timestamp);
Thread.sleep(timestamp);
} catch (Exception e) {
e.printStackTrace();
} finally {
Console.log("{}离开了车位", Thread.currentThread().getName());
semaphore.release();
}
}, "车" + (i + 1)).start();
}
}
}
# Atomic
Atomic 原子类指的是 java.util.concurrent.atomic
包下的类,支持线程安全下的数字加减操作,例如在 lambda 表达式中使用 AtomicInteger
原子类实现计数器功能,可在并行流下使用
# Callable
Callable 是第三种创建线程的方式,相比 Thread 和 Runnable 多了一个返回值,需要配合TaskFuture
使用获取返回结果
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 构建Callable线程对象
Callable<Integer> callable = () -> 200;
// 由于Thread类并不能接收Callable对象,所以这里将Callable封装为FutureTask对象
FutureTask<Integer> future = new FutureTask<>(callable);
// 创建线程并启动
new Thread(future).start();
// 获取线程返回结果
System.out.println(future.get());
}
}
# 阻塞队列
阻塞队列使用的是java.util.concurrent.BlockingQueue
接口,该接口下有两个主要的实现类:
- ArrayBlockingQueue:基于数组固定长度(有界)的阻塞队列
- LinkedBlockingQueue:基于链表无固定长度(无界)的阻塞队列,可手动指定长度限制
接口中提供了三组API对阻塞队列进行增删查操作
异常 | 返回 | 等待 | 超时 | |
---|---|---|---|---|
插入 | add() | offer() | put() | poll(E e, long timeout, TimeUnit unit) |
删除 | remove() | poll() | take() | poll(long timeout, TimeUnit unit) |
检查 | element() | peek() |
public class BlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
// 构建阻塞队列,队列长度为3
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); // 默认为 Integer.MAX_VALUE
// executeThrow(queue);
// executeNull(queue);
// executeWait(queue);
// executeWaitTime(queue);
}
/**
* 操作队列,会抛出异常
*/
private static void executeThrow(BlockingQueue<Integer> queue) {
// 向队列中添加元素,超出长度会抛出异常
queue.add(22);
queue.add(33);
queue.add(44);
// queue.add(55);
// 检查元素,如果队列为空会抛出异常
// Console.log("检查:{}", queue.element());
// 获取队列中的元素,如果队列为空会抛出异常
Console.log("获取:{}", queue.remove());
Console.log("获取:{}", queue.remove());
Console.log("获取:{}", queue.remove());
// System.out.println(queue.remove());
}
/**
* 操作队列,返回false或者null
*/
private static void executeNull(BlockingQueue<Integer> queue) {
// 向队列中添加元素,超出长度会返回false
Console.log(queue.offer(22));
Console.log(queue.offer(33));
Console.log(queue.offer(44));
Console.log(queue.offer(55));
// 检查元素,如果队列为空会返回null
Console.log("检查:{}", queue.peek());
// 获取队列中的元素,如果队列为空会返回null
Console.log("获取:{}", queue.poll());
Console.log("获取:{}", queue.poll());
Console.log("获取:{}", queue.poll());
Console.log("获取:{}", queue.poll());
// 检查元素,如果队列为空会返回null
Console.log("检查:{}", queue.peek());
}
/**
* 操作队列,支持阻塞线程
*/
private static void executeWait(BlockingQueue<Integer> queue) throws InterruptedException {
// 向队列中添加元素,超出长度会阻塞线程,当队列有空闲位置时在执行
queue.put(22);
queue.put(33);
queue.put(44);
// queue.put(55);
// 获取队列中的元素,队列为空会阻塞线程,当队列有有元素时在执行
Console.log("获取:{}", queue.take());
Console.log("获取:{}", queue.take());
Console.log("获取:{}", queue.take());
// Console.log("获取:{}", queue.take());
}
/**
* 操作队列,支持限时阻塞线程
*/
private static void executeWaitTime(BlockingQueue<Integer> queue) throws InterruptedException {
// 向队列中添加元素,超出长度会阻塞线程,超出时间会返回false
Console.log(queue.offer(22, 2L, TimeUnit.SECONDS));
Console.log(queue.offer(33, 2L, TimeUnit.SECONDS));
Console.log(queue.offer(44, 2L, TimeUnit.SECONDS));
Console.log(queue.offer(44, 2L, TimeUnit.SECONDS));
// 获取队列中的元素,队列为空会阻塞线程,超出时间会返回false
Console.log("获取:{}", queue.poll(2L, TimeUnit.SECONDS));
Console.log("获取:{}", queue.poll(2L, TimeUnit.SECONDS));
Console.log("获取:{}", queue.poll(2L, TimeUnit.SECONDS));
Console.log("获取:{}", queue.poll(2L, TimeUnit.SECONDS));
}
}
# 线程池
线程池指的是java.util.concurrent.ExecutorService
接口,记录一下常用方法
方法名 | 解释说明 |
---|---|
execute(Runnable command) | 提交一个 Runnable 任务给线程池执行 |
submit(Callable<T> task) | 提交一个 Callable 任务给线程池执行 |
shutdown() | 不在接收新的任务,待池内任务完成后就关闭线程池 |
awaitTermination(10, TimeUnit.SECONDS) | 超时等待,当池内任务全部执行完成,或者达到超时时间后自动唤醒 |
# 常见线程池
线程池可以通过Executors
工具类进行创建,常见的有三个线程池
- Executors.newFixedThreadPool(int nThreads):固定数量的线程池
- Executors.newCachedThreadPool():根据并发量动态扩容的线程池
- Executors.newSingleThreadExecutor():单线程池
public class ExecutorsTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
// ExecutorService pool = Executors.newCachedThreadPool();
// ExecutorService pool = Executors.newSingleThreadExecutor();
try {
for (int i = 1; i <= 10; i++) {
final int num = i;
pool.execute(() -> Console.log("{}线程运行了{}", Thread.currentThread().getName(), num));
}
} finally {
pool.shutdown();
}
}
}
其他线程池,后续接触到随时补充:
- Executors.newScheduledThreadPool(int corePoolSize):定时动态扩容线程池,可在规定时间内或周期性执行定时任务
- schedule(Runnable command, long delay, TimeUnit unit):延迟一段时间后执行任务
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):周期性执行定时任务
# 线程池状态
- RUNNING:线程池正常运行,正常处理队列中的任务,同时接收新任务
- SHUTDOWN:执行
shutdown
方法后,线程处于正在关闭状态,队列中的任务执行结束后就停止,不会接收新的任务 - STOP:执行
shutdownnow
方法后,线程处于停止状态,队列中的任务不会执行,也不接收新的任务,并且正在运行的线程也会中断 - TIDYING:当线程池内没有正在运行的线程时会进出TIDYING状态,触发
terminated
方法,如有需要可以自行实现这个方法在线程池停止前进行一些处理 - TERMINATED:
terminated
方法执行完成后线程池就进入TERMINATED状态彻底停止
# 自定义线程池
线程池底层与七大参数
使用Executors
构建线程池的方法,随便找一个点进去看源码都会发现最终创建的是ThreadPoolExecutor
实例(有些线程池需要多往里找几层才能看到)
这里整理一下创建ThreadPoolExecutor
实例所需要的七大参数
参数列表 | 参数解释 |
---|---|
int corePoolSize | 核心线程池大小,不会被回收 |
int maximumPoolSize | 最大核心线程池大小,当核心线程数量不够会自动扩容,最大不超过该值 |
long keepAliveTime | 线程超时时间,自动扩容的线程空闲时间超过该值后会被回收 |
TimeUnit unit | 线程超时单位,对应超时时间的单位 |
BlockingQueue<Runnable> workQueue | 线程任务阻塞队列,当并发量较多而线程不够用时就会存放在阻塞队列中 |
ThreadFactory threadFactory | 线程工厂,负责创建线程,一般情况下用默认工厂即可 Executors.defaultThreadFactory() |
RejectedExecutionHandler handler | 拒绝策略, 当线程池资源满载无法接收新任务时的处理方法 |
这里整理一下拒绝策列
拒绝策略 | 作用效果 |
---|---|
ThreadPoolExecutor.AbortPolicy | 资源满载后抛出异常 |
ThreadPoolExecutor.CallerRunsPolicy | 资源满载后将任务交还给调用者去执行 |
ThreadPoolExecutor.DiscardPolicy | 资源满载后直接丢弃任务不做任何处理,也不会抛出异常 |
ThreadPoolExecutor.DiscardOldestPolicy | 资源满载后尝试丢掉队列中最早的任务,新的任务会替换原本的位置实现插队效果 |
阿里巴巴的Java开发手册
1、【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1. newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
2. newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
自定义线程池
自定义线程池中配置核心线程有 8 个,当任务较多时可扩容至最多 32 个线程,阻塞队列中最多可容纳 1024 个任务进行排队,线程和队列中都满载的情况下会抛出异常,而任务较少时空闲时间超过 5 分钟的线程会被收回,自定义线程池代码:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(8, 32, 5L, TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1024),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
# 分支合并
JUC 中提供了 Fork/Join 分支合并框架,简单来说就是把一件工作量很大的任务按照规则拆分成一个个的小任务并行工作,最后可以将每个小任务的运行结果合并为最终结果
分支合并框架用到的是java.util.concurrent.ForkJoinTask
抽象类,这里记录下他的实现类ForkJoinTask
public class ForkJoinTest {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
try {
Task task = new Task(1, 100, 10);
ForkJoinTask<Integer> submit = pool.submit(task);
System.out.println(submit.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
public static class Task extends RecursiveTask<Integer> {
// 开始位置
private final int start;
// 结束位置
private final int end;
// 跨度
private final int step;
public Task(int start, int end, int step) {
this.start = start;
this.end = end;
this.step = step;
}
@Override
protected Integer compute() {
// 定义返回结果
int result = 0;
// 差距小于等于跨度时直接计算,否则执行else拆分
if (end - start <= step) {
Console.log("计算从{}累加到{}", start, end);
for (int i = start; i <= end; i++) {
result += i;
}
} else {
// 计算中间值
int diff = (end + start) / 2;
// 拆分计算
Task beforeTask = new Task(start, diff, step);
Task afterTask = new Task(diff + 1, end, step);
beforeTask.fork();
afterTask.fork();
Console.log("将[{},{}]拆分为[{}, {}]和[{}, {}]的累加", start, end, start, diff, diff + 1, end);
// 合并结果
result += beforeTask.join() + afterTask.join();
}
// 返回结果
return result;
}
}
}
# Volatile
volatile 关键字通常用于多线程修改访问的某个成员变量上,作用为
保证变量的可见性
Java 中每个线程都有自己的工作内存(Thread-local memory),而所有线程共享同一个主内存(Main Memory),被 volatile 修饰的成员变量会存储到主内存中,当线程 A 操作变量时会先读取变量值到自己的工作内存中,期间对变量的读取和修改都是在自己的工作内存进行的,线程 A 执行结束会将更改写回到主内存中,如果线程 A 对变量进行了修改但是并没有写回,而线程 B 读取到的就是 A 修改之前的变量,使用 volatile 关键字修饰的效果就是当变量被修改可以及时写回到主内存中
禁止指令重排
代码真正运行时并不是按照我们写的顺序去执行的,会对代码进行指令重排,指令重排是指在不改变程序执行结果的前提下可能会对指令执行的顺序进行重新排序,用于提高程序执行性能,指令重排在多线程下操作同一变量可能会产生问题,所以 volatile 会禁用指令重排
volatile 最典型的应用场景就是单例模式的双重检查锁
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
// volatile 保证变量的可见性,禁止指令重排,但并不支持原子性,所以这里需要手动加锁
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}