文章是学习 尚硅谷JUC源码讲授实战教程完整版(java juc线程精讲) 时做的笔记与总结
文章目录
1. JUC 简介
JUC 是指 java.util.concurrent , 在jdk的rt.jar
中 1.5版本就以及引入了 , rj.jar属于bootstrap classloader加载的内存,通过双亲委派机制去加载
JUC下面有多个工具类,如并发集合类:ConcurrentHashMap
CopyOnWriteArrayList
, 锁工具: 闭锁 CountDownLatch
, 循环栅栏 CyclicBarrier
信号量 Semaphore
等,以及两个子包分别是atomic
和locks
2. volatile 关键字特性
2.1 内存可见性,不保证原子性
在同一进程的不同线程访问进程共享数据时,会将共享变量加载到当前cpu缓存中,因此不同线程对共享数据的修改不能及时在其他线程读取中体现,内存可见性就能够在写的时候强制刷新到主存中,这样就避免了其他线程的脏读
代码案例
代码本意:在线程run中 更改 状态变量之后打印,main线程中循环读状态变量,如果为真打印停止
bug原因:当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。因此,在run线程中状态变量已经true , 然而 main线程中仍然是 之前读到的false 所以也就陷入了死循环中
package jdk.learn.juc;
import lombok.Data;
import java.util.concurrent.TimeUnit;
public class VolatileTest {
public static void main(String[] args) {
Run run = new Run();
new Thread(run).start();
while (true) {
if (run.isStopped()) {
System.out.println("主-监听停止");
break;
}
}
}
@Data
static class Run implements Runnable {
// private volatile boolean stopped;
private boolean stopped;
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
stopped = true;
System.out.println("run is stopped");
}
}
}
那么有什么解决办法?让不同线程之间的变量赋值能够实时可见
- 使用volatile修饰状态变量
- 使用synchronized代码块包裹读状态变量代码,它也可以刷新缓存(它的正确使用应该在于同步)
这里是解决可见性,那么更优先选择是volatile
对于操作系统硬件层面内存知识可以看这篇JMM
2.2 防止指令重排
在高级语言中我们也追求性能最优化,那么在计算机的底层,cpu层面上也是追求性能的最优化,在计算机设计时也在速度较慢的内存与速度超快的cpu中间加高速缓存来粘合中间的速度不匹配,逻辑层面则使用了指令重排对输入的代码经行乱排序之后错位执行
的处理器优化策略,让cpu的运算单元能够充分利用
重排序需要遵守一定规则:
- 重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运
行时这两个操作不会被重排序
- 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变 量及其后面语句可见。
2.3 单例中双重校验使用原理
3. Atomic 原子类
在并发操作中有经典的 i++
问题,一条命令对于CPU而言,其实包含了三步操作,读i的值
i+1
写入i=i+1
示例代码
import java.util.concurrent.TimeUnit;
public class AtomicTest {
public static void main(String[] args) {
AtomicCount count = new AtomicCount();
for (int i = 0; i < 10; i++) {
new Thread(count).start();
}
}
}
class AtomicCount implements Runnable {
private int count = 0;
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count++);
}
}
**输出 **
3
8
6
0
7
2
0
5
4
1
可以在 AtomicCount.count 加上volatile修饰 验证 无法保证原子性
这里是为了引出Atomic原子类,将count 改为AtomicInteger
++
操作换成getAndIncrement
其底层使用的就是 sun.misc.Unsafe
的 CAS操作
3.1 CAS 算法
CAS 算法是 CompareAndSwap , 如 : 从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存 .
**并不包含不等一直循环操作 , 一直循环这是自定义的业务逻辑 , 如 sun.misc.Unsafe#getAndAddInt **
CAS 就是通过这样的过程避免了加锁的开销,并且实现了多线程安全问题,同时在学了上一节可以知道,还有一点,仅有CAS 还不能保证我们每次取值V 都是最新的值,因此需要使用 volatile保证内存可见性
同时如AtomicInteger 类中 getAndIncrement
incrementAndGet
等操作都是使用 sun.misc.Unsafe#getAndAddInt
因此 包含自旋操作:未获得预期值替换则会一直循环CAS 形成自旋
自旋锁在竞争激烈的情况下性能反而不高 , 因为大多数都在无效循环判断预期值
总结
- CAS 其实可以算乐观锁 , 与预期值一致才替换
- 长时间自旋非常消耗资源,不适合竞争激烈操作
- 不能保证代码块的原子性,只能保证单个变量的原子性
4. CurrentHashMap 和 CopyOnWriteArrayList
这里视频中讲解的是jdk1.8以前的实现,采用的是分段锁 , 而1.8之后使用 synchronized node 对象锁+cas 保证容器的并发安全问题
分段锁大致理念就是 把 hashMap中的桶元素 将一些桶元素归类于一段
并发操作时 给这一段加锁 ,从而保证并发安全
详细比较可以看这篇文章
5. 闭锁 CountDownLatch
在Java 5.0 引入的同步辅助类,可以在其他线程未执行完之前
让 一个或多个线程等待
,闭锁可以延迟线程的执行进度,确保某些活动在其他活动完成才执行:
- 保证某个计算任务在所有资源初始化后才能执行
- 确保某个服务在所有其他服务都启动之后才启动
- 等某个操作的所有参与者都就绪再继续执行
示例
程序 application 一定会在 数据源 和 属性文件加载完毕后运行
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
static CountDownLatch countDownLatch = new CountDownLatch(2);
static Boolean Datasource = false;
static Boolean Properties = false;
public static void main(String[] args) {
new Thread(() -> {
try {
countDownLatch.await();
System.out.println("application is running ...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
Datasource = true;
System.out.println("Datasource 就绪");
countDownLatch.countDown();
}).start();
new Thread(() -> {
Properties = true;
System.out.println("Properties 就绪");
countDownLatch.countDown();
}).start();
}
}
5.1 闭锁关键代码详解
闭锁实现 其实是依赖内部类 Sync , 闭锁构造器实际声明一个Sync 对象
private static final class Sync extends AbstractQueuedSynchronizer
5.2 countDown 方法
直接调用sync 的方法
开始尝试释放闭锁判断
实际是个自旋CAS 修改state 数量控制, 成功后 执行doReleaseShared
这一步也是个自旋 ,循环判断状态 , 才是真实释放闭锁
内部维护了一个 Head
双向链表 , 遍历链表判断状态量 CANCELLED
SIGNAL
5.3 await 方法
sync.acquireSharedInterruptibly(1);
6. 四种执行线程方式
- Runable
- Thread
- Callable
- ExecutorService
Callable 方法 在FutureTask 接收时会阻塞,因此,FutureTask 也可以实现等同闭锁操作
注意 : 在多个任务时,不要第一时间调用get方法,因为会形成阻塞 ,正确方式应该是 放到集合中当完成时调用get方法获得返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Call call = new Call();
FutureTask<Double> task = new FutureTask<>(call);
new Thread(task).start();
System.out.println("线程执行====");
Double res = task.get();
System.out.println(res);
}
}
class Call implements Callable<Double> {
@Override
public Double call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return Math.random();
}
}
7. 同步锁 Lock
解决代码并发安全问题之前都是采用 同步方法
和 同步代码块
, 在1.5之后可以采用 Lock
的方式更加灵活的完成加锁解锁,并且也有更高的性能
示例-错误代码
import java.util.concurrent.TimeUnit;
public class UnLockTest {
public static void main(String[] args) {
Unlock unlock = new Unlock();
new Thread(unlock, "一号窗口").start();
new Thread(unlock, "二号窗口").start();
new Thread(unlock, "三号窗口").start();
}
}
class Unlock implements Runnable {
private volatile int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 完成购票,余票" + --ticket);
}
}
}
由于没有加锁,所以会线程安全问题 , 如果加上Lock 方法
正确代码示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UnLockTest {
public static void main(String[] args) {
Unlock unlock = new Unlock();
new Thread(unlock, "一号窗口").start();
new Thread(unlock, "二号窗口").start();
new Thread(unlock, "三号窗口").start();
}
}
class Unlock implements Runnable {
private volatile int ticket = 100;
private static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (ticket <= 0) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + ": 完成购票,余票" + --ticket);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
7.1 生产消费,等待唤醒
使用lock 完成对应锁机制的等待唤醒,而其中经典的方式是生产消费者模式
7.1.1 生产者消费者-无等待唤醒
这种会出现 , 无库存不等待重复消费 , 库存满 不等待重复生产,应该在对应地方等待
import java.util.concurrent.TimeUnit;
public class CPTest {
public static void main(String[] args) {
Store store = new Store();
new Thread(new Producter(store), "生产者a").start();
new Thread(new Customer(store), "消费者b").start();
}
}
class Producter implements Runnable {
Store store;
public Producter(Store store) {
this.store = store;
}
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
store.setProduct();
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
class Store {
private int product = 0;
public synchronized void getProduct() {
if (product <= 0) {
System.out.println("无库存");
} else {
System.out.println(Thread.currentThread().getName() + ":消费" + --product);
}
}
public synchronized void setProduct() {
if (product >= 20) {
System.out.println("库存已满");
} else {
System.out.println(Thread.currentThread().getName() + ":生产" + ++product);
}
}
}
class Customer implements Runnable {
private Store store;
public Customer(Store store) {
this.store = store;
}
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
store.getProduct();
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
7.1.2 生产者消费者-等待唤醒
需要注意的点 :
- 不能放入else 分支
- wait 需要在循环中,防止虚假唤醒
import java.util.concurrent.TimeUnit;
public class CPTest {
public static void main(String[] args) {
Store store = new Store();
new Thread(new Producter(store), "生产者a").start();
new Thread(new Customer(store), "消费者b").start();
}
}
class Producter implements Runnable {
Store store;
public Producter(Store store) {
this.store = store;
}
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
store.setProduct();
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
class Store {
private int product = 0;
public synchronized void getProduct() {
while (product <= 0) {
System.out.println("无库存");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":消费" + --product);
this.notifyAll();
}
public synchronized void setProduct() {
// 需要放入循环中 不然会出现
while (product >= 20) {
System.out.println("库存已满");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":生产" + ++product);
this.notifyAll();
}
}
class Customer implements Runnable {
private Store store;
public Customer(Store store) {
this.store = store;
}
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
store.getProduct();
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
7.2 Lock 版生产消费者
Lock中可以使用Lock 代替 synchronized , 而使用 Condition 实现 唤醒等待
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CPLockTest {
public static void main(String[] args) {
Store store = new Store();
new Thread(new Producter(store), "生产者a").start();
new Thread(new Customer(store), "消费者b").start();
new Thread(new Producter(store), "生产者c").start();
new Thread(new Customer(store), "消费者d").start();
}
static class Producter implements Runnable {
Store store;
public Producter(Store store) {
this.store = store;
}
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
store.setProduct();
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
static class Store {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private int product = 0;
public void getProduct() {
lock.lock();
while (product <= 0) {
System.out.println("无库存");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":消费" + --product);
condition.signalAll();
lock.unlock();
}
public void setProduct() {
lock.lock();
// 需要放入循环中 不然会出现
while (product >= 20) {
System.out.println("库存已满");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":生产" + ++product);
condition.signalAll();
lock.unlock();
}
}
static class Customer implements Runnable {
private Store store;
public Customer(Store store) {
this.store = store;
}
@Override
public void run() {
for (int i = 0; i < 2000; i++) {
store.getProduct();
// try {
// TimeUnit.MILLISECONDS.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
}
}
7.3 线程按序交替
编写一个程序 开启3个线程 线程名 分别为A B C,在屏幕上面打印10遍 要求按序显示,如:ABCABCABC 依次显示
import java.io.Serializable;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SerializableThreadPrintTest {
static String A = "A";
static String B = "B";
static String C = "C";
public static void main(String[] args) {
SerializableThreadPrint exec = new SerializableThreadPrint();
Runnable run = () -> {
for (int i = 0; i < 10; i++) {
exec.loop();
}
};
new Thread(run, A).start();
new Thread(run, B).start();
new Thread(run, C).start();
}
static class SerializableThreadPrint implements Serializable {
private static String next = A;
private static Lock lock = new ReentrantLock();
private static Condition a = lock.newCondition();
private static Condition b = lock.newCondition();
private static Condition c = lock.newCondition();
private void loop() {
lock.lock();
try {
inprint();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private Condition getCurrent() {
if (A.equals(Thread.currentThread().getName())) {
return a;
}
if (B.equals(Thread.currentThread().getName())) {
return b;
}
if (C.equals(Thread.currentThread().getName())) {
return c;
}
return null;
}
private void inprint() throws InterruptedException {
if (!next.equals(Thread.currentThread().getName())) {
getCurrent().await();
}
System.out.print(Thread.currentThread().getName());
notifyNext();
}
private void notifyNext() {
if (A.equals(Thread.currentThread().getName())) {
next = B;
b.signal();
}
if (B.equals(Thread.currentThread().getName())) {
next = C;
c.signal();
}
if (C.equals(Thread.currentThread().getName())) {
next = A;
a.signal();
}
}
}
}
9. 读写锁
读写锁是为了区分 读写 操作 , 因为在实际场景中 读是不需要互斥,而写是需要互斥的,程序设计过程中,锁的粒度更小性能越好,这样的场景下,如果读写区分锁,那么锁的粒度是更小的
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWirteLockTest {
public static void main(String[] args) {
RW rw = new RW();
Runnable r = () -> {
for (int i = 0; i < 100; i++) {
rw.setNum((int) (Math.random() * 100));
rw.getNum();
}
};
new Thread(r, "A").start();
new Thread(r, "B").start();
new Thread(r, "C").start();
}
static class RW {
private int num = 0;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public int getNum() {
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName() + ":读" + num);
readWriteLock.readLock().unlock();
return num;
}
public void setNum(int num) {
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + ":写" + num);
this.num = num;
readWriteLock.writeLock().unlock();
}
}
}
10. 线程八锁
线程八锁代表线程锁的八个案例,针对不同方法,不同实例 产生的锁的情况
- 1.非静态方法的锁 this, 静态方法的锁 对应的Class实例
- 2.某一个时刻内,只能由一个线程持有锁,无论几个方法
- 1.两个同步方法,两个线程,打印 one two
- 2.新增Thread.sleep()给getOne 打印 one two
- 3.新增普通方法getThree,打印 three one two
- 4.注释getThree,number2.getTwo,打印 two one
- 5.修改getOne为静态同步方法,改为number.getTwo,打印 two one
- 6.两个方法都为静态同步方法,一个number对象,打印 one two
- 7.getOne为静态同步方法,getTwo为同步方法,改为number2.getTwo,打印two one
- 8.两个静态同步方法,两个number对象,打印 one two
其中案例就是讲诉两个知识点:
- 静态方法 synchronized 与 实例方法 synchronized 锁的对象区别
- 时间片获取与sleep 休眠 是否释放锁
静态方法 使用的是.class 如 synchronized (LockMethod.class)
实例方法 使用的是 实例对象的this synchronized (this)
这里的对象就是当前调用这个方法的实例
因此,如果是实例方法,多个实例调用同一个同步实例方法,因为锁对象不同 不会产生竞争,也就不用等待
如果是静态方法,锁对象为.class 所以就会产生锁等待
同时sleep 不会释放锁 因此会有先后
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class LockMethodTest {
public static void main(String[] args) {
LockMethod lockMethod = new LockMethod();
LockMethod lockMethod2 = new LockMethod();
// 1.先获得锁的对象 先执行 其他等待
// new Thread(() -> {
// lockMethod.static_sync();
// }).start();
// new Thread(() -> {
// lockMethod.static_sync2();
// }).start();
// 2. 静态锁住了 , 实例方法能不能拿到锁
// CountDownLatch countDownLatch = new CountDownLatch(2);
// new Thread(() -> {
// lockMethod.simple_sync_method();
// }).start();
new Thread(() -> {
lockMethod.simple_sync_sleep3_method();
}).start();
new Thread(() -> {
// countDownLatch.countDown();
// try {
// countDownLatch.await();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
lockMethod2.simple_sync_method();
}).start();
}
static class LockMethod {
private static synchronized void static_sync() {
System.out.println("static_sync");
}
private static synchronized void static_sync2() {
System.out.println("static_sync2");
}
private static synchronized void static_sleep3_sync() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("static_sleep3_sync");
}
private static void static_method() {
System.out.println("static_method");
}
private void simple_method() {
System.out.println("simple_method");
}
private synchronized void simple_sync_method() {
System.out.println("simple_sync_method");
}
private synchronized void simple_sync_method2() {
System.out.println("simple_sync_method2");
}
private synchronized void simple_sync_sleep3_method() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("simple_sync_sleep3_method");
}
}
}
11. 线程池
线程池,是为了减少线程创建销毁的额外开销,与其他池化技术相同都是提前创建好对应线程,等待需要执行的任务提交直接执行,执行完毕后放入池中等待下次执行.
11.1 线程池 相关对象
同时还有对应的工具类 java.util.concurrent.Executors
可以创建常规场景中的线程池,但是不推荐在生产环境中使用,如Executors.newCachedThreadPool()
会创建无限大小的线程池,生产环境中很容易造成OOM
11.2 使用示例
import java.util.concurrent.*;
public class ExecutorServiceTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = new ThreadPoolExecutor(4, 4, 60, TimeUnit.MINUTES
, new ArrayBlockingQueue<>(100), Executors.defaultThreadFactory()
, new ThreadPoolExecutor.AbortPolicy());
Future<?> submit = executorService.submit(() -> {
System.out.println("1111111111");
});
executorService.execute(() -> {
System.out.println("1111111111");
});
executorService.awaitTermination(10, TimeUnit.DAYS);
executorService.shutdown();
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.scheduleAtFixedRate(()->{
System.out.println("执行一次");
}, 2, 2, TimeUnit.SECONDS);
}
}
详细参数可以看这篇总结 https://blog.csdn.net/qq_35530042/article/details/109001536
11.3 Fork/Join 框架
- ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比ExecutorService 更好。
- ForkJoinPool 主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如 quick sort 等。
- ForkJoinPool 最适合的是计算密集型的任务,并且任务量大的情况,任务量小反而比普通线程池慢 , 如果存在 I/O,线程间同步,sleep() 等会造成线程长时间阻塞的情况时,最好配合使用 ManagedBlocker。
- 同时它也是在jdk8中并行流使用的池
最后
之前很多知识都是零散的学习过,或者学过又忘记了,过一遍视频总结一些知识点,并且在基础上找网上资源和之前的总结做了一些补充,感觉对这些知识点更清晰了,共勉!