本文是学习了黑马程序员唐僧老师的课,觉得说的很好~ 根据课程内容形成的一些重点总结内容~
进程:操作系统分配资源的计算单位,一个应用程序启动之后可执行文件加载到内存,要为它分配资源然后运行就得到一个进程
线程:一个进程内有很多的分支(或者说是很多个不同的任务),进程内部拆分出很多个线程,cpu调度时基于不同线程任务来调度,为防止堵塞提高cpu的利用率
线程总结:线程可以认为是轻量级的进程,所以线程的创建、销毁要比进程更快;线程是cpu的最小调度单元/最小执行单元(核心),同一进程下的所有线程共享该进程的资源,在多核cpu架构中能够实现真正的并行执 行,在单核cpu架构中也能做到多个线程分时复用cpu
并行是在某一个时刻同时执行
并发是在同一段时间内交替执行
被称为绿色的线程,它属于用户管理而不是操作系统(OS)管理的,操作系统并不知道协程,它只知道线程,协程是在线程之上的。
协程并没有 增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
java官方没有,go、php有
1、单核CPU设定多线程是否有意义?
有意义,分时复用
2、工作线程数是不是设置的越大越好? 工作线程数(线程池中的线程数)设置多少合适?
cpu密集型,几个cpu就设置成几个,6个cpu就设置为6;有时候生产实践会设置n+1,为了避免某个出问题
io密集型,一般是cpu的两倍
应该根据实践,压测,结合机器的配置不断压测得出具体的数据,一般这个线程数跟 CPU的核数有关系
3、实现runnable还是thread好?
runnable,本质还是new一个thread然后start。thread本身就是实现runnable中的run方法
1、public void interrupt() {…};public boolean isInterrupted() {…};public static boolean interrupted() {…}
2、interrupt遇到wait,join,sleep,产生InterruptedException也是一种线程优雅退出的方案
3、interrupt遇到synchronized/lock.lock() 获取锁是无法打断的;lock.lockInterruptibly()获取锁竞争过程是可以打断的
已废弃:public final void stop();suspend暂停、resume恢复
volatile 共享变量(标志位)
interrupt() + isInterrupted() / Thread.interrupted() / InterruptedException
可见性:Visibility 有序性:Ordering 原子性:Atomicity
可见性问题:多核cpu,每个核都有自己的缓存
原子性问题:线程切换带来的,CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符
顺序性问题:指令重排序带来的,可能会带来未初始化的对象,访问该对象的成员变量可能就会造成空指针异常
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存 中的变量。不同的线程间也无法直接访问对方工作内存中的变量,线程间变量 值的传递均需要通过主内存来完成。
可以解决可见性和顺序性问题
工作内存数据一致性:可见性问题
约束指令重排序优化:有序性问题
前面一个操作的结果对后续操作是可见的。有个特性:传递性
1、程序的顺序性规则
2、volatile变量规则:指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作(先写后读)
3、管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
4、线程启动规则:指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程B 前的操作。
5、线程join规则:主线程 A 等待子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。
6、线程中断规则:线程interrupt()方法的调用 Happens-Before 被中断线程的代码检测 到中断事件的发生
7、对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法(回收)的开始。
volatile是JVM提供的最轻量级的同步机制;volatile修饰变量是为了保证变量在多线程中的可见性,他告诉编译器这个变量的读写必须从内存中读取或写入,是不能使用cpu缓存的
当一条线程修改了volatile变量的值,其他线程是可以立即得到新值的
线程写volatile变量:改变线程工作内存中volatile变量副本的值;然后将改变后副本的值立即从工作内存刷新到主内存
线程读volatile变量:从主内存读取volatile变量的新值到线程工作内存,然后从工作内存读取volatile变量副本
ps:volatile能保证可见性但无法保证原子性,线程安全无法保障;volatile修饰引用类型,它只能保证引用本身的可见性,不能保证所 引用对象内部属性的可见性;禁止指令重排
synchronized加锁不仅仅局限于互斥行为也包括内存可见性。
执行解锁时会将工作内存中的共享变量刷到主内存(相当于JMM的unlock)
执行加锁时会清空工作内存中共享变量副本的值,需要使用时从主内存重新加载(相当于JMM的lock)
ps:使用锁来保证可见性较为笨重,因为synchronized是线程独占的其他线程会被阻塞,还存在一些线程调度开销因为他靠操作系统内核互斥锁实现的。而volatile相对轻量级的,但synchronized可以保证原子性
final修饰变量代表是不可变的,这就不存在不同线程间工作内存数据不一致的问题且编译器可进行优化
一个对象的final字段值是在他的构造方法里面设置的,当对象正确的构造后,在构造方法里给final字段的值在没有同步情况下对所有其他的线程都是可见的,引用这些final字段的对象或数组都会看到dinal字段的最新值
禁止指令重排
写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后
读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前
虽然不能禁止指令重排,但它可以保证有序性
线程与线程间,每一个synchronized可以看作是一个原子操作,块与块之间看起来是原子操作且有序可见(线程第一个没结束第二个就进不来)
volatile底层如何实现禁止指令重排?
java源码层面:使用语言层面volatile关键字static volatile C02_KnowOrdering instance;
字节码层面:添加了访问标记(acces flag): [static volatile]
jvm层面:jvm拿到了带有volatile标记的字节码,他的处理是采用内存屏障,保证屏障两边的指令不可重排,保证有序。
JSR内存屏障规范:
LoadLoad;StoreStore;LoadStore;StoreLoad;都是要保证才该语句后的操作执行前,该语句前的数据被读取完毕或写入操作对其他处理器可见。
系统底层支持内存屏障的系统原语:
Ifence,是一种Load Barrier读屏障,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
sfence,是一种Store Barrier写屏障,在写指令之后插入写屏障,让写入缓存的最新数据写回主内存
mfence,全能型屏障,具备lfence和sfence的能力
hotspot实现:
使用lock前缀来完成,Lock会对cpu总线和高速缓存加锁,可以理解为cpu指令级的一种锁。
先对总线/缓存加锁,然后执行后续指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存
在Lock锁住总线时,其他cpu的读写请求会被阻塞直到锁释放。Lock后的写操作会让其他CPU相关的cache line失效,从而从新从 内存加载最新的数据。这个是通过缓存一致性协议做的。
原子性的根源是CPU在执行完任意指令后都有可能发生线程切换。解决:禁用线程切换(单核cpu可行)
多核:线程互斥
真正保证并发原子性的是:同一时刻只有一个线程执行,这个条 件非常重要,我们称之为互斥。如果我们能够保证对共享变量的修改是互斥 的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个 线程访问共享资源;另一个是同步,即线程之间如何通信、协作。
锁模型:搞清锁对象是谁–找到临界区:哪些地方是要对共享资源进行操作,锁对象能不能锁住共享资源的操作(锁住的是一个对象然后走到临界区(需要互斥执行的代码),最后需要释放锁)
三种方法:修饰非静态方法;修饰静态方法;修饰代码块
加锁和解锁操作体现在哪?synchronized 的加锁和解锁是隐式实现的,可以查看字节码
synchronized的锁对象?
如果修饰的是代码块,锁对象是我们自己指定的,指定哪个对象就锁定 哪个对象。
如果修饰的是非静态方法,锁定的是当前实例对象 this。
如果修饰的是静态方法,锁定的是当前类的 Class 对象。
i+=1或i++都不是原子操作的,会有线程安全问题
对一个对象的解锁操作 hb 对一个对象的加锁操作
解决方案:1、将变量i设置成volatile;2、方法get也加锁
锁和资源的关系一定是1:n的,一个锁可以锁住多个资源,但一个资源只能被一把锁锁住
一把锁可以锁住多个没有关联的资源其实可以降低锁的粒度,资源之间有关联的话锁力度也不能太细,影响性能
一把锁如何保护多个资源?
锁的粒度太大:安全性肯定没问题,但性能有影响
调优情况:在保证安全的大前提下降低锁的粒度
保护多个没有关联关系的资源:最好的做法是用不同的锁对受保护资源进行精细化管理,能够提升性能。
定义:一组互相竞争资源的线程因互相等待,导 致永久阻塞的现象。(细颗粒度锁可能会导致死锁)
完整的等待 - 通知机制是这样的:线程首先获取互斥锁,当线程要求的 条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待 的线程,重新获取互斥锁。
解决活锁
加个随机时间,两边都尝试等待一个随机时间
饥饿
因为优先级等原因一直得不到执行,使用notifyAll
CAS:对于变量i,读取当前值为v,然后进行业务操作得到值n,此时再次读取i的值为e,比较v和e的值是否相等,相等则可以修改i的值为n,否则再次进行一遍该流程
ABA问题:在e与v比较之前其它线程修改过但是其修改后的值与原先的v值相等(前任复合问题)
解决ABA问题:增加version版本号,修改一次+1,同时比较version
互斥量–自旋锁–信号量
源码层面:关键字
字节码层面:monitorenter主要是获取监视器锁,monitorexit主要是释放监视器锁
jvm层面:锁的对象,对象发生了变化主要是在markword中记录一些锁的信息
markword 8个字节 – class pointer 4个字节 – 实例数据 4个字节 – padding填充 8个字节 (总共需要是8个字节的整数倍)
markword8个字节64bit,可以优先从后面三位中看出是否有上锁及上的什么锁
锁升级(不会一上来就到mutex状态需要逐步升级)
无锁:主要看后三位001
(临界区70-80%的时间不是并发执行的,第一个线程执行比较长的时间)偏向锁:偏向锁标识位一定为1,后三位101,偏向的是第一个获取锁的线程,它在markword记录当前线程的指针javaThread54位(当前线程的内核id)
当线程多了开始有竞争了(轻级)就上轻量级锁,需要撤销偏向锁重置标识位,拿掉前面的线程指针。一个对象来了两个线程t1和t2,用cas的方式去写,向对象头markword中写线程栈里生成的锁的lockword(锁记录),相当于就是markword指向线程栈lockRecord的指针。哪个线程以cas的方式去操作成功哪个就能获取到这个锁。后两位00
一个线程拿到之后其他线程在自旋,cas获取不到会重新发起,会当竞争激烈的时候对cpu有损耗。
竞争越来越加剧的时候获取重量级锁,上到(底层的mutex)互斥量,在markword中记录锁的信息(指向互斥量的指针),后两位10,锁竞争情况严重,某个达到最大自旋次数的线程(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)
默认偏向锁启动有个JVM延迟(4s),因为JVM启动过程中也会开启些线程,这其中的线程也会用到锁的机制(这是JVM自己创建自己使用的,还未完全启动不会有用户线程)
4s前就已经new了一个对象obj的话会直接上轻量级锁,4s后偏向锁启动就是先上偏向锁
如果有hashcode的话会直接上轻量级锁,因为一开始无锁状态占用31位hashcode导致javaThread不够(有重叠部分)
并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线 程访问共享资源;另一个是同步,即线程之间如何通信、协作。
语言层面:synchronized,wait ,notify/notifyAll
SDK层面:Lock(“互斥”对应synchronized)、Condition(“同步”对应wait ,notify/notifyAll)
因为synchronized有弊端,1、获取不到锁时会等待,拿到一个锁等待第二个锁的时候不会释放第一个锁;2、获取锁获取不到的时候,在等待队列中的时候是不能被唤醒,不能被中断的除非拿到锁,且他是非异步的
所以Lock具备以下特征:能够被中断;支持超时获取;支持非阻塞的获取锁
1、互斥信号量:任务之间通过互斥信号量访问临界资源,这其实就是锁机制(解决互斥访问共享资源)
2、计数信号量:任务之间竞争性的访问共享资源 (多个任务之间可以并发的访问共享资源,控制多个线程访问一个临界区)
3、二值信号量:任务之间的同步机制(解决任务之间的同步)
计数器(s.count)记录目前有多少个任务(线程)来访问
等待队列(s.queue)资源不够用时调用线程进入阻塞队列等待
三个操作方法:初始化count,设置为非负整数;设置为1,任何时刻只能有一个线程来访问他,互斥机制;设置为2时就是同时允许2个线程并发访问,其他线程无法进来
p操作:wait,申请对应的资源
--s.count ;//表示申请一个资源
if (s.count < 0 ) {//表示没有空闲资源
//调用进程/线程进入阻塞队列 s.queue
//阻塞等待 }
v操作:signal,对资源的释放
++s.count;//表示释放一个资源
if (s.count <=0) { //表示有进程/线程处于阻塞等待状态
//从队列s.queue中取出一个进程/线程
//进程/线程进入就绪执行 }
互斥量:计数器初始化为1
计数信号量:计数器初始化大于1
二值信号量:计数器初始值为0
Semaphore这个类对应的几个操作分别是:acquire(),release(), 对于信号量的使用一般有以下几种
1、互斥信号量,充当互斥锁
2、计数信号量,限流器
读写锁三条基本原则:允许多个线程读,但读的时候禁止写;允许并发读;写的时候只允许一个线程写且禁止其他线程读
两种模式:读锁、写锁
三种模式:写锁、悲观读锁、乐观读锁
写锁、悲观读与前面读写锁一致,读的时候不允许写这样导致性能并不高
乐观锁就是加了版本号,写一次版本号v+1(jdk实现具体用的时间戳)
一等多:一个线程等多个线程
多等一:多个线程等一个,计数器值为1
弊端:latch是一次性的,使用后就没了
一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到 最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续工 作,CyclicBarrier是一种同步机制允许一组线程相互等待,等到所有线程都到达 一个屏障点才退出await方法。
1、 CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可
以使用reset() 2、因为可以reset()。所以CyclicBarrier能处理更为复杂的业务场景,比如
如果计算发生错误,可以重置计数器,并让线程们重新执行一次
3、CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可 以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否 被中断
4、某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待
总之:CountDownLatch是计数器,线程完成一个记录一个,只不过计数 不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀 门才能打开,然后继续执行。
抽象队列同步器,一套多线程访问共享资源的同步器框架,提供sdk层面的锁机制
1、AQS用一个 属性表示锁状态,1表示锁被持有, 0表示未被持有,具体的维护由子类去维护,但是提供了修改该属性的三个方 法:getState(),setState(int newState) ,compareAndSetState(int expect,int update),其中CAS方法是核心。
2、框架内部维护了一个FIFO的等待队列(双联表,先进先出),我们称之为CLH队列
3、也实现了条件变量 Condition,用它来实现等待唤醒机制,并且支持多个条件变量
4、AQS支持两种资源共享的模式:独占模式(Exclusive)和共享模式 (Share),所谓独占模式就是任意时刻只允许一个线程访问共享资源,譬如 ReentrantLock;而共享模式指的就是允许多个线程同时访问共享资源,譬如Semaphore/CountDownLatch
5、使用者只需继承并重写指定的方法, 在方法内完成对共享资源的获取和释放,至于具体线程等待队列的维护,AQS已经在顶层实现好了,在那些的模板方法里。
{@link #tryAcquire}
{@link #tryRelease}
{@link #tryAcquireShared}
{@link #tryReleaseShared}
{@link #isHeldExclusively}
6、AQS底层使用了模板方法模式,给我们提供了许多模板方法,我们直接使用即可。
假设现在有三个线程一起来,线程t1获取到锁将state置为1,
线程t2就要入队列:(队列中的头节点就是当前获取到锁的线程节点)t1直接tryAcquire拿到锁了没有机会进行到addWaiter创建节点,所以t2先初始化Node空节点head=null(thread=null),head会指向这个空节点这代表的是拿到锁的线程节点,prev代表上一个节点,next代表下一个节点。然后创建t2的Node(thread=t2);同时他要判断他的前驱是否为head,如果是则会去tryAcquire如果失败则park。
t3入队列就和前面操作一样,不过他判断前驱不是head就会直接park。
t1释放锁将state置为0,唤醒head.next节点。然后该节点判断prev==head&&tryAcquire(前一个节点是否为head和尝试获取锁)获取锁成功就往下执行,获取失败就继续park(可能会有别的线程插队,看源码:因为线程进来先tryAcquire再去看队列)
t2成功获取锁就更新state置为1然后setHead为当前节点
在这里插入图片描述
线程一来首先调用 tryAcquire ,在 tryAcquire 中直接CAS获取锁, 如果获取不成功通过 addWaiter 加入等待队列,然后走 acquireQueued 让队列中的某个等待线程去获取锁。不公平体现在没有先查等待队列是否有线程在等待就直接去获取锁。
解决办法:提供了一个 hasQueuedPredecessors 函数:如果在当前线程之前有 一个排队的线程,则为True; 如果当前线程位于队列的头部(head.next)或队列 为空,则为false。
锁:不同线程是互斥的,对同一个线程前后也是互斥的
重入次数:lock/unlock是一对的,即有几次lock就对应有几次unlock
不仅要记录锁已被获取,还要记录锁被获取了多少次(status+1记录)
获取锁时,如果发现锁已被获取,不能立马判断获取失败;此时要获取锁的线程是否是自己,如果是自己还可以继续获取锁(AbstractOwnableSynchronizer)
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 基于 aqs实现锁
*/
public class MyLock2 implements Lock {
//同步器
private Sync syn ;
MyLock2 () {
syn = new NoFairSync();
}
MyLock2 (boolean fair) {
syn = fair ? new FairSync():new NoFairSync();
}
@Override
public void lock() {
//调用模板方法
syn.acquire(1);
}
@Override
public void unlock() {
//调用模板方法
syn.release(1);
}
// Lock接口其他方法暂时先不实现
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
return false;
}
@NotNull
@Override
public Condition newCondition() {
return null;
}
// 实现一个独占同步器
class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryRelease(int arg) {
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean realRelease = false;
int nextState = getState() - arg;
if (nextState == 0) {
realRelease = true;
setExclusiveOwnerThread(null);
}
setState(nextState);
return realRelease;
}
}
class FairSync extends Sync {
@Override
protected boolean tryAcquire(int arg) {
final Thread currentThread = Thread.currentThread();
int currentState = getState();
if (currentState == 0 ) { // 可以获取锁
//先判断等待队列中是否有线程在排队 没有线程排队则直接去获取锁
if (!hasQueuedPredecessors() && compareAndSetState(0,arg)) {
setExclusiveOwnerThread(currentThread);
return true;
}
}else if (currentThread == getExclusiveOwnerThread()) {
//重入逻辑 增加 state值
int nextState = currentState + arg;
if (nextState < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextState);
return true;
}
return false;
}
}
class NoFairSync extends Sync {
@Override
protected boolean tryAcquire(int arg) {
final Thread currentThread = Thread.currentThread();
int currentState = getState();
if (currentState ==0 ) { // 可以获取锁
//直接去获取锁
if (compareAndSetState(0,arg)) {
setExclusiveOwnerThread(currentThread);
return true;
}
}else if (currentThread == getExclusiveOwnerThread()) {
//重入逻辑 增加 state值
int nextState = currentState + arg;
if (nextState < 0) {
throw new Error("Maximum lock count exceeded");
}
setState(nextState);
return true;
}
return false;
}
}
}
容器四大类:List、Map、Set 和 Queue
不是所有java容器都是线程安全的,ArrayList、HashMap 就不是线程安全的
1、面向对象的思想,把非线程安全的容器封装在对象内部,对象提供线程安全的访问方法
Collections 这个类中提供了一套完备的包装类, 分别把 ArrayList、HashSet和 HashMap包装成了线程安全的 List、Set和 Map
1.7锁分段
1.8cas、synchronized、volatile
java中的线程池是一种:生产者 - 消费者模式
线程池使用方是生产者,线程池是消费者,线程池里是任务的队列
核心线程数:coreSize(假设5个)
最大线程数:maxSize(假设10个)
线程空闲等待时间(假设1s)
任务的拒绝策略
线程工厂:创建线程
当线程池中的线程数小于核心线程数时,线程来了会去创建线程(5),达到核心线程数之后会进入任务的阻塞队列(workQueue),执行完的线程也会从队列中去获取;
当队列满了之后,会又去创建线程直到达到最大线程数(10);
当到达最大线程数之后队列仍满了就会采用拒绝策略:不接收;抛异常
当有线程空闲等待达到设置时间(1s)后,就会退出,退出至核心线程数量(5)就不会再退出(要设置 allowCoreThreadTimeout=true允许核心线程超时);如果设置为flase(默认)那么核心线程超时了也会退出,再有线程来的时候会重新再创建
1、线程池是如何保证线程不被销毁的呢?
如果队列中没有任务时,核心线程会一直阻塞在获取任务的方法,直到返回任务。而任务执行完后,又会进入下一轮 work.runWork()中循环
2、那么线程池中的线程会处于什么状态?
RUNNABLE,WAITING,因为要么就在执行任务,要么就在阻塞等待获取任务
3、核心线程与非核心线程有区别吗?
没有。被销毁的线程和创建的先后无关。即便是第一个被创建的核心线程,仍然有可能被销毁
验证:看源码,每个work在runWork()的时候去getTask(),在getTask内 部,并没有针对性的区分当前work是否是核心线程或者类似的标记。只要判断 works数量超出core,就会调用poll(),否则take()
1、Executors.newSingleThreadExecutor()
corePoolSize和maximumPoolSize都为1;workQueue是无界阻塞队列,队列的大小不限制。
2、Executors.newFixedThreadPool(int nThreads)
创建了一个固定大小的线程池,可以指定同时运行的线程数量为 nThreads;队列的大小不限制。
3、Executors.newCachedThreadPool()
构造一个缓冲功能的线程池,配置corePoolSize=0,无容量的阻塞队列 SynchronousQueue;因此任务提交之后,将会创建新的线程执 行;线程空闲超过60s将会销毁。
4、Executors.newScheduledThreadPool(int corePoolSize)
有定时功能的线程池,配置corePoolSize,无界延迟阻塞队列 DelayedWorkQueue;maximumPoolSize=Integer.MAX_VALUE,由于 DelayedWorkQueue是无界队列,所以这个值是没有意义的
阿里规范:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor 的方式
1: I/O密集型: CPU使用率较低,程序中会存在大量I/O操作占据时间,导致线程空余时间
出来,线程个数为CPU核数的两倍。当其中的线程在IO操作的时候,其他线程 可以继续用CPU,提高了CPU的利用率
2:CPU密集型:
CPU使用率较高(也就是一些复杂运算,逻辑处理),所以线程数一般只 需要CPU核数的线程就可以了(实践中会选择core+1)。 这一类型的在开发中 多出现的一些业务复杂计算和逻辑处理过程中。线程个数为CPU核数。这几个 线程可以并行执行,不存在线程切换到开销,提高了CPU的利用率的同时也减 少了切换线程导致的性能损耗