多线程
1.为什么要学习多线程
生活:流水线打工
public static void main(String[] args) {
// 代码… for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// 代码...
}
多线程:多(个) 线程
1.1 进程和线程
线程:是进程中的单个顺序控制流,是一条执行路径。
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
进程:是正在运行的软件
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
1.2 并发和并行
那到底是什么是并发
并发:在同一时刻,有多个指令在单个CPU上交替执行。
**解释:**相信大家看到这张图就可以明白什么是并发了。三个菜,但是现在厨房里面只有一个厨师,那么这名厨师就需要先炒会第一个菜,再炒会第二个菜,再炒会第三个菜,再炒会第二个菜。由于,这个厨师在三个菜之间切换的速度比较快,那么在外人客户眼中看来,就是也是同时执行的。但是在某一个瞬间,他只在做其中一件事情。
并行:在同一时刻,有多个指令在多个CPU上同时执行。
**解释:**现在饭馆需要炒三个菜:分别是西红柿炒番茄,青椒肉丝,海参炒饭。现在有三个厨师同时炒菜,那么这就是并行,重点:事情同时进行。
1.3 简单了解多线程
是指从软件或者硬件上实现多个线程并发执行的技术。(并发)
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。(并行)
1.4 小结
并发和并行
并行:在同一时刻,有多个指令在多个CPU上同时执行。
并发:在同一时刻,有多个指令在单个CPU上交替执行。
进程和线程
进程:就是操作系统中正在运行的一个应用程序。
线程:就是应用程序中做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾。
而我们今天要学习的,就是多个线程的并发情况。
你觉得他有什么好处呢?如果没有多线程技术,那么我们电脑中的程序同时只能运行一个。
比如记事本跟打字软件就不能一起执行,打开记事本的时候就不能打字。玩游戏的时候就不能喷人。
而有了多线程,就能实现刚刚的需求。CPU在记事本软件和打字软件之间做高速的切换,那么在我们眼中这两款软件就是同时执行的了。
补充了解:
CPU线程数指的是CPU能够同时处理的线程数量。它的作用是决定CPU的计算能力和运行效率。CPU的线程数越多,就能并行地执行更多的指令,从而提高计算速度和系统响应速度。因此,在需要大量计算和数据处理的场景下,CPU线程数多的计算机具有更优越的性能表现。
每个 CPU 内核可以有两个线程。因此,具有两个内核的处理器将具有四个线程。具有 16 个内核的处理器将有32个线程。具有 24 个内核的处理器将有 48 个线程。
线程对计算机的功能很重要,因为它们决定了计算机在任何给定时间可以执行的任务数。
1.5 了解java中的多线程和cpu的线程区别
(1) 线程是CPU级别的,一个cpu核心支持一个线程
(2)Java多线程并不是由于cpu线程数为多个才称为多线程,当Java线程数大于cpu线程数,操作系统使用时间片机制,采用线程调度算法,频繁的进行线程切换。
(3) 线程是操作系统最小的调度单位,进程是资源(比如:内存)分配的最小单位
(4)Java中的所有线程在JVM进程中,CPU调度的是进程中的线程
2.多线程的实现方式
2.1 多线程的实现方案
继承Thread类的方式进行实现
实现Runnable接口的方式进行实现
利用Callable和Future接口方式实现
2.2 方式1:继承Thread类
步骤:
定义一个类MyThread继承Thread类
在MyThread类中重写run()方法
创建MyThread类的对象
启动线程
代码展示:
/**
目标:多线程的创建方式一:继承Thread类实现。
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程(执行的还是run方法)
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程类继承Thread类
*/
class MyThread extends Thread{
/**
2、重写run方法,里面是定义线程以后要干啥
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
两个小问题:
1.为什么要重写run()方法?
因为run()是用来封装被线程执行的代码
2.run()方法和start()方法的区别?
**run():**封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。
**start():**启动线程;然后由JVM调用此线程的run()方法
创建两个线程,那么Java中就有main线程和自己创建出来的两条线程,共计三条线程。
2.3 方式2:实现Runnable接口
步骤:
1.定义一个类MyRunnable实现Runnable接口
2.在MyRunnable类中重写run()方法
3.创建MyRunnable类的对象
4.创建Thread类的对象,把MyRunnable对象作为构造方法的参数
5.启动线程
代码展示:
/**
目标:学会线程的创建方式二,理解它的优缺点。
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// Thread t = new Thread(target, "1号");
// 5、启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程任务类 实现Runnable接口
*/
class MyRunnable implements Runnable {
/**
2、重写run方法,定义线程的执行任务的
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
简化写法
public static void main(String[] args) {
//这种模式先 会进行线程的抢占
new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行" + i);
}
}).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行" + i);
}
}
2.4 方式3:Callable和Future
步骤:
1.定义一个类MyCallable实现Callable接口
2.在MyCallable类中重写call()方法
3.创建MyCallable类的对象
4.创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
5.创建Thread类的对象,把FutureTask对象作为构造方法的参数
6.启动线程
7.再调用get方法,就可以获取线程结束之后的结果。
代码演示:
MyCallable
/**
1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
*/
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
/**
2、重写call方法(任务方法)
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
Demo
/**
目标:学会线程的创建方式三:实现Callable接口,结合FutureTask完成。
FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable(100);
// 4、把Callable任务对象 交给 FutureTask 对象
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
t1.setName("一号线程");
// 6、启动线程
t1.start();
System.out.println("线程名字为:" + t1.getName()+","+"线程编号为:"+t1.getId());
Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.setName("二号线程");
t2.start();
System.out.println("线程名字为:" + t2.getName() + "," + "线程编号为:" + t2.getId());
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
//调用get方法,就可以获取线程结束之后的结果
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.5 三种方式的对比
优点 | 缺点 | |
---|---|---|
实现Runnable、Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。 | 编程相对复杂,不能直接使用Thread类中的方法 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
3.线程类的常见方法
3.1 方法
方法 | 描述 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 将此线程的名称更改 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒。 |
public final void setPriority(int newPriority) | 设置线程的优先级 |
public final int getPriority() | 获取线程的优先级 |
线程优先级有10个等级,分别用整数1-10表示。其中1位最低优先级,10为最高优先级,5为默认值。
线程优先级高,只是权重高,获得CPU调度的概率高,并不是一定排前面
代码展示
MyThread
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"输出:" + i);
}
}
}
ThreadDemo
public static void main(String[] args) {
//main方法是由主线程负责调用的
Thread t1 = new MyThread("一号");
// t1.setName("一号");
System.out.println("一号线程的优先级为:" + t1.getPriority());
t1.start();
System.out.println(t1.getName());
Thread t2 = new MyThread("二号");
System.out.println("二号线程的优先级为:" + t2.getPriority());
// t1.setName("二号");
t2.start();
System.out.println(t2.getName());
Thread t3 = new MyThread("三号");
System.out.println("三号线程的优先级为:" + t3.getPriority());
t3.start();
System.out.println(t2.getName());
// 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
// 主线程是main 默认下称优先级为5
Thread thread = Thread.currentThread();//当前线程 为 main
System.out.println("当前线程的优先级为:" + Thread.currentThread().getPriority());
System.out.println(thread.getName());
thread.setName("最牛线程");
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName()+"输出:" + i);
}
}
public class ThreadDemo02 {
// main方法是由主线程负责调度的
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 5; i++) {
System.out.println("输出:" + i);
if(i == 3){
// 让当前线程进入休眠状态
// 段子:项目经理让我加上这行代码,如果用户愿意交钱,我就注释掉。
Thread.sleep(3000);
}
}
}
}
3.2 线程生命周期问题
一个宫女的故事:宫女出生了------新建
宫女参加选秀成为才女-------------就绪
宫女获得皇帝侍奉权---------------运行
宫女侍奉完成回寝宫等待-----------就绪
宫女侍奉中被打断------------------阻塞----就绪
宫女完成侍奉任务----------------死亡
就绪: 有资格抢cput执行权,但是没有执行代码的权利。(在这个过程中不停地抢cpu)
运行:在运行状态 有资格并且可以执行代码,但是有可能被其他线程抢走cpu的执行权 回退到就绪状态。
**阻塞:**遇到sleep进入阻塞状态。
问题:sleep方法会让线程睡眠,睡眠时间到了,立马就会执行下面的代码吗?
4.线程的安全问题
4.1 案例:卖票
需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票。
思路
1.定义一个类Ticket实现Runnable接口,里面定义一个成员变量:
private int ticketCount = 100;
2.在Ticket类中重写run()方法实现卖票,代码步骤如下:
1.判断票数大于0,就卖票,并告知是哪个窗口卖的
2.票数要减1
3.卖光之后,线程停止
3.定义一个测试类TicketDemo,里面有main方法,代码步骤如下:
A:创建Ticket类的对象
B:创建三个Thread类的对象,把Ticket对象作为构造方法的参数,并给出对应的窗口名称
C:启动线程
代码演示
public class Ticket implements Runnable {
//定义初始票数
private int ticketCount = 100;
@Override
public void run() {
while (true) {
if (ticketCount > 0) {
ticketCount--;
System.out.println(Thread.currentThread().getName() + "抢到了票!!" + "还有" + ticketCount + "张票");
}else {
System.out.println("一张票也没有了!!!");
break;
}
}
}
}
public class TicketDemo {
public static void main(String[] args) {
Ticket tk1 = new Ticket();
Thread t1 = new Thread(tk1);
Thread t2 = new Thread(tk1);
Thread t3 = new Thread(tk1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
4.2卖票案例的思考
刚才讲解了电影院卖票程序,好像没有什么问题。但是在实际生活中,售票时出票也是需要时间的,所以,在出售一张票的时候,需要一点时间的延迟,接下来我们去修改卖票程序中卖票的动作:
每次出票时间100毫秒,用sleep()方法实现【正常开发逻辑进入if判断之前和if判断里面都会有逻辑代码】
卖票出现了问题
相同的票出现了多次
问题原因:
线程执行的随机性导致的
代码演示:
public class Ticket implements Runnable {
//定义初始票数
private int ticketCount = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
if (ticketCount > 0) {
ticketCount--;
System.out.println(Thread.currentThread().getName() + "抢到了票!!" + "还有" + ticketCount + "张票");
}else {
System.out.println("一张票也没有了!!!");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.3 卖票案例数据安全问题的解决
为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
多线程操作共享数据
如何解决多线程安全问题呢?
基本思想:让程序没有安全问题的环境
怎么实现呢?
1.把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
2.Java提供了同步代码块的方式来解决
4.4 同步代码块
在上述案例中我们用到了synchronized,那synchronized到底是什么呢?
锁多条语句操作共享数据,可以使用同步代码块实现
格式:
synchronized(任意对象) { 多条语句操作共享数据的代码 }
默认情况:是打开的,只要有一个线程进去执行代码了,锁就会关闭
当线程执行完出来了,锁才会自动打开
synchronized(this)锁的是调用该代码块的当前对象(不能保证线程安全)
synchronized(xxx.class)是对当前类上锁,不管是不是同一线程,只要是该类的对象,就会被上锁同步的
同步的好处和弊端
**好处:**解决了多线程的数据安全问题
**弊端:**当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
代码展示:
public class Ticket implements Runnable {
//定义初始票数
private int ticketCount = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
synchronized (Ticket.class){
if (ticketCount > 0) {
ticketCount--;
System.out.println(Thread.currentThread().getName() + "抢到了票!!" + "还有" + ticketCount + "张票");
}else {
System.out.println("一张票也没有了!!!");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.5 同步方法
同步方法:就是把synchronized关键字加到方法上
格式: [把休眠时间放到锁之前]
修饰符 synchronized 返回值类型 方法名(方法参数) { }
同步代码块和同步方法的区别:
同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
同步代码块可以指定锁对象,同步方法不能指定锁对象
同步方法的锁对象是什么呢?
this
同步静态方法:就是把synchronized关键字加到静态方法上
格式:
修饰符 static synchronized 返回值类型 方法名(方法参数) { }
同步静态方法的锁对象是什么呢?
类名.class
快捷方式 选中代码 ctrl+alt+m
用同步方法实现代码演示:
public class Ticket implements Runnable {
//定义初始票数
private int ticketCount = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
if (extracted()) break;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized boolean extracted() {
if (ticketCount > 0) {
ticketCount--;
System.out.println(Thread.currentThread().getName() + "抢到了票!!" + "还有" + ticketCount + "张票");
} else {
System.out.println("一张票也没有了!!!");
return true;
}
return false;
}
}
4.6 Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁【放到finally里面释放锁】
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
代码演示:
public class Ticket implements Runnable {
//定义初始票数
private int ticketCount = 100;
//定义锁
private final static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//4.解决单窗口卖票问题 win11处理机制的问题
try {
Thread.sleep(10);
//在休眠后锁住
lock.lock();
if (ticketCount > 0) {
ticketCount--;
System.out.println(Thread.currentThread().getName() + "抢到了票!!" + "还有" + ticketCount + "张票");
} else {
System.out.println("一张票也没有了!!!");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
}
4.7 按照获取的锁分类
获取对象锁
synchronized(this|object) {}
修饰非静态方法
获取类锁
synchronized(类.class) {}
修饰静态方法
对象锁与类锁的区别
对象锁:每个实例都会有一个monitor对象,即Java对象的锁,类的对象可以有多个,所以每个对象有其独立的
对象锁,互不干扰
类锁:每个类只有一个Class对象,所以每个类只有一个类锁;类锁是加载类上的,而类信息是存在JVM方法区的,并且整个JVM只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的
5.死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
代码演示:
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
new Thread(() -> {
while (true){
synchronized (o1){
synchronized (o2){
System.out.println("一锁二");
}
}
}
}).start();
new Thread(() -> {
while (true){
synchronized (o2){
synchronized (o1){
System.out.println("二锁一");
}
}
}
}).start();
}
6.生产者消费者
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。就是一个典型的等待唤醒机制。
生产者 - 消费者模型( Producer-consumer problem) 是一个非常经典的多线程并发协作的模型,在分布式系统里非常常见
6.1步骤
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(没有到末尾)
* 4.判断共享数据是否到了末尾(到了末尾)
消费者步骤:
1.判断桌子上是否有汉堡包。
2.判断桌子上是否有汉堡
3.如果没有就等待。
4.如果有就开吃
5.吃完 总数-1
6.吃完之后 唤醒老八继续做汉
7.修改桌子的状态
生产者步骤:
1.判断桌子上是否有汉堡包
2.如果有就等待
3.如果没有 则制作
4.修改桌子上食物的状态
5.唤醒消费者开吃
6.2 等待和唤醒的方法
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中
Object类的等待和唤醒方法:
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
代码实现:
Desk
/**
* 作用:控制生产者和消费者的执行
*/
public class Desk {
//定义一个标记
//0 没有 1 有
public static int flag = 0;
//定义汉堡总数
public static int hamburger = 10;
//创建锁对象
用于锁住一个普通类对象
public static Object lock = new Object();
//使用类锁 线程必须一个一个的执行
//类锁会锁住一个类的所有创造出来的所有实例对象,无论这些线程持有的是否是同一个对象
}
People
/**
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(没有到末尾)
* 4.判断共享数据是否到了末尾(到了末尾)
*/
public class People implements Runnable {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
//判断桌子上是否有汉堡
if (Desk.hamburger == 0) {
break;
} else {
if (Desk.flag == 0) {
//如果没有 就等待
try {
Desk.lock.wait();//让当前线程和锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//吃完 总数-1
Desk.hamburger--;
//如果有 就开吃
System.out.println("我们正在吃老八做的小汉堡,还能吃" + Desk.hamburger + "个");
//吃完之后 唤醒老八继续做汉堡
Desk.lock.notifyAll();
//修改桌子的状态
Desk.flag = 0;
}
}
}
}
}
}
OldEight
/**
* 1.循环
* 2.同步代码块
* 3.判断共享数据是否到了末尾(没有到末尾)
* 4.判断共享数据是否到了末尾(到了末尾)
*/
public class OldEight implements Runnable {
@Override
public void run() {
while (true) {
synchronized (Desk.lock) {
if (Desk.hamburger == 0){
break;
}else {
//判断桌子上是否有汉堡
if (Desk.flag == 1) {
//如果有 则等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//如果没有 则制作
System.out.println("老八正在加急制作小汉堡");
//修改桌子上食物的状态
Desk.flag = 1;
//唤醒消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
Test
public class Test {
public static void main(String[] args) {
People people = new People();
OldEight oldEight = new OldEight();
Thread thread = new Thread(people);
thread.setName("小敏");
thread.start();
Thread thread1 = new Thread(oldEight);
thread.setName("老八");
thread1.start();
}
}
7.线程的状态
小结
虚拟机中线程的六种状态:
新建状态( NEW )---------------创建线程对象
就绪状态( RUNNABLE )--------start方法
阻塞状态( BLOCKED )-----------无法获得锁对象
等待状态( WAITING )-----------wait方法
计时等待( TIMED_WAITING )—sleep方法
结束状态( TERMINATED )------全部代码运行完毕
运行不属于线程中的状态 这是 当线程抢夺到cpu的执行权的时候 交给操作系统进行管理。java就不管啦!
8.线程池
8.1 线程池主要核心原理
1.创建一个池子,池子中是空的
2.有任务需要执行时,创建线程对象 。
任务执行完毕,线程对象归还给池子 。
下次再次提交任务,不需要创建新的线程,直接复用已有线程。
3.所有的任务全部执行完毕,关闭线程池。
8.2 线程池创建过程
1.创建Executors中的静态方法
方法名称 | 说明 |
---|---|
public static ExecutorService newcachedThreadPool() | 创建一个没有上限的线程池 (也是有最大上限int类型最大值 通常到不了电脑就崩溃了) |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建有上限的线程池 |
2.提交任务 submit方法
3.销毁线程池(通常不需要)shutdown方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future submit(Callable task) | 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() | 等任务执行完毕后关闭线程池 |
代码展示:
MyRunnable
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"--------"+i);
}
}
}
MyCallable
public class CallableTest implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"------"+i);
}
return null;
}
}
ThreadPoolDemo
public class ThreadPoolDemo {
public static void main(String[] args) {
//创建线程池
ExecutorService pool = Executors.newCachedThreadPool();
//执行任务
//每次提交一个任务 开始一个线程 1-1
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
// ExecutorService pool = Executors.newCachedThreadPool();
// pool.submit(new CallableTest());
// pool.submit(new CallableTest());
// pool.submit(new CallableTest());
//关闭线程池
pool.shutdown();
}
}
如果指定了线程池的数量 则可以用指定线程池数量的方法
public static void main(String[] args) throws InterruptedException {
//创建线程池
// ExecutorService pool = Executors.newCachedThreadPool();
ExecutorService pool = Executors.newFixedThreadPool(3);
//执行任务
//每次提交一个任务 开始一个线程 1-1
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());//最多只能由三个线程。
//关闭线程池
pool.shutdown();
}
8.3 自定义线程池
8.3.1案例引入创建自定义线程池
创建一个核心线程数2,最大线程数5,临时线程空闲2秒,队列10,默认创建线程,拒绝策略抛出异常的线程池。
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
2,//临时线程空闲时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(10),//队列
Executors.defaultThreadFactory(),//创建线程方式
new ThreadPoolExecutor.AbortPolicy());// 线程拒绝策略
// 获取线程
pool.submit(new MyRunnable());
// 关闭线程池
pool.shutdown();
8.3.2 线程池细节
排队问题?
招募临时线程问题?
处理上线问题?
8.3.3 故事中的核心元素
8.3.4 线程池的创建
ThreadPoolExecutor构造器的参数说明
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler)
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
参数一:指定线程池的线程数量(核心线程): corePoolSize -------------------不能小于0
参数二:指定线程池可支持的最大线程数: maximumPoolSize-------------------最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime-------------------不能小于0
参数四:指定存活时间的单位(秒、分、时、天): unit-------------------时间单位
参数五:指定任务队列: workQueue -------------------不能为null
参数六:指定用哪个线程工厂创建线程: threadFactory -------------------不能为null
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler -------------------不能为null
代码展示:
public class ThreadPoolDemo3 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,
3,TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//正常2个线程处理
pool.submit(new CallableTest());
pool.submit(new CallableTest());
//队列中最多放3个
pool.submit(new CallableTest());
pool.submit(new CallableTest());
pool.submit(new CallableTest());
//最大线程是5个 最多再开启3个临时线程
pool.submit(new CallableTest());
pool.submit(new CallableTest());
pool.submit(new CallableTest());
//再多一个就报错
// pool.submit(new CallableTest());
pool.shutdown();
}
}
8.3.5 任务拒绝策略
策略 | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
8.3.6 注意问题
Executors使用可能存在的陷阱大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
8.3.7 面试题
临时线程什么时候创建啊?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。常见: 游戏/购物 排队。
8.3.8 高级面试题
高阶面试题冲刺2W月薪!!!
- 自定义线程池什么时候创建临时线程?当任务数>core+queue
- 队列什么时候生效呢?当任务数>core
- 自定义线程池什么时候拒绝策略生效? 任务数>max+queue
- 什么时候创建核心线程?懒汉式 饿汉式
- 案例:
-
创建一个:基本线程为2,最大线程数为5,等待时间为5s,任务队列为10,创建工厂默认,拒绝策略默认-的线程池。
- for循环测试创建2个线程
- for循环测试创建5个线程
- for循环测试创建12个线程
- for循环测试创建13个线程
- for循环测试创建17个线程
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 5,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 15; i++) {
pool.submit(new MyRunnable());
}
pool.shutdown();
}
常。是默认的策略 |
| ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
| ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
| ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
8.3.6 注意问题
Executors使用可能存在的陷阱大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
[外链图片转存中…(img-WyOvg8CK-1715501236869)]
8.3.7 面试题
临时线程什么时候创建啊?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。常见: 游戏/购物 排队。
8.3.8 高级面试题
高阶面试题冲刺2W月薪!!!
- 自定义线程池什么时候创建临时线程?当任务数>core+queue
- 队列什么时候生效呢?当任务数>core
- 自定义线程池什么时候拒绝策略生效? 任务数>max+queue
- 什么时候创建核心线程?懒汉式 饿汉式
- 案例:
-
创建一个:基本线程为2,最大线程数为5,等待时间为5s,任务队列为10,创建工厂默认,拒绝策略默认-的线程池。
- for循环测试创建2个线程
- for循环测试创建5个线程
- for循环测试创建12个线程
- for循环测试创建13个线程
- for循环测试创建17个线程
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 5,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 15; i++) {
pool.submit(new MyRunnable());
}
pool.shutdown();
}