【Java基础】【java.lang】多线程

概述

线程

  • 线程(Thread)是一个程序内部的一条执行流程。
  • 程序中如果只有一条执行流程,那这个程序就是单线程的程序。

多线程

  • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
  • 例如:买票系统、百度网盘下载、消息通信、淘宝、京东系统都离不开多线程技术。

创建方法

Java是通过java.lang.Thread 类的对象来代表线程的。

注意main方法由一条默认的主线负责执行。在main里面启动的线程称为子线程。

方式1:继承Thread类

步骤

  1. 定义一个子类MyThread继承线程类java.lang.Thread。
  2. 重写run()方法。
  3. 创建MyThread类的对象。
  4. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)。

优缺点

  • 优点:编码简单。
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

注意事项

  1. 启动线程必须是调用start方法,不是调用run方法。
    • 如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。
  2. 不要把主线程任务放在启动子线程之前(否则先跑完主线程才启动子线程)。
    • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
    • 只有调用start方法才是启动一个新的线程执行。
    • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread{
// 2、必须重写Thread类的run方法
@Override
public void run() {
// 描述线程的执行任务。
for (int i = 1; i <= 5; i++) {
System.out.println("子线程MyThread输出:" + i);
}
}
}

再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTest1 {
// main方法是由一条默认的主线程负责执行。
public static void main(String[] args) {
// 3、创建MyThread线程类的对象代表一个线程
Thread t = new MyThread();
// 4、启动线程(自动执行run方法的)
t.start();

for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}

MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样)。

方式2:实现Runnable接口

步骤

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。

  2. 创建MyRunnable任务对象。

  3. 把MyRunnable任务对象交给Thread线程对象处理。

    public Thread(Runnable target)。封装Runnable对象成为线程对象。

  4. 调用线程对象的start()方法启动线程。

优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  • 缺点:需要多一个Runnable对象。

先准备一个Runnable接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 1、定义一个任务类,实现Runnable接口
*/
public class MyRunnable implements Runnable{
// 2、重写runnable的run方法
@Override
public void run() {
// 线程要执行的任务。
for (int i = 1; i <= 5; i++) {
System.out.println("子线程输出 ===》" + i);
}
}
}

再写一个测试类,在测试类中创建线程对象,并执行线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTest2 {
public static void main(String[] args) {
// 3、创建任务对象。
Runnable target = new MyRunnable();
// 4、把任务对象交给一个线程对象处理。
// public Thread(Runnable target)
new Thread(target).start();

for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出 ===》" + i);
}
}
}

匿名内部类写法(简化代码风格)

  1. 可以创建Runnable的匿名内部类形式(任务对象)。
  2. 再交给Thread线程对象。
  3. 再调用线程对象的start()启动线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadTest2_2 {
public static void main(String[] args) {
// 1、直接创建Runnable接口的匿名内部类形式(任务对象)
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(target).start();

// 简化形式1:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();

// 简化形式2:
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();

for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}

方式3:实现Callable接口

原因:

  • 前面两种的问题:假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。

解决方案:

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
  • 这种方式最大的优点:可以返回线程执行完毕后的结果。

步骤

  1. 创建任务对象
    • 定义一个类实现Callable接口重写call方法,封装要做的事情,和要返回的数据。
    • 把Callable类型的对象封装成FutureTask(线程任务对象)
  2. 把线程任务对象交给Thread对象。
  3. 调用Thread对象的start方法启动线程。
  4. 线程执行完毕后,通过FutureTask对象的的get方法去获取线程任务执行的结果。

未来任务对象的作用?

  1. 是一个任务对象,实现了Runnable对象。
  2. 可以在线程执行完毕后,通过FutureTask对象的的get方法去获取线程任务执行的结果。

先准备一个Callable接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 1、让子类继承Thread线程类。
*/
public class MyThread extends Thread{
// 2、必须重写Thread类的run方法
@Override
public void run() {
// 描述线程的执行任务。
for (int i = 1; i <= 5; i++) {
System.out.println("子线程MyThread输出:" + i);
}
}
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ThreadTest3 {
public static void main(String[] args) throws Exception {
// 3、创建一个Callable的对象
Callable<String> call = new MyCallable(100);
// 4、把Callable的对象封装成一个FutureTask对象(任务对象)
// 未来任务对象的作用?
// 1、是一个任务对象,实现了Runnable对象.
// 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
FutureTask<String> f1 = new FutureTask<>(call);
// 5、把任务对象交给一个Thread对象
new Thread(f1).start();


Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
new Thread(f2).start();


// 6、获取线程执行完毕后返回的结果。
// 注意:如果执行到这儿,假如上面的线程还没有执行完毕
// 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
String rs = f1.get();
System.out.println(rs);

String rs2 = f2.get();
System.out.println(rs2);
}
}

FutureTask的API

  • 构造器:public FutureTask<>(Callable call)。把Callable对象封装成FutureTask对象。
  • 方法:public V get() throws Exception。获取线程执行call方法返回的结果。

优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
  • 缺点:编码复杂一点。

3种方法比较

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

Thread的常用方法

常用方法

  1. public void run()。线程的任务方法。
  2. public void start()。线程的任务方法。
  3. public String getName()。获取当前线程的名称,线程名称默认是Thread-索引。
  4. public void setName(String name)。为线程设置名称
  5. public static Thread currentThread()。获取当前执行的线程对象
  6. public static void sleep(long time)。让当前执行的线程休眠多少毫秒后,再继续执行。
  7. public final void join()…。让调用当前这个方法的线程先执行完

构造器

  1. public Thread(String name)。可以为当前线程指定名称。
  2. public Thread(Runnable target)。封装Runnable对象成为线程对象。
  3. public Thread(Runnable target, String name)。封装Runnable对象成为线程对象,并指定线程名称。

其他

Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,用到再说。

线程安全

什么是线程安全问题

  • 多个线程,同时访问同一个共享资源,并存在修改该资源的时候,可能会出现业务安全问题。

取钱的线程安全问题

  • 场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

用程序模拟线程安全问题

先定义一个共享的账户类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Account {
private String cardId; // 卡号
private double money; // 余额。

public Account() {
}

public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}

// 小明 小红同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}

public String getCardId() {
return cardId;
}

public void setCardId(String cardId) {
this.cardId = cardId;
}

public double getMoney() {
return money;
}

public void setMoney(double money) {
this.money = money;
}
}

再定义一个是取钱的线程类

1
2
3
4
5
6
7
8
9
10
11
12
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
// 取钱(小明,小红)
acc.drawMoney(100000);
}
}

最后,再写一个测试类,在测试类中创建两个线程对象

1
2
3
4
5
6
7
8
9
public class ThreadTest {
public static void main(String[] args) {
// 1、创建一个账户对象,代表两个人的共享账户。
Account acc = new Account("ICBC-110", 100000);
// 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。
new DrawThread(acc, "小明").start(); // 小明
new DrawThread(acc, "小红").start(); // 小红
}
}

运行程序,会发现两个人都取了10万块钱,余额为-10万了。

线程同步

认识线程同步

  • 线程同步:解决线程安全问题的方案。
  • 思想:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

常见方案

  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

方式1:同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
1
2
3
4
5
6
7
8
synchronized(同步锁) {
访问共享资源的核心代码
}
//快捷键:ctrl+alt+enter,选择第9个
//同步锁例如可以写“黑马”,因为它在常量中只有一份
//但是有个问题,就是小明来取锁住了自己家的账户,其他家的人也取不了钱了……(锁的对象太大!影响无关线程的执行)
//所以同步锁可以用“this”
//拓展:假设遇到多个线程调用静态方法,官方建议使用“类名.class”作为锁
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
  • 同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

只需要修改DrawThread类中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 小明 小红线程同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
// this正好代表共享资源!
synchronized (this) {
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}
}

【锁对象如何选择的问题】

1
2
3
1.建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象
2.对于实例方法,建议使用this作为锁对象
3.对于静态方法,建议把类的字节码(类名.class)当做锁对象

方式2:同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

同步方法

1
2
3
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}

同步代码块好还是同步方法好?

  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大。(锁的范围越小,性能越好~)
  • 可读性:同步方法更好。(计算机性能上去了,前一个差别不大~)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 同步方法
public synchronized void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}

方式3:Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

构造器

  1. public ReentrantLock()。获得Lock锁的实现类对象。

常用方法

  1. void lock()。获得锁。
  2. void unlock()。释放锁。

格式

1
2
3
4
5
6
1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
private final Lock lk = new ReentrantLock();
2.在需要上锁的地方加入下面的代码
lk.lock(); // 加锁
//...中间是被锁住的代码...
lk.unlock(); // 解锁

使用Lock锁改写前面DrawThread中取钱的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建了一个锁对象
private final Lock lk = new ReentrantLock();

public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
try {
lk.lock(); // 加锁
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock(); // 解锁
}
}
}

记得用try-catch-finally来保证一定会解锁!

线程通信[了解]

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  • 生产者线程负责生产数据。
  • 消费者线程负责消费生产者生产的数据。
  • 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

Object类的等待和唤醒方法:

  1. void wait()。让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法。
  2. void notify()。唤醒正在等待的单个线程。
  3. void notifyAll()。唤醒正在等待的所有线程。

注意

  • 上述方法应该使用当前同步锁对象进行调用。

分析一下完成这个案例的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.先确定在这个案例中,什么是共享数据?
答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。

2.再确定有那几条线程?哪个是生产者,哪个是消费者?
答:厨师是生产者线程,3条生产者线程;
顾客是消费者线程,2条消费者线程

3.什么时候将哪一个线程设置为什么状态
生产者线程(厨师)放包子:
1)先判断是否有包子
2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
3)有包子时,不做包子了,直接唤醒别人、然后让自己等待

消费者线程(顾客)吃包子:
1)先判断是否有包子
2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待
3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待

先写桌子类,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Desk {
private List<String> list = new ArrayList<>();

// 放1个包子的方法
// 厨师1 厨师2 厨师3
public synchronized void put() {
try {
String name = Thread.currentThread().getName();
// 判断是否有包子。
if(list.size() == 0){
list.add(name + "做的肉包子");
System.out.println(name + "做了一个肉包子~~");
Thread.sleep(2000);

// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}else {
// 有包子了,不做了。
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}

// 吃货1 吃货2
public synchronized void get() {
try {
String name = Thread.currentThread().getName();
if(list.size() == 1){
// 有包子,吃了
System.out.println(name + "吃了:" + list.get(0));
list.clear();
Thread.sleep(1000);
this.notifyAll();
this.wait();
}else {
// 没有包子
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ThreadTest {
public static void main(String[] args) {
// 需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
// 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
Desk desk = new Desk();

// 创建3个生产者线程(3个厨师)
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师1").start();

new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师2").start();

new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师3").start();

// 创建2个消费者线程(2个吃货)
new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货1").start();

new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货2").start();
}
}

线程池

认识线程池

线程池就是一个可以复用线程的技术。

不使用线程池的问题

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的, 而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

线程池:正在进行的称为工作线程WorkThread,等待执行的任务在任务队列(WorkQueue)。这些任务对象必须要实现Runnable或者Callable对象。

如何创建线程池?

JDK 5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象?

  1. 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
  2. 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

ThreadPoolExecutor构造器【重要】

1
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 
  1. 参数一:corePoolSize : 指定线程池的核心线程的数量。
  2. 参数二:maximumPoolSize:指定线程池的最大线程数量。(一般要大于corePoolSize,多出来的就是临时线程。)
  3. 参数三:keepAliveTime :指定临时线程的存活时间。
  4. 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)。
  5. 参数五:workQueue:指定线程池的任务队列。
  6. 参数六:threadFactory:指定线程池的线程工厂。(负责创建线程。)
  7. 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)。

理解

image-20240409162613513

用构造器创建线程池对象的代码

1
2
3
4
5
6
7
8
9
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

注意事项

核心线程–>任务队列–>临时线程–>until占满最大线程–>拒绝新任务。

(1)临时线程什么时候创建?

  • 新任务提交时,发现核心线程都在忙任务队列满了、并且还可以创建临时线程,此时会创建临时线程。

(2)什么时候开始拒绝新的任务?

  • 核心线程和临时线程都在忙任务队列也满了新任务过来时才会开始拒绝任务。

程池执行的任务可以有2种:

  1. Runnable任务;
  2. callable任务。

线程池处理Runnable任务

ExecutorService常用方法

  1. void execute(Runnable command)。执行 Runnable 任务。
  2. Future submit(Callable task)。执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果。
  3. void shutdown()。等全部任务执行完毕后,再关闭线程池!(关闭线程池的2种方法:1.点红方块停止,2.调用本方法。注意,执行了含有线程池的程序之后,线程池不停止是正常现象!)
  4. List shutdownNow()。立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务。

新任务拒绝策略(参数七:handler)

  1. ThreadPoolExecutor.AbortPolicy。丢弃任务并抛出。RejectedExecutionException异常。是默认的策略。
  2. ThreadPoolExecutor.DiscardPolicy。丢弃任务,但是不抛出异常,这是不推荐的做法。
  3. ThreadPoolExecutor.DiscardOldestPolicy。抛弃队列中等待最久的任务, 然后把当前任务加入队列中。
  4. ThreadPoolExecutor.CallerRunsPolicy。由主线程负责调用任务的run()方法从而绕过线程池直接执行

代码

先准备一个线程任务类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyRunnable implements Runnable{
@Override
public void run() {
// 任务是干啥的?
System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
//为了模拟线程一直在执行,这里睡久一点
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

Runnable target = new MyRunnable();
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
//下面4个任务在任务队列里排队
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);

//下面2个任务,会被临时线程的创建时机了
pool.execute(target);
pool.execute(target);
// 到了新任务的拒绝时机了!
pool.execute(target);

执行上面的代码,结果输出如下

1668067745116

线程池处理Callable任务

Callable任务与Runnable任务最大的不同:执行完毕后可以返回结果。

上一小节提到的submit()方法。

先准备一个Callable线程任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}

// 2、重写call方法
@Override
public String call() throws Exception {
// 描述线程的任务,返回线程执行返回后的结果。
// 需求:求1-n的和返回。
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
}
}

再准备一个测试类,在测试类中创建线程池,并执行callable任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ThreadPoolTest2 {
public static void main(String[] args) throws Exception {
// 1、通过ThreadPoolExecutor创建一个线程池对象。
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
8,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());

// 2、使用线程处理Callable任务。
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));

// 3、执行完Callable任务后,需要获取返回结果。
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}

执行后,结果如下图所示

1668067964048

说明线程2复用了。

Executors工具类实现线程池

是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

常用方法:

  1. public static ExecutorService newFixedThreadPool(int nThreads)。创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
  2. public static ExecutorService newSingleThreadExecutor()。创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。(不会死掉!)
  3. public static ExecutorService newCachedThreadPool()。线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
  4. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)。创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

【核心线程数量配置多少?】行业通用—>

  • 计算密集型的任务:核心线程数量 = CPU核数 + 1;
  • IO密集型的任务:核心线程数量 = CPU核数 * 2;

【Executors使用可能存在的陷阱】

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。(小的系统也最好不要用,容易留安全隐患。)
  • image-20240409192641654

其他细节知识:并发、并行

进程

  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程属于进程的,一个进程中可以同时运行很多个线程
  • 进程中的多个线程其实是并发和并行执行的。

并发

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限为了保证全部线程都能往前执行CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

并行

  • 在同一个时刻上,同时有多个线程在被CPU调度执行

【多线程到底是怎么执行的?】

并发和并行同时进行的!

  • 并发:CPU分时轮询的执行线程。
  • 并行:同一个时刻同时在执行。

其他细节知识:线程的生命周期

线程的生命周期

  • 也就是线程从生到死的过程中,经历的各种状态及状态转换。
  • 理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态

  • Java总共定义了6种状态。
  • 6种状态都定义在Thread类的内部枚举类中。(Thread.State可以调出来。)
1
2
3
4
5
6
7
8
9
10
11
12
public class Thread{
...
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
...
}
线程状态 说明
NEW(新建) 线程刚被创建,但是并未启动。
Runnable(可运行) 线程已经调用了start(),等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程的6种状态互相转换

image-20240409194225380