一、进程与线程的概念
进程:进程是程序的基本执行实体。【线程是进程里面的】
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。(简单理解:应用软件中互相独立,可以同时运行的功能)
程序的执行是需要时间的,多线程就是让cpu像牛马一样不间断的工作,在等待的时间也要去做其他事情,充分利用cpu来达到提高效率的目的,一句话:不许歇着,起来干活!

二、并发与并行的概念
并发:在同一时刻,有多个指令在单个cpu上交替执行
并行:在同一时刻,有多个指令在多个cpu上同时执行

三、多线程的三种实现方式
方式1.继承 Thread 类并重写run方法
myThread 类
package com.进程和多线程;
//多线程的第一种实现方式:继承 Thread 类并重写run方法
public class myThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+ " hello world");
}
}
}
测试类
package com.进程和多线程;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式1
method1();
}
// 方式一:多线程的第一种实现方式:继承 Thread 类并重写run方法
private static void method1() {
System.out.println("--------------------------------- 方式一:多线程的第一种实现方式:继承 Thread 类并重写run方法 ---------------------------------");
myThread t1 = new myThread();
myThread t2 = new myThread();
// 给线程取名字
t1.setName("线程1");
t2.setName("线程2");
// 开启线程 - 可以看到控制台是交替执行的两个线程 - “并发”
t1.start();
t2.start();
}
}
方式2.实现 Runnable 接口重写run方法
myThread2 类
package com.进程和多线程;
// 多线程的第二种实现方式:实现 Runnable 接口重写run方法
public class myThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// Thread.currentThread() 获取当前执行的线程
System.out.println(Thread.currentThread().getName() + " hello world");
}
}
}
测试类
package com.进程和多线程;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式2
method2();
}
// 方式二:多线程的第二种实现方式:实现 Runnable 接口重写run方法
private static void method2(){
System.out.println("--------------------------------- 方式二:多线程的第二种实现方式:实现 Runnable 接口重写run方法 ---------------------------------");
// 定义一个任务
myThread2 m = new myThread2();
// 将任务传递给线程,表示执行这个任务一次
Thread t1 = new Thread(m);
// 将任务传递给线程,表示执行这个任务两次
Thread t2 = new Thread(m);
// 给线程取名字
t1.setName("线程1");
t2.setName("线程2");
// 开启线程 - 可以看到控制台是交替执行的两个线程 - “并发”
t1.start();
t2.start();
}
}
方式3.利用Callable接口和Future接口实现 - 注意:这种方式可以获取多线程的 “返回值”
myThread3 类,注意你想要的线程的返回值也就是 Callable 的泛型
package com.进程和多线程;
import java.util.concurrent.Callable;
/**
* 想要获取多线程的返回值,那么使用第三种方式 利用Callable接口和Future接口实现
* 多线程的第三种实现方式:
* 1.定义一个类实现 Callable 接口重写call方法(有返回值的,表示多线程的运行结果)
* 2.创建 Callable 的对象(表示多线程要执行的任务)
* 3.创建 FutureTask 对象来管理多线程的运行结果
* 4.创建线程对象
* 5.在 FutureTask 里面获取多线程的运行结果
*/
public class myThread3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求1-100的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
测试类
package com.进程和多线程;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式3
method3();
}
// 方式三:利用Callable接口和Future接口实现 - 这种方式可以获取多线程的返回值
private static void method3() throws ExecutionException, InterruptedException {
System.out.println("--------------------------------- 方式三:多线程的第三种实现方式 - 利用Callable接口和Future接口实现 ---------------------------------");
// 1.定义一个类实现 Callable 接口重写call方法(有返回值的,表示多线程的运行结果)
// 2.创建 Callable 的对象(表示多线程要执行的任务)
myThread3 m3 = new myThread3();
// 3.创建 FutureTask 对象来管理多线程的运行结果
FutureTask<Integer> ft = new FutureTask<>(m3);
// 4.创建线程对象
Thread t1 = new Thread(ft);
t1.start();
// 5.在 FutureTask 里面获取多线程的运行结果
Integer result = ft.get();
System.out.println(result);
}
}
四、多线程中常用的成员方法

下面列出的是几个难以理解的方法
(1) 当前的线程对象 Thread.currentThread()
Thread.currentThread() 代表执行到这行代码时当前的线程对象,如果是main方法里面直接调用会发现有个main线程。
当JVM虚拟机启动之后,会自动启动多条线程,其中一条线程就是main线程,他的作用就是去调用main方法并执行里面的代码。
image.png
(2) 线程的优先级相关方法 setPriority() 和 getPriority()
Java 在主流系统 如Windows/Linux上,线程调度是抢占式的。特点:
线程优先级(Priority):Java 线程有优先级(1~10,默认5),但不保证严格按优先级执行。高优先级线程更可能被调度器选中,也就是带权重的随机。
时间片(Time Slicing):大多数现代操作系统(如Windows、Linux)使用时间片轮转的抢占式调度。每个线程执行一段时间后会被强制暂停,让其他线程运行。
不可控性:开发者无法精确控制线程切换的时机,由JVM和操作系统共同决定。
private static void memberMethods() {
// 采用的上面第二种线程实现方式
myThread2 m = new myThread2();
Thread t1 = new Thread(m, "飞机");
Thread t2 = new Thread(m, "坦克");
System.out.println("默认线程优先级:" + t1.getPriority() + "--" + t2.getPriority());
// 线程优先级是 1~10 之间
t1.setPriority(1);
t2.setPriority(8);
t1.start();
t2.start();
}
(3) 守护线程 setDaemon() (备胎线程)
当其他非守护线程执行完毕,守护线程就会陆续结束,即使它的代码未完全执行完毕。
注意这个结束不是立即结束,是陆续结束。就比如有个聊天窗口,里面的文件传输设置为守护线程,当聊天窗口关闭,那么文件传输也就不用传了,可以停了。

(4) 礼让线程 Thread.yield() --- 了解,平时很少用
礼让线程就是把cpu的执行权让出去,然后重新开始和其他线程争夺cpu执行权,让几个线程尽可能的均匀执行。
比如这个例子,如果没有下面的礼让线程代码,那么执行结果可能是某个线程执行了很久才轮到其他线程,而有了礼让线程那么打印结果就会比较均匀。

(5) 插入线程 join() --- 了解,平时很少用
表示的是把某个线程插入到正在执行的线程“前”执行,也就是插个队,先执行这个线程再执行当前线程。
比如下面的代码,如果没有t.join()这行代码,那么t线程和main线程会抢夺执行权,交替执行!如果我想让t线程执行完毕后再去执行main线程后面的代码,那么给t线程插个队就行了。

五、线程的生命周期(六大状态)


问:sleep 方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
答:不会,需要重新抢到cpu的执行权才会执行
六、线程的安全问题
因为线程执行的时候有“随机性”,线程在执行代码的时候随时可能被其他线程抢走执行权,那么在“操作共享数据”的时候就有问题。
比如卖票的例子,总共100张票,有3个线程卖,那么可能出现卖的票重复或者超出范围的情况,因为线程的执行有随机性!
为了解决这个问题,那么出现了一个概念:同步代码块。
(1)同步代码块
就像抢厕所一样,如果有人在里面那其他人就不能进去了,只能等里面的人处理完了出来了才能再抢这个厕所。
image.png
代码如下:
售票类 - SellTicket
package com.进程和多线程2_安全问题之卖票例子;
public class SellTicket extends Thread {
// 表示这个类的所有对象都共享 ticket 数据
static int ticket = 0;
// synchronized 需要指定一个唯一对象,用 static 随便定义一个就行了,但是一般会使用这个类的字节码对象
// static Object obj = new Object();
@Override
public void run() {
while (true) {
// 将共享代码用 synchronized 包裹,代表锁起来,当有线程在里面时其他线程不能执行里面的代码
synchronized (SellTicket.class) {
if (ticket >= 100) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!");
}
}
}
}
测试类
package com.进程和多线程2_安全问题之卖票例子;
public class demo1 {
public static void main(String[] args) {
SellTicket t1 = new SellTicket();
SellTicket t2 = new SellTicket();
SellTicket t3 = new SellTicket();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
特别注意这里 synchronized 里面的对象一定要是唯一的,这样才能锁起来。
(2)同步方法
image.png
将上面的 售票类 - SellTicket 改写成同步方法的方式,测试类不变
package com.进程和多线程2_安全问题之卖票例子;
public class SellTicket extends Thread {
// 表示这个类的所有对象都共享 ticket 数据
static int ticket = 0;
// 方式二:同步方法的方式
@Override
public void run() {
while (true) {
// 执行同步方法
if(extracted()) break;
}
}
//synchronized 包裹的是同步方法
static synchronized boolean extracted() {
if (ticket >= 100) {
return true;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票!!");
return false;
}
}
特别注意:这里是用的static synchronized修饰的,因为这个卖票类是直接继承的 Thread 类,我们在使用的时候根据这个类创建了三个不同的对象,这三个对象之间需要共享锁,因此需要用 static 修饰。
如果是实现 Runnable 接口这种方式来创建线程的,那就可以 不用static 修饰,只需要创建一次对象,如对象a,然后再创建三个Thread对象来共享这个对象a就行了。这里 synchronized 锁的对象就是this,也就是对象a。
(3)同步方法的扩展知识:StringBuilder 和 StringBuffer 的区别
StringBuilder 和 StringBuffer 在使用上基本是一模一样的,但 StringBuilder 对于多线程是不安全的,这个时候需要改为使用 StringBuffer。如果是单线程的就直接使用 StringBuilder 就行了

七、Lock 锁
相比同步代码块synchronized,Lock 可以自己来手动上锁,手动释放锁。注意:Lock 锁是接口不能直接实例化,需要使用他的实现类ReentrantLock来实例化。

将上面的 售票类 - SellTicket 改写成Lock 锁的方式,测试类不变
package com.进程和多线程2_安全问题之卖票例子;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SellTicket extends Thread {
// 表示这个类的所有对象都共享 ticket 数据
static int ticket = 0;
// 方式三:lock 方法的方式 start ---------
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 加锁
lock.lock();
try {
if (ticket >= 100) {
break;
}
Thread.sleep(10);
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁 - 注意:break 后会执行 finally里面的代码后才终止循序
// finally 的优先级高于 break/return(除非调用 System.exit() 或 JVM 崩溃)。
lock.unlock();
}
}
}
// 方式三:lock 方法的方式 end ---------
}
这里有两个注意点:
1.Lock 这里是 static 修饰的,同样是因为这个类是继承的 Thread ,我们在使用的时候实例化了三次这个类,为了保证三个实例化对象都共享一把锁,所以需要使用 static
2.lock.unlock();释放锁是放到的 try catch 的 finally 里面,这里有个特性是 finally 的优先级高于 break/return(除非调用 System.exit() 或 JVM 崩溃)。所以当执行 break 的时候,会先执行 finally 里面的代码才会去终止 while 循环。
八、死锁的错误
死锁,不需要学习其他代码,这是一种程序的错误,我们需要理解并且避免使用它。死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去,程序无法正常运行。
典型死锁示例:

要想不出现死锁,我们平时写代码的时候需要注意不要让锁出现嵌套的情况。
九、生产者和消费者(等待唤醒机制)

方式一:简单实现

吃货和厨师的例子:
调度中心 - Desk (桌子)类
package com.进程和多线程3_等待唤醒机制;
public class Desk {
/*
* 作用:控制生产者消费者的执行
* */
// 是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
// 总的要生产的个数
public static int count = 10;
// 锁对象
public static Object lock = new Object();
}
消费者 - Foodie (吃货) 类
package com.进程和多线程3_等待唤醒机制;
public class Foodie extends Thread {
/*
* 多线程的套路:
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(到了末尾)
* 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
* */
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
// 吃完了跳出循环
if(Desk.count == 0) {
break;
}else{
// 先判断桌子上是否有面条
if(Desk.foodFlag == 0) {
// 如果没有,就等待
try {
Desk.lock.wait(); // 让当前线程和锁进行变绑定,后面通知的时候是通知的和锁绑定的线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// 把吃的总数 -1
Desk.count--;
// 如果有就开吃
System.out.println("吃货正在吃面条,还能吃"+ Desk.count + "碗!!!");
// 吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
// 修改桌子的状态
Desk.foodFlag = 0; }
}
}
}
}
}
生产者 - Cook (厨师)类
package com.进程和多线程3_等待唤醒机制;
public class Cook extends Thread {
/*
* 多线程的套路:
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(到了末尾)
* 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
* */
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
// 已经生产完了,消费者吃不下了,就停止
if(Desk.count == 0) {
break;
}else{
// 桌子上有是否有食物
if(Desk.foodFlag == 1) {
// 有就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
// 没有就生产
System.out.println("厨师做了一碗面条");
// 修改桌子上食物的状态
Desk.foodFlag = 1;
// 唤醒等待的消费者,开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
测试类
package com.进程和多线程3_等待唤醒机制;
public class test {
public static void main(String[] args) {
Foodie foodie = new Foodie();
Cook cook = new Cook();
// 设置名字
foodie.setName("吃货");
cook.setName("厨师");
// 启动线程
foodie.start();
cook.start();
}
}
执行结果如下:
可以看到是abababab...这种有规律的执行顺序,也就是生产一份消费一份生产一份消费一份。。。这种
image.png
方式二:阻塞队列方式实现
相比上一种,这个生产者可以一次生产多个,直到放不下了才开始等待,消费者则是可以吃完多个,直到“管道“拿不出来了再等待。


注意这两个实现类:ArrayBlockingQueue 和 LinkedBlockingQueue,一个是有界的一个是无界的。
消费者 - Foodie 类
package com.进程和多线程4_阻塞队列方式实现;
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread {
// 定义一个队列来接收和消费者公用的阻塞队列
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
// 不断的从阻塞队列中获取面条
try {
// 注意:queue.take 里面有锁,这里不需要再定义
String food = queue.take();
// 注意下面这个打印语句是在锁外面执行的,因此运行后看起来的结果可能是同时执行了多次,但是不影响,因为没有操作共享数据
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者 - Cook 类
package com.进程和多线程4_阻塞队列方式实现;
import java.util.concurrent.ArrayBlockingQueue;
public class Cook extends Thread {
// 定义一个队列来接收和消费者公用的阻塞队列
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
// 不断的把面条放到阻塞队列里面
try {
// 注意:queue.put 里面有锁,这里不需要再定义
queue.put("面条");
//注意下面这个打印语句是在锁外面执行的,因此运行后看起来的结果可能是同时执行了多次,但是不影响,因为没有操作共享数据
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
test 测试类
package com.进程和多线程4_阻塞队列方式实现;
import java.util.concurrent.ArrayBlockingQueue;
public class test {
public static void main(String[] args) {
// 定义一个阻塞队列,注意这个队列必须是生产者和消费者共享的
// 注意:ArrayBlockingQueue 是有界的,这里的参数1代表这个队列容量是1个,因此生产者只会生产1个就等待
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
// 定义线程并启动
Cook cook = new Cook(queue);
Foodie foodie = new Foodie(queue);
cook.start();
foodie.start();
}
}
注意:
1.这个例子会一直执行,因为没有在生产者和消费者里面写终止循环的代码,这里只是演示阻塞队列的用法,实际也可以像方式一一样定义一个调度中心来进行判断。
2.注意这里的打印语句是在锁外面执行的,可能看到的结果是同时执行了多次,但是不影响,因为没有操作共享数据,这里只是打印的显示有问题。
方式三: 将等待、通知的逻辑写到调度中心里面去
这个方式和上一种方式有些类似。生产者只关心怎么生产数据,消费者只关心怎么消费数据, 将等待、通知的逻辑写到调度中心里面去。
package com.进程和多线程6_生产者消费者demo6;
import java.util.LinkedList;
import java.util.Queue;
public class test {
public static void main(String[] args) {
Buffer bf = new Buffer();
Producer producer = new Producer(bf);
Consumer consumer = new Consumer(bf);
producer.start();
consumer.start();
}
}
// 生产者
class Producer extends Thread {
private Buffer buf;
public Producer(Buffer buf) {
this.buf = buf;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
buf.add(i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer extends Thread {
private Buffer buf;
public Consumer(Buffer buf) {
this.buf = buf;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
int poll = buf.poll();
System.out.println("消费者 - " + Thread.currentThread().getName() + "消费了数据:" + poll);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Buffer {
// 数据存储队列
private Queue<Integer> queue = new LinkedList<>();
// 这个队列的大小是5
private int size = 5;
// 添加数据
public synchronized void add(int x) throws InterruptedException {
// 数据满了则阻塞生产者生产数据
if (queue.size() >= size) {
wait();
}
// 数据不够则开始生产
queue.add(x);
// 通知消费者消费
notify();
}
// 获取数据
public synchronized int poll() throws InterruptedException {
// 没有数据则等待生产者生产数据
if (queue.size() == 0) {
wait();
}
// 有数据就获取,获取了再通知消费者生产
int x = queue.poll();
// 通知生产者继续生产
notify();
return x;
}
}
十、多线程的内存图解
注意:每个线程都有自己的栈空间,而堆空间只有一个

十一、线程池
Java线程池是Java并发编程中非常重要的一个组件,它能够有效地管理线程的生命周期,减少线程创建和销毁的开销,提高系统性能。
通常是创建一个线程池,这个线程池不会销毁,当需要线程的时候在里面拿来用,用完还回去,后面的任务来了如果线程池没有空闲线程,而且没有达到上限,那么会新创建线程。如果没有空闲线程并且达到上限了就会等待,直到有空闲线程了再执行这个任务。

方式1:用java的工具类来创建线程池

myRunnable 类
package com.进程和多线程6_线程池;
public class myRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
测试类
package com.进程和多线程6_线程池;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test {
public static void main(String[] args) throws InterruptedException {
method1();
method2();
}
// 没有上限的线程池
private static void method1() throws InterruptedException {
// 创建线程池对象
ExecutorService pool = Executors.newCachedThreadPool();
// 提交任务
pool.submit(new myRunnable());
//Thread.sleep(100); // 睡眠上个线程可以看到线程是复用的
pool.submit(new myRunnable());
// 销毁线程池
//pool.shutdown();
}
// 有上限的线程池
private static void method2(){
// 创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交任务
pool.submit(new myRunnable());
//Thread.sleep(100); // 睡眠上个线程可以看到线程是复用的
pool.submit(new myRunnable());
pool.submit(new myRunnable());
pool.submit(new myRunnable());
pool.submit(new myRunnable());
// 销毁线程池
//pool.shutdown();
}
}
方式2:更灵活的方式 - 自定义线程池 (ThreadPoolExecutor)
这个类的构造方法的参数太多,这里类比饭店例子来理解。


测试类:
package com.进程和多线程6_线程池;
import java.util.concurrent.*;
public class test {
public static void main(String[] args) throws InterruptedException {
method3();
}
// 方式三:自定义线程池 - 就是参数太多了点
private static void method3(){
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数量,不能小于0
6, // 最大线程数,>= 核心线程数量 (临时线程数 = 最大线程数 - 核心线程数)
60, // 空闲线程最大存活时间
TimeUnit.SECONDS, // 时间单位,这里是秒,注意:需要用 TimeUnit
new LinkedBlockingQueue<>(), // 任务队列, ArrayBlockingQueue 或者 LinkedBlockingQueue
Executors.defaultThreadFactory(), // 创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
);
// 提交任务
pool.submit(new myRunnable());
pool.submit(new myRunnable());
}
}
那么什么时候创建临时线程?
答:核心线程都在忙,并且排队的队伍已经满了
任务的执行顺序一定是先提交的先执行吗?
答:不是,就像下面图里面展示的,7和8任务先于任务4,5,6执行。先得排队排满了才会去创建临时线程执行后面的任务。
image.png
任务拒绝策略
当任务超出了(核心线程数 + 临时线程数 + 队伍长度)就会触发任务拒绝策略。
image.png
image.png

十二、线程池设置多大合适
1.获取最大并行数量
首先4核8线程的电脑的最大并行数量为8,就是最理想的情况下同时可以做8件事。(4核8线程的CPU在操作系统层面可以同时管理8个线程,通过超线程技术让4个物理核心模拟出8个逻辑处理器的效果,在理想情况下可以近似实现8个任务的并发执行,但实际并行计算能力仍以4个物理核心为基础。)
在java里面可以用下面的代码来获取java 虚拟机可用的处理器数目
int i = Runtime.getRuntime().availableProcessors();
System.out.println("java 虚拟机可用的处理器数目"+i);
2.计算公式

CPU 密集型运算是指你的项目是偏计算的,I/O密集型运算是指你的项目读取数据库或者本地文件的操作比较多。根据不同场景来套上面的公式达到cpu最大利用率。
十三、多线程扩展内容
通常面试才用的到,初学略过。。。







