2025.04.24

Java 多线程&JUC

一、多线程概述

1.1 多线程的概念

  • 线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,使进程中的实际运作单位
  • 进程:进程是程序的基本执行实体
  • 多线程:多线程是指在同一进程中并发执行多个线程的技术。多线程可以提高程序的执行效率和响应能力
  • 多线程的应用场景
    • 软件中的耗时操作
      • 拷贝、迁移大文件
      • 加载大量的资源文件
    • 所有的聊天软件
    • 所有的后台服务器

1.2 并发和并行

  • 并发:在同一时刻,有多个指令在单个CPU上交替执行
  • 并行:在同一时刻,有多个指令在多个CPU上同时执行

二、多线程的实现方式

2.1 继承Thread类

  • 继承Thread类的方式实现多线程
  • 步骤:
    1. 自己定义一个类继承Thread
    2. 重写run方法
    3. 创建子类的对象,并启动线程

示例:

1
2
3
4
5
6
7
8
9
// MyThread类继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(getName() + "Hello, World!");
}
}
}
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
public class ThreadDemo1 {
public static void main(String[] args) {
/*
1. 自己定义一个类继承Thread
2. 重写run方法
3. 创建子类的对象,并启动线程
*/

MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
// start启动线程
myThread1.setName("线程1");
myThread2.setName("线程2");

myThread1.start();
myThread2.start();
}
}


// 输出:
// 线程1Hello, World!
// 线程1Hello, World!
// 线程2Hello, World!
// 线程2Hello, World!
// 线程2Hello, World!
// 线程1Hello, World!

2.2 实现Runnable接口

  • 实现Runnable接口的方式实现多线程
  • 步骤:
    1. 自己定义一个类实现Runnable接口
    2. 重写run方法
    3. 创建Runnable接口的实现类对象,并将其作为参数传递给Thread类的构造方法
    4. 调用Thread类的start方法启动线程

示例:

1
2
3
4
5
6
7
8
9
// MyRunnable类实现Runnable接口
public class MyRun implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "Hello, World!");
}
}
}
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
public class Main {
public static void main(String[] args) {
/*
1. 自己定义一个类实现Runnable接口
2. 重写run方法
3. 创建Runnable接口的实现类对象,并将其作为参数传递给Thread类的构造方法
4. 调用Thread类的start方法启动线程
*/

MyRun myRun = new MyRun();

Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);

t1.setName("线程1");
t2.setName("线程2");

t1.start();
t2.start();
}
}


// 输出:
// 线程2Hello, World!
// 线程1Hello, World!
// 线程2Hello, World!
// 线程2Hello, World!
// 线程1Hello, World!
// 线程1Hello, World!

2.3 Callable接口和Future接口

  • Callable接口和Future接口的方式实现多线程
  • 特点:
    • Callable接口可以有返回值,而Runnable接口不能有返回值
    • Callable接口可以抛出异常,而Runnable接口不能抛出异常
  • 步骤:
    1. 自己定义一个类MyCallable实现Callable接口
    2. 重写call方法(有返回值,表示多线程运行的结果)
    3. 创建Callable接口的实现类对象(表示多线程要执行的任务)
    4. 创建FutureTask对象(用于管理多线程运行的结果)
    5. 创建Thread对象,并启动线程

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// MyCallable类实现Callable接口
import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
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
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
1. 自己定义一个类MyCallable实现Callable接口
2. 重写call方法(有返回值,表示多线程运行的结果)
3. 创建Callable接口的实现类对象(表示多线程要执行的任务)
4. 创建FutureTask对象(用于管理多线程运行的结果)
5. 创建Thread对象,并启动线程
*/

MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<Integer>(mc);

Thread t1 = new Thread(ft);
t1.start();

Integer result = ft.get();
System.out.println(result);
}
}


// 输出:
// 5050

2.4 三种实现方式对比

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

三、常见的成员方法

  • String getName():返回此线程的名称
  • void setName(String name):设置此线程的名称
  • static Thread currentThread():返回对当前正在执行的线程对象
  • static void sleep(long millis):使当前线程休眠指定的毫秒数
  • setPriority(int newPriority):设置线程的优先级
  • final int getPriority():返回线程的优先级
  • final void setDaemon(boolean on):将线程设置为守护线程
  • public static void yield():出让线程/礼让线程
  • public static void join():插入线程/插队线程

3.1 getName()、setName()、currentThread()和sleep()

  • String getName():返回此线程的名称
    • 如果没有设置名字,默认名字为Thread-X,X为序号,从0开始
  • void setName(String name):设置此线程的名称
    • 线程名称也可以使用构造方法设置
    • 使用构造方法时需要在子类中继承父类的构造方法
  • static Thread currentThread():返回对当前正在执行的线程对象
    • 当JVM虚拟机启动之后会自动启动多条线程,其中有一条线程叫做main线程
    • main线程作用就是调用main方法,并执行里面的代码
  • static void sleep(long millis):使当前线程休眠指定的毫秒数
    • 哪条线程执行到这个方法,就会在这里停留对应的时间
    • 时间到了之后线程会自动醒来,继续执行后面的代码

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MyThread类
public class MyThread extends Thread {

public MyThread() {
}

public MyThread(String name) {
super(name);
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + 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
29
30
31
32
33
34
35
36
37
38
39
40
public class Main {
public static void main(String[] args) throws InterruptedException {

MyThread t1 = new MyThread("线程1");
MyThread t2 = new MyThread("线程2");

t1.start();
t2.start();

// Thread thread = Thread.currentThread();
// System.out.println(thread.getName()); // main

// System.out.println("Hello World");
// Thread.sleep(5000);
// System.out.println("Hello World");
}
}


// 输出:
// Thread-1@0
// Thread-1@1
// Thread-1@2
// Thread-1@3
// Thread-1@4
// Thread-1@5
// Thread-1@6
// Thread-1@7
// Thread-0@0
// Thread-1@8
// Thread-1@9
// Thread-0@1
// Thread-0@2
// Thread-0@3
// Thread-0@4
// Thread-0@5
// Thread-0@6
// Thread-0@7
// Thread-0@8
// Thread-0@9

3.2 setPriority()和getPriority()

  • 抢占式调度:线程的优先级越高,越有可能获得CPU的使用权
  • 非抢占式调度:线程的优先级越高,越有可能获得CPU的使用权,但不一定会获得CPU的使用权
  • 优先级范围:1-10
    • 1:MIN_PRIORITY:最低优先级
    • 5:NORM_PRIORITY:默认优先级
    • 10:MAX_PRIORITY:最高优先级
  • 线程的优先级只是一个概率,优先级高的线程不一定会先执行
  • setPriority(int newPriority):设置线程的优先级
  • final int getPriority():返回线程的优先级

示例:

1
2
3
4
5
6
7
8
9
// MyThread类
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "@" + 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Main {
public static void main(String[] args) {

MyRunnable myRunnable = new MyRunnable();

Thread t1 = new Thread(myRunnable, "线程1");
Thread t2 = new Thread(myRunnable, "线程2");

t1.setPriority(10);
t2.setPriority(1);

System.out.println(t1.getPriority());
System.out.println(t2.getPriority());

t1.start();
t2.start();
}
}


// 输出:
// 10
// 1
// 线程2@0
// 线程2@1
// 线程2@2
// 线程2@3
// 线程1@0
// 线程2@4
// 线程1@1
// 线程1@2
// 线程1@3
// 线程2@5
// 线程2@6
// 线程1@4
// 线程2@7
// 线程1@5
// 线程2@8
// 线程1@6
// 线程2@9
// 线程1@7
// 线程1@8
// 线程1@9

3.3 setDaemon()

  • final void setDaemon(boolean on):将线程设置为守护线程
  • 当其他的非守护线程执行完毕之后,守护线程会陆续结束

示例:

1
2
3
4
5
6
7
8
9
// MyThread1类
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}
1
2
3
4
5
6
7
8
9
// MyThread2类
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + 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
29
30
31
32
33
34
35
36
37
38
public class Main {
public static void main(String[] args) {

MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();

t1.setName("线程1");
t2.setName("线程2");

t2.setDaemon(true);

t1.start();
t2.start();
}
}


// 输出:
// 线程1@0
// 线程1@1
// 线程2@0
// 线程2@1
// 线程1@2
// 线程1@3
// 线程1@4
// 线程1@5
// 线程1@6
// 线程1@7
// 线程1@8
// 线程1@9
// 线程2@2
// 线程2@3
// 线程2@4
// 线程2@5
// 线程2@6
// 线程2@7
// 线程2@8
// 线程2@9

3.4 yield()

  • public static void yield():出让线程/礼让线程
  • 让当前线程出让CPU的使用权,当前线程会进入就绪状态

示例:

1
2
3
4
5
6
7
8
9
10
11
// MyThread类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);

Thread.yield();
}
}
}
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
public class Main {
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@0
// 线程1@0
// 线程2@1
// 线程2@2
// 线程2@3
// 线程1@1
// 线程2@4
// 线程2@5
// 线程2@6
// 线程2@7
// 线程1@2
// 线程2@8
// 线程1@3
// 线程2@9
// 线程1@4
// 线程1@5
// 线程1@6
// 线程1@7
// 线程1@8
// 线程1@9

3.5 join()

  • public static void join():插入线程/插队线程
  • 将当前线程插入到指定线程的后面,等待指定线程执行完毕之后,当前线程才会继续执行

示例:

1
2
3
4
5
6
7
8
9
10
11
// MyThread类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "@" + i);

Thread.yield();
}
}
}
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 Main {
public static void main(String[] args) throws InterruptedException {

MyThread t = new MyThread();
t.setName("线程1");
t.start();

// 表示把t线程插入到当前线程之前
t.join();

for (int i = 0; i < 5; i++) {
System.out.println("main线程" + "@" + i);
}
}
}


// 输出:
// 线程1@0
// 线程1@1
// 线程1@2
// 线程1@3
// 线程1@4
// main线程@0
// main线程@1
// main线程@2
// main线程@3
// main线程@4

四、线程的生命周期和安全问题

4.1 线程的生命周期

  • 线程的生命周期
    • 新建状态:创建线程对象,调用start方法之前
    • 就绪状态:调用start方法之后,等待CPU分配时间片
    • 运行状态:获得CPU的使用权,正在执行run方法
    • 阻塞状态:调用sleep、wait、join等方法,等待其他线程执行完毕
    • 死亡状态:run方法执行完毕,线程结束

线程的生命周期

4.2 线程的安全问题

  • 线程的安全问题:多个线程同时访问同一个资源,可能会导致数据不一致的问题
  • 解决方式:使用同步代码块

4.2.1 同步代码块

  • 同步代码块格式
1
2
3
synchronized (锁对象) {
操作共享数据的代码
}
  • 特点
    • 锁默认打开,有一个线程进去了,锁自动关闭
    • 里面的代码全部执行完毕,线程出来,锁自动打开

示例:

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
// MyThread类
public class MyThread extends Thread {

static int ticket = 0;

@Override
public void run() {
while (true) {
// 同步代码块
synchronized (Mythread.class) {
if (ticket < 20) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

4.2.2 同步方法

  • 同步方法:就是把synchronized关键字放在方法上面
  • 格式:
1
2
3
public synchronized void method() {
// 操作共享数据的代码
}
  • 特点:
    • 同步方法是锁住方法里面的所有代码
    • 锁对象不能自己指定
      • 非静态:this
      • 静态:当前类的字节码文件对象MyThread.class

示例:

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
// MyRunnable类
public class MyRunnable implements Runnable {

int ticket = 0;

@Override
public void run() {
// 1. 循环
// 2. 同步代码块
// 3. 判断共享数据是否到了末尾,如果到了末尾
// 4. 判断共享数据是否到了末尾,如果没有到末尾

while (true) {
if (method()) break;
}
}

private synchronized boolean method() {
if (ticket == 100) {
return true;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖" + ticket + "张票");
}
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {

MyRunnable mr = new MyRunnable();

Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);

t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

2025.04.25

4.3 Lock锁

  • 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁
  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
  • Lock实现提供比synchronized方法和语句可以获得更广泛的锁定操作
  • Lock中提供了获得锁和释放锁的方法
    • void lock():获得锁
    • void unlock():释放锁
  • Lock是接口不能直接实例化,这里采用的是它的实现类ReentrantLock来实例化
  • ReentrantLock的构造方法
    • ReentrantLock():创建一个ReantrantLock实例

示例:

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
// MyThread类
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread {

static int ticket = 0;

static Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
// synchronized (MyThread.class) {
lock.lock();
if (ticket == 10) {
break;
}else {
try {
Thread.sleep(10);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}

ticket++;
System.out.println(getName() + "在卖第" + ticket + "张票");
}
// }
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();

t1.start();
t2.start();
t3.start();
}
}

4.4 死锁

  • 死锁:两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的现象
  • 死锁的产生条件
    • 互斥条件:一个资源只能被一个线程使用
    • 请求与保持条件:一个线程已经持有了一个资源,但又请求其他资源
    • 不剥夺条件:已经获得资源的线程在未使用完之前,不能被其他线程剥夺
    • 循环等待条件:多个线程之间形成一种头尾相接的循环等待关系

4.5 生产者和消费者(等待唤醒机制)

  • 生产者和消费者问题
    • 生产者线程负责生产数据
    • 消费者线程负责消费数据
  • 生产者和消费者常见方法
    • void wait():当前线程等待,直到被其他线程唤醒
    • void notify():随机唤醒在此对象监视器上等待的单个线程
    • void notifyAll():唤醒在此对象监视器上等待的所有线程

生产者实现:

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
public class Cook extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
}else {
// 判断桌子上是否有面
// 有的话等吃货吃
if (Desk.flag == 1) {
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
// 没有的话就做,做好了唤醒吃货吃面
System.out.println("厨师做了一碗面");
Desk.flag = 1;
Desk.lock.notify();
}
}
}
}
}
}

消费者实现:

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
public class Foodie extends Thread {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
// 首先判断还能不能吃面条了,不能吃则直接break
if (Desk.count == 0){
break;
}else {
// 能吃则判断桌子上还有没有面条了
// 如果没有则等待
if (Desk.flag == 0) {
try {
Desk.lock.wait(); // 让当前线程和锁绑定在一起
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
// 如果有则吃,且吃完后唤醒厨师继续做
Desk.count--;
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗");
// 吃完后设置flag为0,并唤醒厨师
Desk.flag = 0;
Desk.lock.notify();
}
}
}
}
}
}

桌子实现:

1
2
3
4
5
6
7
8
9
10
11
12
public class Desk {

// 用于查看桌子上是否有面条
// 0:没有 1:有
public static int flag = 0;

// 用于控制总个数
public static int count = 5;

// 锁对象
public static Object lock = new Object();
}

Main函数

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
public class Main {
public static void main(String[] args) {

Cook cook = new Cook();
Foodie foodie = new Foodie();

cook.setName("厨师");
foodie.setName("吃货");

cook.start();
foodie.start();
}
}


// 输出:
// 厨师做了一碗面
// 吃货在吃面条,还能再吃4碗
// 厨师做了一碗面
// 吃货在吃面条,还能再吃3碗
// 厨师做了一碗面
// 吃货在吃面条,还能再吃2碗
// 厨师做了一碗面
// 吃货在吃面条,还能再吃1碗
// 厨师做了一碗面
// 吃货在吃面条,还能再吃0碗

4.6 等待唤醒机制(阻塞队列方式实现)

  • 阻塞队列:是一个线程安全的队列,支持阻塞操作

  • 阻塞队列的继承结构

    • 接口
      • Iterable
      • Collection
      • Queue
      • BlockingQueue
    • 实现类
      • ArrayBlockingQueue
      • LinkedBlockingQueue

生产者实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

消费者实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

Main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.ArrayBlockingQueue;

public class Main {
public static void main(String[] args) {


ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);

c.setName("厨师");
f.setName("吃货");

c.start();
f.start();
}
}

4.7 线程的状态

  • 线程的七种状态
    • 新建状态(NEW):创建线程对象,调用start方法之前
    • 就绪状态(RUNNABLE):调用start方法之后,等待CPU分配时间片
    • 运行状态:获得CPU的使用权,正在执行run方法
    • 阻塞状态(BLOCKED):调用sleep、wait、join等方法,等待其他线程执行完毕
    • 死亡状态(TERMINATED):run方法执行完毕,线程结束
    • 等待状态(WAITING):调用wait方法,等待其他线程唤醒
    • 计时等待状态(TIMED_WAITING):调用sleep方法,等待指定时间后自动唤醒
  • 实际上Java内部并没有定义运行状态

五、练习

5.1 练习1:抢红包

  • 需求:假设100块分成了3个红包,现在有5个人去抢
  • 其中红包是共享数据
  • 5个人是5条线程
  • 打印结果如下:
    • XXX抢到了XXX元
    • XXX抢到了XXX元
    • XXX抢到了XXX元
    • XXX没抢到
    • XXX没抢到
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
// MyThread类
import java.util.Random;

public class MyThread extends Thread {

static int num = 3;
static double money = 100;
static final double MIN = 0.01;

@Override
public void run() {
synchronized (MyThread.class) {
if (num == 0) {
System.out.println(getName() + "没抢到");
}else {
double price = 0;
if (num == 1) {
price = money;
}else {
Random random = new Random();
double v = money - (num - 1) * MIN;
price = random.nextDouble(v);
if (price < MIN) {
price = MIN;
}
}

money = money - price;
num--;
System.out.println(getName() + "抢到了" + price + "元");
}
}
}
}
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
public class Main {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
MyThread t5 = new MyThread();

t1.setName("张三");
t2.setName("李四");
t3.setName("王五");
t4.setName("赵六");
t5.setName("孙七");

t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}


// 输出:
// 张三抢到了83.19171676112362元
// 孙七抢到了0.901172141810879元
// 赵六抢到了15.907111097065506元
// 王五没抢到
// 李四没抢到

2025.04.26

5.2 练习2:抽奖箱抽奖

  • 需求:假设有一个抽奖箱,该抽奖箱中存放了奖励的金额,该抽奖箱内的奖项为{10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700};
  • 创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”和“抽奖箱2”;
  • 随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
    • 每次抽出一个奖项就打印一个(随机)
    • 抽奖箱1又产生了一个10元大奖
    • 抽奖箱1又产生了一个100元大奖
    • 抽奖箱1又产生了一个200元大奖
    • 抽奖箱1又产生了一个800元大奖
    • 抽奖箱1又产生了一个700元大奖
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
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;

public class MyThread extends Thread {

ArrayList<Integer> list;

public MyThread(ArrayList<Integer> list) {
this.list = list;
}

@Override
public void run() {
while (true) {
synchronized (MyThread.class) {
if (list.size() == 0) {
break;
}else {
// Random r = new Random();
// int n = r.nextInt(list.size());
// int prize = list.get(n);
// list.remove(n);
Collections.shuffle(list);
int prize = list.remove(0);
System.out.println(getName() + "又产生了一个" + prize + "元大奖");
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
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
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;

public class Main {
public static void main(String[] args) {

ArrayList<Integer> box = new ArrayList<>();
Collections.addAll(box, 10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700);

MyThread t1 = new MyThread(box);
MyThread t2 = new MyThread(box);

t1.setName("抽奖箱1");
t2.setName("抽奖箱2");

t1.start();
t2.start();
}
}


// 输出:
// 抽奖箱2又产生了一个80元大奖
// 抽奖箱1又产生了一个50元大奖
// 抽奖箱2又产生了一个100元大奖
// 抽奖箱1又产生了一个500元大奖
// 抽奖箱2又产生了一个5元大奖
// 抽奖箱1又产生了一个700元大奖
// 抽奖箱2又产生了一个200元大奖
// 抽奖箱1又产生了一个2元大奖
// 抽奖箱2又产生了一个800元大奖
// 抽奖箱1又产生了一个10元大奖
// 抽奖箱2又产生了一个20元大奖
// 抽奖箱1又产生了一个300元大奖

六、线程池

6.1 线程池的概念

  • 以前写多线程的弊端
    • 用到线程的时候就创建
    • 用完之后线程消失
  • 线程池
    • 用到线程的时候就创建
    • 用完之后线程不会消失,线程会被放入线程池中
    • 下次用到线程的时候直接从线程池中获取即可
    • 如果线程池中的线程不够用,则创建新的线程放入线程池中
  • 线程池的主要核心原理
    1. 创建一个池子,池子中是空的
    2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
    3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

6.2 线程池的实现

  • 线程池的实现类:Executors
    • Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
    • public static ExecutorService newFixedThreadPool():创建一个没有上限的线程池
    • public static ExecutorService newCachedThreadPool(int nThreads):创建一个有上限的线程池

示例:

1
2
3
4
5
6
7
8
9
10
// MyRunnable类
public class MyRunnable implements Runnable {

@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "---" + 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
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
public static void main(String[] args) throws InterruptedException {

ExecutorService pool = Executors.newCachedThreadPool();

pool.submit(new MyRunnable());
Thread.sleep(1000);
pool.submit(new MyRunnable());

pool.shutdown();
}
}


// 输出:
// pool-1-thread-1---0
// pool-1-thread-1---1
// pool-1-thread-1---2
// pool-1-thread-1---3
// pool-1-thread-1---4
// pool-1-thread-1---0
// pool-1-thread-1---1
// pool-1-thread-1---2
// pool-1-thread-1---3
// pool-1-thread-1---4

6.3 自定义线程池

  • 自定义线程池的实现类:ThreadPoolExecutor
    • ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
    • corePoolSize:核心线程数,线程池中最小的线程数
    • maximumPoolSize:最大线程数,线程池中最大的线程数
    • keepAliveTime:线程存活时间,超过这个时间没有任务执行,则线程会被销毁
    • unit:时间单位
    • workQueue:任务队列的实现类
      • ArrayBlockingQueue:有界阻塞队列,线程池中的线程数超过corePoolSize时,新的任务会被放入这个队列中
      • LinkedBlockingQueue:无界阻塞队列,线程池中的线程数超过corePoolSize时,新的任务会被放入这个队列中,直到队列满了才会创建新的线程
      • SynchronousQueue:同步阻塞队列,线程池中的线程数超过corePoolSize时,新的任务会被放入这个队列中,直到队列满了才会创建新的线程
    • threadFactory:线程工厂,创建线程的工厂类
    • handler:拒绝策略,当线程池中的线程数超过maximumPoolSize时,新的任务会被拒绝执行,抛出异常或者放入一个队列中等待执行
      • ThreadPoolExecutor.AbortPolicy:默认策略,丢弃任务并抛出RejectedExecutionException异常
      • ThreadPoolExecutor.DiscardPolicy:丢弃任务,但不抛出异常
      • ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中
      • ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Main {
public static void main(String[] args) {

ThreadPoolExecutor pool = new ThreadPoolExecutor(
3, // 核心线程数量,不能小于0
6, // 最大线程数量,不能小于0,最大数量>=核心线程数量
60, // 空闲线程最大存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(3), // 任务队列
Executors.defaultThreadFactory(), // 创建线程工厂
new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略,内部类
);
}
}

6.4 最大并行数

  • 线程池应该多大
    • CPU密集型:最大并行数 + 1
    • IO密集型:最大并行数 * 期望CPU利用率 * 总时间(CPU计算时间 + 等待时间) / CPU计算时间
    • 示例4核8线程:
      • CPU密集型:8 + 1 = 9
      • IO密集型:8 * 100% * 100% / 50% = 16

示例:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
// 向java虚拟机返回可用的处理器的数目
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);
}
}


// 输出:16

参考资料:
[1] 黑马程序员Java零基础视频教程_下部