目录
(4)setPriority 和 getPriority 方法
1、定义
进程:进程是程序的基本执行实体(每个软件都有对应的一个进程)
线程:线程是操作系统能够运行调度的最小单位。它被包含在进程之中,是进程的实际运作单位。
(简单理解:应用软件中相互独立,可以同时运行的功能,就是不同的线程)
单线程程序:CPU从上往下依次运行,不会切换到其他代码中运行
多线程程序:CPU会在不同程序间进行切换,将等待的时间充分利用起来
简单理解:多线程可以让程序同时做多件事情,从而提高效率。
2.并发和并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
以CPU是 2 核 4 线程为例,4 线程代表CPU可以同时并行 4 个线程。
每个物理核心在支持超线程技术的情况下,最多可以同时执行 2 条线程,所以 2*2=4
如果超出了 4 个线程需要运行,就会在多个线程之间依靠算法进行调度(切换),也就是并发。
所以,在计算机中,并发和并行是可能同时存在的。
3.实现方式
(1)继承Thread类
步骤:
1.自定义一个类继承Thread类
2.重写run方法
3.创建子类对象,并启动线程
public class MyThread extends Thread {
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "HelloWorld");
}
}
}
注意:由于自定义类继承了 Thread类,所以可以使用 Thread类中的 getName 方法。
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
//给线程起名字
t1.setName("线程1");
t2.setName("线程2");
//启动线程
t1.start();
t2.start();
}
}
运行结果:
、
根据结果,可以发现,两个线程会不停的交替执行。
(2)实现Runnable接口
步骤:
1.自定义一个类实现 Runnable 接口
2.重写 run 方法
3.创建自定义类对象
4.创建 Thread 类对象,并开启线程
public class MyRun implements Runnable {
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 5; i++) {
//获取到当前线程的对象
Thread t = Thread.currentThread();
System.out.println(t.getName() + "HelloWorld");
}
}
}
注意:
由于自定义类没有继承 Thread类,和 Thread类无关,所以不能直接使用 getName 方法。
需要先使用 Thread类的静态方法 currentThread 来获取执行当前任务的线程对象。
public class ThreadDemo {
public static void main(String[] args) {
//创建自定义类的对象(表示多线程要执行的任务)
MyRun mr=new MyRun();
//创建线程对象(将任务传递给该线程对象)
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
//给线程起名字
t1.setName("线程1");
t2.setName("线程2");
//启动线程
t1.start();
t2.start();
}
}
运行结果:
(3)利用Callable接口和Future接口
特点:可以获取到多线程执行的结果
前两种方式都是通过重写 run 方法,而 run 方法的返回值类型是 void 类型。
如果需要获取多线程执行的结果(返回一个其他类型的返回值),将无法实现。
步骤:
1.自定义类实现 Callable 接口
2.重写 call 方法(是有返回值的,表示多线程运行的结果)
3.创建自定义类对象(表示多线程要执行的任务)
4.创建 Future 接口的实现类 FutureTask 的对象(作用:管理多线程运行的结果)
5.创建 Thread 类的对象,并启动(表示线程)
public class MyCallable 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;
}
}
注意: Callable 接口中的泛型就是多线程运行结果的返回值类型,所以需要指定该数据类型。
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建自定义类对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
//创建Future接口的实现类FutureTask的对象(作用:管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程的对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
}
细节:
FutureTask 对象就是用来管理多线程运行的结果的,所以该对象可以调用 get 方法获取多线程运行的结果,返回值类型就是泛型中指定的数据类型。
三种实现方式的对比:
注意:Java中只支持单继承,不支持多继承。所以继承了 Thread 类,就不能再继承其他的类。
4.成员方法
(1)getName 和 setName 方法
细节:
① 如果我们没有使用 setName 方法给线程设置名字,线程默认也是有名字的。
默认格式:Thread-X(X是序号,从 0 开始)
在 Thread 类的空参构造方法, 第三个参数就是给线程起名字。
nextThreadNum 方法就是线程的序号,其中 threadInitNumber 没有赋值,默认初始化值为 0。
② 不仅可以通过 Thread 对象调用 setName方法给线程起名字,Thread 的构造方法也可以设置名字。
注意:
Thread 类的构造方法可以传递参数设置名字,而 Java 中构造方法是不能继承的。
自定义类想要通过构造方法设置名字,就必须在本类的构造方法中调用父类 Thread 的构造方法。
(2)currentThread 方法
细节:
当 JVM 虚拟机启动后,会自动的启动多条线程。
其中,有一条线程就叫做 main 线程,作用是去调用 main 方法,执行其中的代码。
(3)sleep 方法
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("aaaa");
//main线程睡眠5s
Thread.sleep(5000);
System.out.println("bbbb");
}
}
细节:
① sleep 方法是静态方法,直接通过 类名.方法名 调用即可。
② 哪条线程执行 sleep 方法,那它就会在这里停留对应的时间。
③ 方法的参数:就表示睡眠的时间,单位是毫秒 ms 。
④ 当时间到了之后,线程会自动苏醒,继续执行下面的代码。
(4)setPriority 和 getPriority 方法
线程的调度分为两种:抢占式调度和非抢占式调度
抢占式调度:多个线程抢夺CPU的执行权,执行哪个线程,执行多长时间都是随机的。
非抢占式调度:所有的线程轮流执行。
注意:在 Java 当中,线程的调度采用抢占式调度方式。
优先级就表示该线程随机到的概率,优先级越大,抢到 CPU 的概率越大。
细节:
① 如果没有设置优先级,那么默认优先级都是 5 ,main 线程的优先级也是5。
② 在 Thread 类的成员变成中,优先级最小为 1,最大为 10,默认为 5 。
③ 优先级大的只是概率大,不是绝对的优先。
(5)setDaemon 方法(守护线程)
细节:当其他的非守护线程执行完毕之后,守护线程也会(在极短时间内)陆续结束。
运行结果:
在线程 1(非守护线程)执行完毕后,线程 2(守护线程)也就随之结束了(即使没执行完)。
线程 1 执行完毕后,系统会告知线程 2 结束,告知的这个过程也要时间,所以线程 2会继续执行。
应用场景: 主功能(聊天)结束/关闭了,次功能(传输文件)也就随之结束了。
(6)yield 方法(出让线程 / 礼让线程)
细节:
yield 方法表示该线程出让当前CPU的执行权,这样其他线程就能抢到,使运行结果尽可能的均匀
注意:
只是使结果尽可能的均匀,并不完全一定。
因为 t1 在出让执行权之后,很有可能继续抢到该执行权。
(7)join 方法(插入线程)
在正常情况下 ,线程 1 会和 main 线程互相抢夺 CPU 的执行权。
只不过 main 线程在抢到之后,瞬间就执行完毕了。
Question:有没有什么办法,先让线程1先执行完毕,再轮到main 线程中的 for 循环代码执行呢?
使用 join 方法将线程 1 插入到当前线程(main线程)之前,就可以使得线程1 执行完毕之后,才轮到main 线程继续执行后续代码。
5.线程的生命周期
注意:
就绪状态下,线程有执行资格,也就是有资格去抢夺CPU的执行权。
正在抢,但还没有抢到,所以没有执行权。一旦抢到执行权,就会变成运行状态。
Question:sleep方法会让线程睡眠,睡眠时间到了之后,就立马会执行下面的代码吗?
不会,sleep方法结束后,线程会变成就绪状态, 先去抢夺CPU的执行权,抢到才会执行代码。
6.线程的安全问题
需求:有100张票,三个窗口在卖票,设计程序进行模拟。
由于 ticket 是成员变量,new 创建了 3 个线程对象,这三个对象中都有自己独立的 ticket 属性。
所以实际上,卖了 300 张票。为了 让 ticket 共享,要将其设置为静态变量。
但即使将 ticket 修改为静态变量后,我们发现,同一张票也可能被买了多次,甚至可能出现漏票、错票的情况。
Question1:为什么同一张票会卖多次?
这时由于线程执行时,具有随机性,也就是随时随地,都有可能被抢夺CPU的执行权。
当线程 2 首次抢夺到 CPU的执行权后,执行了打印语句。
这时,线程 1 突然抢到了 CPU 的执行权,就会使得线程 2 来不及执行 ticket++;的代码。
所以 ticket 此时还是为 1,线程 1 会继续卖第 1 张票。
同理,此时线程 3 突然抢到了 CPU的执行权,线程 1 也就会来不及执行 ticket++;的代码。
所以 ticket 此时还是为 1,线程 3 也会继续卖第 1 张票。、
Question2:为什么会漏票?
由于线程执行时,具有随机性。
当线程 1 卖完第 4 张票后,如果执行权还在,继续执行 ticket++;的代码,ticket自增为 5。
此时线程 2 突然抢到了 CPU 的执行权,由于也卖完了第 4 张票,会执行 ticket++,ticket自增为 6
接下来,线程 3 突然抢到了 CPU 的执行权,它就会卖出第 6 张票。
7.同步代码块
为了解决上述的线程安全问题,我们可以将操作共享数据 ticket 的代码进行加锁。
当线程运行到被锁住的代码的时候,只有该线程能执行操作,其他线程将等待锁被释放。
只有当该线程执行完毕后,其他线程才有资格去抢夺执行权。
同步代码块:把操作共享数据的代码锁起来
特点:
① 锁默认打开,当有一个线程进去了,锁自动关闭。
② 锁中代码全部执行完毕,线程出来后,锁自动打开。
运行结果:
注意:
① synchronized 同步代码块要写在循环内,不能写在循环外。
如果写在循环外,那么线程将会一次性将内部循环全部执行完毕,就轮不到其他线程执行。
运行结果 :
线程 1 在抢到执行权之后,会运行完整个 while 语句,才能从锁中出来。
所以 其他线程就算抢到执行权,也会被关在锁外,全程只有线程 1 在执行代码。
② synchronized 的括号中传递的锁对象,可以是任意对象,但一定要是唯一的(static修饰)。
如果锁对象是唯一的,那么线程 1进入后立即上锁,线程 2 发现锁关闭后,就会进入等待。
但如果锁对象是不同的,那么同步代码块等于没写:
线程 1 看 锁对象 1 有没有上锁,没上锁进入代码块执行代码。
线程 2 看 锁对象 2 有没有上锁,也没上锁进入代码块执行代码。
③ synchronized 的括号中传递的锁对象,我们一般设为当前类的 class 对象,因为其是唯一的。
④ sleep 方法最好写在 synchronized 外,因为写在内部的话,即使其他线程抢到了执行权,也会被锁在外面。
8.同步方法
如果整个方法都需要进行加锁,就没必要将整个方法的所有代码都放进同步代码块中了。
同步方法:将 synchronized 关键字加到方法上,此时这个方法称为同步方法。
特点:
① 同步方法会锁住方法里面所有的代码
② 锁对象不能自己指定,是 java 已经规定好的:
非静态方法:this
静态方法:当前类的字节码文件
注意:
在上面,ticket 设置为静态变量,是因为通过继承 Thread 类实现的多线程。
如果要创建多个线程,就会创建多个 MyThread对象。
而现在是通实现 Runnable 接口实现的多线程,MyRunnable 类只会创建一个对象。
因为这个对象是作为参数交给线程去执行的,所以只创建 1 个对象,ticket 就不用设置为静态变量了。
9.Lock锁
在同步代码块和同步方法中,我们并不能清晰的看到在哪里加锁,哪里释放锁。
JDK 5以后提供了一个新的锁对象 Lock,实现提供比使用 synchronized方法和语句更广泛的锁定操作。
Lock 中提供了获得锁和释放锁的方法:
注意:
Lock 是一个接口,不能直接实例化,需要采用它的实现类 ReentrantLock 来实例化。
运行结果:
Question1:根据结果发现,又出现一张票被卖多次,和漏票的情况,这是怎么回事呢?
在 MyThread 类的成员变量中,我们 new 创建了一个 Lock 的实现类对象。
但是,在创建线程时,我们创建了多个 MyThread 类的对象。
这就会导致每个 MyThread 对象内部都会创建一个 Lock 锁的对象,又导致了不同线程看不同锁的情况。
解决办法:我们必须在 Lock 锁的对象前,加上 static 关键字,让多个线程共享同一把锁。
运行结果:
Question2:为什么程序运行结果是正常的,但程序不会停止呢?
当线程第 101 次进入循环,这时 ticket =101,那么将直接从 break 语句跳出循环。
所以第 101 次并没有执行 lock.unlock(); 锁也就不会释放,那么程序也就不会停止。
虽然可以在break 语句前再加上 lock.unlock(); 但这样会导致释放锁的(结束)代码写了两遍。
解决办法:利用 try - catch - finlly 语句的性质,将释放锁的代码写在 finally 中。
此时就可以保证,锁一定会被释放,程序也就可以正常结束了。
10.死锁
运行结果:
原因:
由于线程 B 先抢到执行权,所以执行代码,将 B 锁关闭,执行打印语句(“线程B拿到了B锁...”)
但线程执行时,具有随机性。
此时线程 A 突然抢到了执行权,发现 A锁是打开的,于是执行打印语句("线程A拿到了A锁")。
接下来,线程 A 继续往下执行,发现 B 锁被线程 B拿到,处于关闭状态,则会一直等待B锁释放。
线程 B 继续往下执行,发现 A 锁被线程 A 拿到,处于关闭状态,则会一直等待A锁释放。
这就会造成多个线程处于互相等待状态,谁都无法继续执行下去,这就称为死锁。
结论:在涉及多个锁时,不要让两个锁发生嵌套,否则有可能会产生死锁。
11.等待唤醒机制(生产者与消费者)
在这个过程中,涉及以下方法:
(1)一般写法
//消费者:消费数据
public class Consumer extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
//1.判断桌子上是否有食物
if (Desk.foodFlag == 0) {
//2.如果没有,就等待
try {
Desk.lock.wait();//底层:让当前线程跟Desk.lock锁绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//3.如果有,就吃
Desk.count--;
System.out.println("正在吃面条,还能再吃" + Desk.count + "碗");
Desk.foodFlag = 0;//修改桌子上食物的状态
//4.吃完之后,唤醒生产者继续制作
Desk.lock.notifyAll();//唤醒只跟这把锁绑定的所有线程
}
}
}
}
}
}
注意:
唤醒的时候我们需要唤醒指定的某些线程,而不是唤醒计算机中所有的线程。
Desk.lock.wait();在底层会让当前线程跟Desk.lock锁绑定,这样唤醒的时候就只会唤醒跟这把锁绑定的线程。
public class Producer extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
//1.判读桌子上是否有食物
if (Desk.foodFlag == 1) {
//2.如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//3.如果没有,就制作食物
System.out.println("正在制作食物");
Desk.foodFlag=1;//修改桌子上食物的状态
//4.唤醒等待的消费者
Desk.lock.notifyAll();
}
}
}
}
}
}
//中间者(桌子):控制生产者与消费者的执行
public class Desk {
//是否有食物(0:没有 1:有)
public static int foodFlag = 0;
//食物的总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
//完成生产者与消费者交替执行
public class ThreadDemo {
public static void main(String[] args) {
//创建线程对象
Consumer c = new Consumer();
Producer p = new Producer();
//给线程起名字
c.setName("消费者");
p.setName("生产者");
//开启线程
c.start();
p.start();
}
}
运行结果:
(2)利用阻塞队列实现
细节:生产者和消费者必须使用同一个阻塞队列
public class Consumer extends Thread {
ArrayBlockingQueue<String> queue;
public Consumer(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
//不断的从阻塞队列中取出食物
try {
String food = queue.take();
System.out.println("吃了一碗" + food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:
此时的 run 方法是不需要加同步代码块/方法的,因为在 take 方法底层已经加了Lock锁。
我们不能再在 run 方法中添加 同步代码块/方法,这样就会产生锁的嵌套,从而产生死锁。
//生产者
public class Producer extends Thread {
ArrayBlockingQueue<String> queue;
public Producer(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
//不断的把食物放到阻塞队列中
try {
queue.put("面条");
System.out.println("制作了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:
此时的 run 方法是不需要加同步代码块/方法的,因为在 put 方法底层已经加了Lock锁。
我们不能再在 run 方法中添加 同步代码块/方法,这样就会产生锁的嵌套,从而产生死锁。
public class ThreadDemo {
public static void main(String[] args) {
//1.定义一个阻塞队列(泛型:表示队列中数据的类型,参数:表示队列中的数据的上限)
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//2.创建线程的对象,将阻塞队列传递过去(确保生产者和消费者共用一个阻塞队列)
Producer p = new Producer(queue);
Consumer c = new Consumer(queue);
//3.启动线程
p.start();
c.start();
}
}
注意:
要通过创建线程对象的方式,将同一个阻塞队列的对象传递过去,从而确保生产者和消费者共用一个阻塞队列。
运行结果:
Question:结果发现,竟然会打印出消费者多次吃食物和生产者多次制造食物的情况,并不是交替轮流执行,这是为什么呢?
因为在代码中,加锁和释放锁的操作是在 take 和 put 语句内执行的,它确保了共享数据的正常执行。
而打印语句,是在 take 和 put 语句外,也就是在锁外,所以它仍是有可能被抢占执行权的。
这就造成了打印语句看起来数据处理不正常的情况。
但实际上,数据都在 take 和 put 语句中执行,是不会受到任何影响的。
12.线程的完整状态
注意:
在 Java中,实际上是没有定义运行状态的,只有其他 6 种状态。
因为线程在抢夺到CPU的执行权之后,此时JVM虚拟机就会将当前的线程交给操作系统进行管理。
由于已经交出去了,JVM不再负责管理,所以Java就没有定义运行状态了。
所以,在Java中,只有以下 6 种 状态:
13.线程栈
在 Java中,堆内存是唯一的;而栈内存不是唯一的,栈和线程是相关的。
首先 main 线程会创建自己的栈,将 main 方法进栈。
然后创建 t1 和 t2 对象,线程的名字初始化为 Thread-0 和 Thread-1。
接着,通过 setName 方法,重新给两个线程起名字,对象中 name 属性的值就会随之发生改变。
start 方法启动线程后,内存中就会给 线程 1 和 线程 2 开辟两个新的栈。
注意:每一个线程都有一个自己独立的栈空间。
接着,线程 1和 线程 2 都会去执行 run 方法,所以 run 方法会同时进入两个线程的栈。
那么 run 方法中的 局部变量 b,也就会在两个线程的栈中都会定义。
14.线程池
通过之前的方式所创建的线程,会带来以下弊端:
① 用到线程的时候就创建
② 用完之后线程就消失
但这种方式并不好,会过度浪费操作系统的资源。
(1)定义
线程池就是一个容器,初始状态下是空的。
当给线程池提交一个任务时,线程池就会自动创建一个线程。
这个线程就会去执行任务,任务执行完毕后,线程不会消失,会回到线程池中。
如果下次继续有新的任务,会再次将该线程拿来复用,不会创建新的线程。
如果任务过多,就会自动创建新的线程。
线程池中容纳线程的数量是有上限的,而且这个上限是可以自行设置的。
如果任务超出了该上限,线程池中没有空闲线程,那么其他任务就只能排队等待。
(2)实现
注意:没有上限并不是真正的没有上限,而是上限为 int 类型的最大值,也就是 21 亿多。
Ⅰ. newCachedThreadPool 方法
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public class PoolDemo {
public static void main(String[] args) throws InterruptedException {
//1.获取线程池对象(无上限)
ExecutorService pool1= Executors.newCachedThreadPool();
//2.提交任务
pool1.submit(new MyRunnable());
//让main线程睡眠0.1s,使得线程有充足的时间执行完任务,然后回到线程池中
Thread.sleep(100);
pool1.submit(new MyRunnable());
Thread.sleep(100);
pool1.submit(new MyRunnable());
//3.销毁线程池
pool1.shutdown();
}
}
注意:
如果main线程不睡眠,那么它将会和线程中的线程抢夺执行权。
线程池中的线程1还没执行完, main线程就已经会往下执行了。
这时线程池中没有空闲线程,从而线程池只能分配新的线程去执行新的任务。
运行结果:
可以发现,虽然执行了三次任务,但都是同一个线程在执行。
Ⅱ. newFixedThreadPool 方法
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "@" + i);
}
}
}
public class PoolDemo2 {
public static void main(String[] args) throws InterruptedException {
//1.获取线程池对象
ExecutorService pool1= Executors.newFixedThreadPool(2);
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池
pool1.shutdown();
}
}
运行结果:
可以看出,由于线程池指定了上限为2,所以最多只有两个线程在执行任务。
且只有当有线程执行完毕,回到线程池中后,线程池中才会有空闲线程,才能执行下次任务。
15.自定义线程池
通过上述两个静态方法可以创建线程池,但仍是不够灵活。
例如:有多个任务需要处理,且没有空闲线程,那么任务就会排队等待,我们能否设置此队列的长度呢?
查看 newCachedThreadPool 和 newFixedThreadPool 的源码,我们发现:
底层事实上 new 创建了一个 ThreadPoolExecutor 对象,就是代表线程池的类。
(1)定义
ThreadPoolExecutor 的构造方法中,有 7 个参数:
① 允许创建的最大核心线程数量(不能小于0)
② 允许创建的最大线程数量
(不能小于 0 且必须 >= 核心线程数量,
最大线程数量 - 最大核心线程数量 = 最大临时线程数量)
③ 临时线程的最大存活时间1(设置时间的值,不能小于0)
④ 临时线程的最大存活时间2(设置时间的单位,使用TimeUnit指定)
⑤ 任务队列(其实就是一个阻塞队列,不能为null)
⑥ 创建线程的工厂(也就是怎样创建一个线程,不能为null)
⑦ 任务的拒绝策略(一共有四种策略,四选一,一般选择第一个,不能为null)
(2)过程
初始状态下,线程池是空的,我们设置核心线程的数量为 3,临时线程的数量也为 3。
当提交了3个任务时,线程池就会创建 3 个核心线程分别去执行任务。
当提交了 5 个任务时,线程池中的 3 个核心线程执行前三个任务。
剩下两个任务进入任务队列排队等待,直到有空闲线程,才会被执行。
我们将任务队列的长度定义为 3。
当提交了 8 个任务时,前三个任务交给核心线程执行,456 三个任务放入任务队列中。
此时任务队列的容量已经装满,那么就会创建 2 个临时线程去执行剩下的任务 7 和 8。
细节:
① 当核心线程都在运行,任务队列已经满了时,才会创建临时线程。
② 任务在执行时,不一定是严格按照提交的顺序执行的(如上:1-2-3-7-8-4-5-6)
当提交了 10 个任务是,前三个任务交给核心线程执行,456 三个任务放入任务队列中。
此时任务队列的容量已经装满,那么就会创建 3 个临时线程去执行剩下的任务 789。
由于临时线程的数量也已达到最大,那么此时剩下的任务 10 就会触发任务拒绝策略(默认舍弃)
//创建自定义线程池
ThreadPoolExecutor pool2=new ThreadPoolExecutor(
3, //设置核心线程的最大创建数量
6, //设置最大线程数量(最大临时线程数量也就是6-3=3)
60, //设置临时线程的最大空闲时间的值部分
TimeUnit.SECONDS, //设置临时线程的最大空闲时间的单位部分
new ArrayBlockingQueue<>(3, //任务队列
Executors.defaultThreadFactory(), //创建线程的工厂
new ThreadPoolExecutor.AbortPolicy() //任务的拒绝策略
);
(3)最大并行数
前面说过,以CPU是 2 核 4 线程为例:
2 核代表CPU有 2 个物理核心,4 线程代表CPU可以同时并行 4 个线程。
每个物理核心在支持超线程技术的情况下,最多可以同时执行 2 条线程,所以 2 * 2=4。
最大并行数:就是最多可以同时并行线程的数量,也就是 4。
Question: 那线程池的最大线程数量是不是越大越好呢?
其实不然,线程池的最大线程数量通常是按照规定来的,这取决于开发的项目是CPU密集型还是I/O密集型的。
CPU密集型:项目中频繁涉及计算(计算需要靠 cpu 执行)。
IO密集型:项目中频繁涉及读取本地文件或者数据库(IO操作不需要CPU)--> 目前大多数项目
注意:
① +1是为了保证当已经创建了的某个线程出现问题时,可以利用这个多出来的线程继续工作, 尽可能地将CPU利用率最大化。
② 操作 1 和 操作 2中,操作 2 是相加,涉及计算,需要 CPU 执行,所以CPU计算时间 = 1s 。