当前位置: 首页 > news >正文

java的多线程

文章目录

    • 创建线程
      • 什么是线程?
      • 什么是多线程?
      • 如何在程序中创建出多条线程?
        • 方式一:继承Thread类
        • 方式二:实现Runnable接口
        • 方式三:实现Callable接口
      • 三种创建方式的对比
    • 线程的常用方法
      • Thread提供的常用方法
      • Thread提供的常见构造器
      • 展示前五种方法的用法
      • Sleep方法
      • 线程插队的join方法
    • 线程安全
      • 认识线程安全
      • 模拟线程安全问题
    • 线程同步
      • 认识线程同步
      • 线程同步的常见方案
      • 加锁的三种方式
      • 方式一:同步代码块
      • 方式二:同步方法
      • 方式三:lock锁
    • 线程池
      • 认识线程池
      • 创建线程池
      • 方式一:通过ThreadPoolExecutor创建线程池
        • 七大参数说明
        • 处理Runnable任务
        • 处理Callable任务
      • 方式二通过Executors工具类创建线程池
    • 并发、并行
    • 抢红包 案例

创建线程

什么是线程?

线程(Thread)是一个程序内部的一条执行流程。

程序中如果只有一条执行流程,那这个程序就是单线程的程序。

public static void main(String[] args) {// 代码…for (int i = 0; i < 10; i++) {System.out.println(i);}// 代码...
}

什么是多线程?

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

多线程用在哪里,有什么好处
百度网盘的一边上传一边下载

再例如:消息通信、淘宝、京东系统都离不开多线程技术。

如何在程序中创建出多条线程?

  1. 方式一:继承Thread类
  2. 方式二:实现Runnable接口
  3. 方式三:实现Callable接口
方式一:继承Thread类

多线程的创建方式一:继承Thread类

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

方式一优缺点:

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

创建线程的注意事项

  1. 启动线程必须是调用start方法,不是调用run方法。
    • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
    • 只有调用start方法才是启动一个新的线程执行。
  2. 不要把主线程任务放在启动子线程之前。
    • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
public class ThreadDemo1 {// main方法本身是由一条主线程负责推荐执行的。public static void main(String[] args) {// 目标:认识多线程,掌握创建线程的方式一:继承Thread类来实现// 4、创建线程类的对象:代表线程。Thread t1 = new MyThread();// 5、调用start方法,启动线程。还是调用run方法执行的t1.start(); // 启动线程,让线程执行run方法for (int i = 0; i < 5; i++) {System.out.println("主线程输出:" + i);}//主线程输出:0//子线程输出:0//主线程输出:1//子线程输出:1//主线程输出:2//子线程输出:2//子线程输出:3//子线程输出:4//主线程输出:3//主线程输出:4}
}// 1、定义一个子类继承Thread类,成为一个线程类。
class MyThread extends Thread {// 2、重写Thread类的run方法@Overridepublic void run() {// 3、在run方法中编写线程的任务代码(线程要干的活儿)for (int i = 0; i < 5; i++) {System.out.println("子线程输出:" + i);}}
}
方式二:实现Runnable接口
  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程
Thread类提供的构造器说明
public Thread(Runnable target)封装Runnable对象成为线程对象

方式二的优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  • 缺点:需要多一个Runnable对象。
public class ThreadDemo2 {public static void main(String[] args) {// 目标:掌握多线程的创建方式二:实现Runnable接口来创建。// 3、创建线程任务类的对象代表一个线程任务。Runnable r = new MyRunnable();// 4、把线程任务对象交给一个线程对象来处理Thread t1 = new Thread(r); // public Thread(Runnable r)// 5、启动线程t1.start();for (int i = 0; i < 5; i++) {System.out.println("主线程输出:" + i);}}
}// 1、定义一个线程任务类实现Runnable接口
class MyRunnable implements Runnable {// 2、重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程输出:" + i);}}
}

匿名内部类写法

  • 可以创建Runnable的匿名内部类对象。
  • 再交给Thread线程对象。
  • 再调用线程对象的start()启动线程。
public class ThreadDemo2_2 {public static void main(String[] args) {// 目标:掌握多线程的创建方式二:使用Runnable接口的匿名内部类来创建// 写法1Runnable r = new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程1输出:" + i);}}};Thread t1 = new Thread(r); // public Thread(Runnable r)t1.start();// 写法2new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程2输出:" + i);}}}).start();// 写法3new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("子线程3输出:" + i);}}).start();// 主线程for (int i = 0; i < 5; i++) {System.out.println("主线程输出:" + i);}}
}
方式三:实现Callable接口

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

如何解决这个问题

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

利用Callable接口、FutureTask类创建接口

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

FutureTask的API

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

线程创建方式三的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
  • 缺点:编码复杂一点。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;public class ThreadDemo3 {public static void main(String[] args) {// 目标:掌握多线程的创建方式三:实现Callable接口,方式三的优势:可以获取线程执行完毕后的结果的。// 3、创建一个Callable接口的实现类对象。Callable<String> c1 = new MyCallable(100);// 4、把Callable对象封装成一个真正的线程任务对象FutureTask对象。/*** 未来任务对象的作用?*    a、本质是一个Runnable线程任务对象,可以交给Thread线程对象处理。*    b、可以获取线程执行完毕后的结果。*/FutureTask<String> f1 = new FutureTask<>(c1); // public FutureTask(Callable<V> callable)// 5、把FutureTask对象作为参数传递给Thread线程对象。Thread t1 = new Thread(f1);// 6、启动线程。t1.start();Callable<String> c2 = new MyCallable(50);FutureTask<String> f2 = new FutureTask<>(c2); // public FutureTask(Callable<V> callable)Thread t2 = new Thread(f2);t2.start();// 获取线程执行完毕后返回的结果try {// 如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!System.out.println(f1.get());// 子线程计算1-100的和是:5050} catch (Exception e) {e.printStackTrace();}try {// 如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!System.out.println(f2.get());// 子线程计算1-50的和是:1275} catch (Exception e) {e.printStackTrace();}}
}// 1、定义一个实现类实现Callable接口
class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、实现call方法,定义线程执行体public String call() throws Exception {int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return "子线程计算1-" + n + "的和是:"  + sum;}
}

三种创建方式的对比

对比说一下三种线程的创建方式,和不同点?

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

线程的常用方法

Thread提供的常用方法

Thread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public String getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()…让调用当前这个方法的线程先执行完!

Thread提供的常见构造器

Thread提供的常见构造器说明
public Thread(String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称

展示前五种方法的用法

public class ThreadApiDemo1 {public static void main(String[] args) {// 目标:搞清楚线程的常用方法。Thread t1 = new MyThread("1号线程");// 起名字是为了区分不同线程。当然也可以使用他自带的名称。// t1.setName("1号线程");// 需要在线程启动之前设置名称t1.start();System.out.println(t1.getName()); // 线程默认名称是:Thread-索引Thread t2 = new MyThread("2号线程");// t2.setName("2号线程");t2.start();System.out.println(t2.getName()); // 线程默认名称是:Thread-索引// 哪个线程调用这个代码,这个代码就拿到哪个线程Thread m = Thread.currentThread(); // 主线程m.setName("主线程");System.out.println(m.getName()); // main}
}// 1、定义一个子类继承Thread类,成为一个线程类。
class MyThread extends Thread {public MyThread(String name) {super(name); // public Thread(String name) // 继承父类的构造方法}// 2、重写Thread类的run方法@Overridepublic void run() {// 3、在run方法中编写线程的任务代码(线程要干的活儿)for (int i = 0; i < 5; i++) {// 获取当前线程的名称System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);}}
}

Sleep方法

public class ThreadApiDemo2 {public static void main(String[] args) {// 目标:搞清楚Thread类的Sleep方法(线程休眠)for (int i = 1; i <= 10; i++) {System.out.println(i);try {// 让当前执行的线程进入休眠状态,直到时间到了,才会继续执行。// 应用场景比如百度网盘充会员下载的快。Thread.sleep(1000); // 1000ms = 1s} catch (Exception e) {e.printStackTrace();}}}
}

线程插队的join方法

public class ThreadApiDemo3 {public static void main(String[] args) {// 目标:搞清楚线程的join方法:线程插队:让调用这个方法线程先执行完毕。MyThread2 t1 = new MyThread2();t1.start();for (int i = 1; i <= 5; i++) {System.out.println(Thread.currentThread().getName() +"线程输出:" + i);if(i == 1){try {t1.join(); // 插队 让t1线程先执行完毕,然后继续执行主线程} catch (Exception e) {e.printStackTrace();}}}// 结果如下//Thread-0子线程输出:1//main线程输出:1    // 这里只要是主线程的i==1时。先执行完子线程, 才会继续往下执行主线程。//Thread-0子线程输出:2//Thread-0子线程输出:3//Thread-0子线程输出:4//Thread-0子线程输出:5//main线程输出:2 // 主线程继续往下执行//main线程输出:3//main线程输出:4//main线程输出:5}
}class MyThread2 extends Thread {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);}}
}

Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会后续需要用到的时候再讲解。

线程安全

认识线程安全

什么是线程安全问题?

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

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

在这里插入图片描述
线程安全问题出现的原因?

  1. 存在多个线程在同时执行
  2. 同时访问一个共享资源
  3. 存在修改该共享资源

模拟线程安全问题

需求
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,用程序模拟两人同时取钱10万元。
分析
怎样描述小明和小红的共同账户呢?
设计一个账户类,创建一个账户对象,代表2人的共享账户

怎样模拟两人同时取钱?
设计一个线程类,创建并启动两个线程,在线程的run方法中调用账户的取钱方法

线程安全问题发生的原因是什么?
多个线程,同时访问同一个共享资源,且存在修改该资源。

// 取钱线程类
public class DrawThread extends Thread{private Account acc; // 记住线程对象要处理的账户对象。public DrawThread(String name, Account acc) {super(name);this.acc = acc;}@Overridepublic void run() {// 小明 小红 取钱acc.drawMoney(100000);}
}// 业务方法
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private String cardId; // 卡号private double money; // 余额// 小明和小红都到这里来了取钱public void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}}
}// 主方法
package com.itheima.demo3threadsafe;public class ThreadDemo1 {public static void main(String[] args) {// 目标:模拟线程安全问题。// 1、设计一个账户类:用于创建小明和小红的共同账户对象,存入10万。Account acc = new Account("ICBC-110", 100000);// 2、设计线程类:创建小明和小红两个线程,模拟小明和小红同时去同一个账户取款10万。new DrawThread("小明", acc).start();new DrawThread("小红", acc).start();// 小明取钱成功,吐出了100000.0元成功!// 小红取钱成功,吐出了100000.0元成功!// 小明取钱成功,取钱后,余额剩余0.0元// 小明取钱成功,取钱后,余额剩余-100000.0元}
}

线程同步

认识线程同步

线程同步是线程安全问题的解决方案。
线程同步的核心思想
让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。

在这里插入图片描述

线程同步的常见方案

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
在这里插入图片描述

加锁的三种方式

  1. 同步代码块
  2. 同步方法
  3. lock锁

方式一:同步代码块

同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。

synchronized(同步锁) {   访问共享资源的核心代码  }

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步锁的注意事项
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

// Account类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private String cardId; // 卡号private double money; // 余额// 小明和小红都到这里来了取钱public void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够synchronized ("dlei") {// 这里需要是一个唯一的,dlei字符串是个唯一的地址// 等这个人取完钱才会把这个唯一地址释放// 但是随便一个唯一值就行吗?// 下面会将if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}}}
}// 取钱线程类
public class DrawThread extends Thread{private Account acc; // 记住线程对象要处理的账户对象。public DrawThread(String name, Account acc) {super(name);this.acc = acc;}@Overridepublic void run() {// 小明 小红 取钱acc.drawMoney(100000);}
}// main方法
public class ThreadDemo1 {public static void main(String[] args) {// 目标:线程同步的方式一演示:同步代码块// 1、设计一个账户类:用于创建小明和小红的共同账户对象,存入10万。Account acc = new Account("ICBC-110", 100000);// 2、设计线程类:创建小明和小红两个线程,模拟小明和小红同时去同一个账户取款10万。new DrawThread("小明", acc).start();new DrawThread("小红", acc).start();//小明取钱成功,吐出了100000.0元成功!//小明取钱成功,取钱后,余额剩余0.0元//小红取钱失败,余额不足}
}

锁对象随便选择一个唯一的对象好不好呢?
不好,会影响其他无关线程的执行。

锁对象的使用规范

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象synchronized(this) { 访问共享资源的核心代码 }
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。synchronized(Account.class) { 访问共享资源的核心代码 }
// Account中的drawMoney方法public void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够synchronized (this) {//1.  这里不能所有账户都用这一个锁,不然这个银行都要// 等其中一个账户取完钱另一个才能再去取钱。//2.使用this说明只有当前账号需要加锁。这一家人来取钱和另外一家人取// 钱互不影响。但是这一家人内部取钱(即同一个账号)多个人取钱会上锁。// .....省略}}// main方法
public class ThreadDemo1 {public static void main(String[] args) {// 1、设计一个账户类:用于创建小明和小红的共同账户对象,存入10万。Account acc = new Account("ICBC-110", 100000);new DrawThread("小明", acc).start();new DrawThread("小红", acc).start();  Account acc2 = new Account("ICBC-111", 80000);new DrawThread("小李", acc).start();new DrawThread("小王", acc).start();  }
}

总结
同步代码块是如何实现线程安全的?

  • 对出现问题的核心代码使用synchronized进行加锁
  • 每次只能一个线程占锁进入访问

同步代码块的同步锁对象有什么要求?

  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

方式二:同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全。

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

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

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

// 其他都一样
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private String cardId; // 卡号private double money; // 余额// 小明和小红都到这里来了取钱public synchronized void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();// 判断余额是否足够if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}}
}

同步代码块好还是同步方法好?
+ 范围上:同步代码块锁的范围更小,同步方法锁的范围更大
+ 可读性:同步方法更好

同步方法是如何保证线程安全的?

  • 对出现问题的核心方法使用synchronized修饰
  • 每次只能一个线程占锁进入访问

同步方法的同步锁对象的原理?

  • 对于实例方法默认使用this作为锁对象。
  • 对于静态方法默认使用类名.class对象作为锁对象。

方式三:lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

构造器说明
public ReentrantLock()获得Lock锁的实现类对象

Lock的常用方法

方法名称说明
void lock()获得锁
void unlock()释放锁
// 其他代码不变。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private String cardId; // 卡号private double money; // 余额private final Lock lk = new ReentrantLock(); //lk使用final修饰防止被篡改 保护锁对象// 小明和小红都到这里来了取钱public void drawMoney(double money) {// 拿到当前谁来取钱。String name = Thread.currentThread().getName();lk.lock(); // 上锁try {// 判断余额是否足够if (this.money >= money) {// 余额足够,取钱System.out.println(name + "取钱成功,吐出了" + money + "元成功!");// 更新余额this.money -= money;System.out.println(name + "取钱成功,取钱后,余额剩余" + this.money + "元");} else {// 余额不足System.out.println(name + "取钱失败,余额不足");}} finally {// 放在这里解锁,如果try中报错,也会执行finally中的代码lk.unlock();// 解锁}}
}

锁对象建议加上什么修饰?
建议使用final修饰,防止被别人篡改
释放锁的操作建议放到哪里?
建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放

线程池

认识线程池

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

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

下面使用线程池来解决上述问题。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

创建线程池

JDK 5.0起提供了代表线程池的接口:ExecutorService。
如何创建线程池对象?
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
在这里插入图片描述
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

方式一:通过ThreadPoolExecutor创建线程池

ThreadPoolExecutor类提供的构造器作用
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
使用指定的初始化参数创建一个新的线程池对象
七大参数说明

参数一:corePoolSize : 指定线程池的核心线程的数量。正式工: 3
参数二:maximumPoolSize:指定线程池的最大线程数量。最大员工数: 5, 临时工: 2
参数三:keepAliveTime :指定临时线程的存活时间。临时工空闲多久被开除
参数四:unit:指定临时线程存活(第三个参数)的时间单位(秒、分、时、天)
参数五:workQueue:指定线程池的任务队列。客人排队的地方
参数六:threadFactory:指定线程池的线程工厂。负责招聘员工的(hr)
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)处理忙不过来咋办的问题(比如报错,删除某些线程)

谁代表线程池?线程池的创建方案有几种?
ExecutorService接口
ThreadPoolExecutor实现线程池对象的七个参数是什么意思?
使用线程池的实现类ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
处理Runnable任务
方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future<T> submit(Callable<T> task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List<Runnable> shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
// 1、定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable {// 2、重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + "输出:" + i);}}
}// Main方法
import java.util.concurrent.*;public class ExecutorServiceDemo1 {public static void main(String[] args) {// 目标:创建线程池对象来使用。// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。ExecutorService pool = new ThreadPoolExecutor(3, 5,10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());// 2、使用线程池处理任务!看会不会复用线程?Runnable target = new MyRunnable();pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务pool.execute(target); // 复用线程pool.execute(target); // 复用线程//pool-1-thread-2输出:0 // 线程池1的线程1//pool-1-thread-1输出:0//pool-1-thread-3输出:0//pool-1-thread-1输出:1//pool-1-thread-2输出:1//pool-1-thread-1输出:2//pool-1-thread-3输出:1//pool-1-thread-1输出:3//pool-1-thread-2输出:2//pool-1-thread-1输出:4 //pool-1-thread-3输出:2//pool-1-thread-2输出:3//pool-1-thread-1输出:0//pool-1-thread-3输出:3//pool-1-thread-1输出:1//pool-1-thread-1输出:2//pool-1-thread-2输出:4//pool-1-thread-1输出:3//pool-1-thread-2输出:0//pool-1-thread-3输出:4//pool-1-thread-2输出:1//pool-1-thread-1输出:4//pool-1-thread-2输出:2//pool-1-thread-2输出:3//pool-1-thread-2输出:4// 一共五个4输出,所以线程池复用了2个线程,执行了5个任务。// 3、关闭线程池 :一般不关闭线程池。// pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!//  pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!}
}

线程池的注意事项

什么时候开始创建临时线程?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
    什么时候会拒绝新任务?
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

任务拒绝策略

策略说明
ThreadPoolExecutor.AbortPolicy()丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor. DiscardPolicy()丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor. DiscardOldestPolicy()抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor. CallerRunsPolicy()由主线程负责调用任务的run()方法从而绕过线程池直接执行
package com.itheima.demo7executorService;// 1、定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable {// 2、重写run方法,设置线程任务@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + "输出:" + i);try {// 线程休眠 Integer.MAX_VALUE值很大 // 可以认为永远沉睡Thread.sleep(Integer.MAX_VALUE);} catch (Exception e) {e.printStackTrace();}}}
}// Main
import java.util.concurrent.*;
public class ExecutorServiceDemo1 {public static void main(String[] args) {// 目标:创建线程池对象来使用。// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。ExecutorService pool = new ThreadPoolExecutor(3, 5,10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());// 2、使用线程池处理任务!看会不会复用线程?Runnable target = new MyRunnable();pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务pool.execute(target); // 提交第3个任务 创建第3个线程 自动启动线程处理这个任务pool.execute(target); // 提交第4个任务 第四个排队 new ArrayBlockingQueue<>(3)有三个座位供用户排队pool.execute(target); // 提交第5个任务 第五个排队 new ArrayBlockingQueue<>(3)有三个座位供用户排队pool.execute(target); // 提交第6个任务 第六个排队 此时等待的座位用完了,正式工也用完了。pool.execute(target); // 到了临时线程的创建时机了,招临时工1。pool.execute(target); // 到了临时线程的创建时机了,招临时工2。pool.execute(target); // 到了任务拒绝策略了,忙不过来// 如果使用线程池pool使用  new ThreadPoolExecutor.AbortPolicy(),会抛出异常,任务拒绝了。// 如果使用线程池pool使用  new ThreadPoolExecutor.CallerRunsPolicy(),会执行任务,但是不创建临时线程。// 如果使用线程池pool使用  new ThreadPoolExecutor.DiscardOldestPolicy(),会丢弃最老的任务,再执行新任务。// 如果使用线程池pool使用  new ThreadPoolExecutor.DiscardPolicy(),会丢弃新任务,不执行。// 如果使用线程池pool使用  new ThreadPoolExecutor.CallerRunsPolicy(),主线程main会执行任务,但是不创建临时线程。}
}

线程池如何处理Runnable任务?
使用ExecutorService的方法:
void execute(Runnable target)

处理Callable任务
方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future<T> submit(Callable<T> task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List<Runnable> shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

总结
线程池如何处理Callable任务,并得到任务执行完后返回的结果?
使用ExecutorService的方法:
Future submit(Callable command)

import java.util.concurrent.Callable;
// 1、定义一个实现类实现Callable接口
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、实现call方法,定义线程执行体public String call() throws Exception {int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return Thread.currentThread().getName() +"计算1-" + n + "的和是:"  + sum;}
}
// Main方法
import java.util.concurrent.*;
public class ExecutorServiceDemo2 {public static void main(String[] args) {// 目标:创建线程池对象来使用。// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。ExecutorService pool = new ThreadPoolExecutor(3, 5,10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),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));try {System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());//pool-1-thread-1计算1-100的和是:5050//pool-1-thread-2计算1-200的和是:20100//pool-1-thread-3计算1-300的和是:45150 // 线程池中的线程会自动复用。//pool-1-thread-2计算1-400的和是:80200} catch (Exception e) {e.printStackTrace();}}
}

方式二通过Executors工具类创建线程池

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

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

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

Executors使用可能存在的陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
在这里插入图片描述
OOM内存溢出异常

Executors工具类底层是基于什么方式实现的线程池对象?
线程池ExecutorService的实现类:ThreadPoolExecutor
Executors是否适合做大型互联网场景的线程池方案?
不合适。
建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;public class ExecutorsDemo3 {public static void main(String[] args) {// 目标:通过线程池工具类:Executors,调用其静态方法直接得到线程池ExecutorService pool = Executors.newFixedThreadPool(3);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));try {System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());//pool-1-thread-1计算1-100的和是:5050//pool-1-thread-2计算1-200的和是:20100//pool-1-thread-3计算1-300的和是:45150 // 线程池中的线程3复用。//pool-1-thread-2计算1-400的和是:80200} catch (Exception e) {e.printStackTrace();}}
}
// 1、定义一个实现类实现Callable接口
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、实现call方法,定义线程执行体public String call() throws Exception {int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return Thread.currentThread().getName() +"计算1-" + n + "的和是:"  + sum;}
}

在工作中,对于线程池的配置每个公司都不一样。
Java中的线程池的核心线程数是和最大线程数量的配置公式是什么样的?
在Java中,线程池的核心线程数量(corePoolSize)和最大线程数量(maximumPoolSize)的配置通常基于应用程序的特性来决定,比如它是CPU密集型还是IO密集型

CPU密集型任务
对于CPU密集型任务(计算密集型),线程池的大小通常设置为与系统可用处理器数量相等或稍多一点,以避免过多的上下文切换和减少线程调度的开销。公式如下:

int corePoolsize = Runtime.getRuntime().availableProcessors();
int maximumPoolSize=corePoolsize+1:// 或者等于 corePoolSize,根据具体需求调整

IO密集型任务
对于I0密集型任务(如网络清求、磁盘读写等),线程可能大部分时间都在等待IO操作完成,此时可以设置更多的线程来充分利用CPU等待的时间。公式如下:

  • 核心线程数 =CPU核数 x(1/(1-阻塞系数)。
  • 最大线程数 =核心线程数 x倍数(根据具体需求调整,例如2倍)

并发、并行

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

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

并行的理解
在同一个时刻上,同时有多个线程在被CPU调度执行。
在这里插入图片描述
简单说说多线程是怎么执行的?
并发和并行同时进行的
并发:CPU分时轮询的执行线程。
并行:同一个时刻同时在执行。

抢红包 案例

红包雨游戏,某企业有100名员工,员工的工号依次是1,2,3, 4,…100。现在公司举办了年会活动,活动中有一个红包雨环节,要
求共计发出200个红包雨。其中小红包在[1-30]元之间,总占比为80%,大红包[31-100]元,总占比为20%。
具体的功能点如下
1、系统模拟上述要求产生200个红包。
2、模拟100个员工抢红包雨,需要输出哪个员工抢到哪个红包的过程,活动结束时需要提示活动结束。
3、活动结束后,请对100名员工按照所抢红包的总金额进行降序排序展示,例如:3号员工抢红包总计:293元、1号员工抢红包总计250元。

// PeopleGetRedPacket类
import java.util.List;// 线程类
public class PeopleGetRedPacket extends Thread{private List<Integer> redPacket;public PeopleGetRedPacket(List<Integer> redPacket, String name) {super(name);this.redPacket = redPacket;}@Overridepublic void run() {String name = Thread.currentThread().getName();while (true) {// 100个人来抢redPacket集合中的钱。synchronized (redPacket){if(redPacket.size() == 0){break;}// 随机一个索引得到红包int index = (int)(Math.random() * redPacket.size());Integer money = redPacket.remove(index);System.out.println(name + "抢到了" + money + "元");if(redPacket.size() == 0){System.out.println("活动结束!");break;}}try {Thread.sleep(2);} catch (Exception e) {e.printStackTrace();}}}
}// main
import java.util.ArrayList;
import java.util.List;
import java.util.Random;public class ThreadTest {public static void main(String[] args) {// 目标:完成多线程的综合小案例// 红包雨游戏,某企业有100名员工,员工的工号依次是1, 2,3, 4,..到100。// 现在公司举办了年会活动,活动中有一个红包雨环节,要求共计发出200个红包雨。其中小红包在[1 - 30]  元之间,// 总占比为80%,大红包[31-100]元,总占比为20%。// 分析:100个员工实际上就是100个线程,来竞争200个红包。List<Integer> redPacket = getRedPacket();// 2、定义线程类,创建100个线程,竞争同一个集合。for (int i = 1; i <= 100; i++) {new PeopleGetRedPacket(redPacket, "人" + i).start();}}// 1、准备这200个随机的红包返回。放到List集合中去返回。public static List<Integer> getRedPacket() {Random r = new Random();// 其中小红包在[1 - 30]  元之间,总占比为80%,大红包[31-100]元,总占比为20%。List<Integer> redPacket = new ArrayList<>();for (int i = 1; i <= 160; i++) {redPacket.add(r.nextInt(30) + 1);}for (int i = 1; i <= 40; i++) {redPacket.add(r.nextInt(70) + 31);}return redPacket;}
}

相关文章:

  • [Python]非零基础的快速上手
  • 染色质开放性测序(ATAC-seq)
  • ElasticSearch深入解析(六):集群核心配置
  • MATLAB长方体磁体3D磁力线生成
  • 大数据应用开发和项目实战-matplotlib(二)
  • 区块链密码学核心
  • PDM是什么?PDM有什么用?怎么选PDM?2025制造PDM/PLM系统盘点(4000字)
  • [密码学实战]SDF之非对称运算类函数(三)
  • 软件系统容量管理:反模式剖析与模式应用
  • IF:22.7 InfoMat:一种用于智能假肢的机器学习辅助多功能触觉传感器
  • 全平台开源即时通讯IM框架MobileIMSDK:7端+TCP/UDP/WebSocket协议,鸿蒙NEXT端已发布,5.7K Stars
  • GA-Transformer遗传算法优化编码器多特征分类预测/故障诊断,作者:机器学习之心
  • 百度CarLife实现手机车机无缝互联
  • MicroBlaze软核的开发使用
  • 2025年4月最新Cursor续杯详细步骤
  • 快速安装Arduino IED的STM32 MCU开发包
  • 在网页中使用【LaTeX 数学公式块】的完整步骤总结
  • 云蝠智能大模型智能呼叫:赋能零售行业服务,助力客户增长
  • labview项目文件架构
  • 机器人“跨协议对话”秘籍:EtherNet IP转PROFINET网关应用实录
  • 华夏幸福:去年营业收入237.65亿元,同比减亏12亿元
  • 海量数据处于“原矿”状态,数据价值释放如何破局?
  • 浦发银行一季度净利175.98亿增1.02%,不良率微降
  • 成都警方:在地铁公共区域用改装设备偷拍女乘客,男子被行拘
  • 直播电商行业代表呼吁:携手并肩伸出援手助力外贸企业攻坚克难
  • 央行副行长:研究建立民营中小企业增信制度,破解民营中小企业信用不足等融资制约