【JavaEE】-- 多线程(初阶)4
文章目录
- 8.多线程案例
- 8.1 单例模式
- 8.1.1 饿汉模式
- 8.1.2 懒汉模式
- 8.2 阻塞队列
- 8.2.1 什么是阻塞队列
- 8.2.2 生产者消费者模型
- 8.2.3 标准库中的阻塞队列
- 8.2.4 阻塞队列的应用场景
- 8.2.4.1 消息队列
- 8.2.5 异步操作
- 8.2.5 自定义实现阻塞队列
- 8.2.6 阻塞队列--生产者消费者模型``
- 8.3 定时器
- 8.3.1 标准库中的定时器
- 8.3.2 实现定时器
- 8.4 线程池
- 8.4.1 线程池是什么
- 8.4.2 为什么要使用线程池
- 8.4.3 标准库中的线程池
- 8.4.4 自定义一个线程池
- 8.4.5 创建系统自带的线程池
- 8.4.6 线程池流程图
- 8.4.7 拒绝策略
- 9. 总结-保证线程安全的思路
- 10. 对比线程和进程
- 10.1 线程的优点
- 10.2 进程与线程的区别
- 11. wait() 和 sleep()的区别
8.多线程案例
8.1 单例模式
单例模式是校招中最常考的设计模式之一。
什么是单例?
在程序中一个类只需要有一个对象实例。
什么是设计模式?
设计模式是对常见的业务场景总结出来的处理方法,可以将设计模式理解为解决某个问题时限制了边界,同时限制了程序员的下限。
1. JVM中哪些类只有一个对象?
类对象:.class文件被加载到JVM中以后,会创建一个描述类结构的对象,称之为类对象,全局唯一。
在Java中可以通过 .class 获取到类对象。
static关键字修饰的属性,在该类所有实例对象中共享。
static 代码块在类加载的时候执行;不带static修饰的代码块,每new 一个对象都执行一次。
Java程序运行过程:
- 从磁盘加载 .class 文件到JVM,同时生成一个类对象。
- 创建实例变量
8.1.1 饿汉模式
实现过程:
- 要实现单例类,只需要定义一个static修饰的变量,就可以保证这个变量全局唯一(单例)。

- 既然是单例,就不想让外部去new这个对象,虽然返回的是同一个对象,已经实现了单例,但是在代码书写上有歧义。
public class Singleton {//定义一个类的成员变量,用static修饰,保证全局唯一private static Singleton instance = new Singleton();public Singleton getInstance() {return instance;}
}
public class Demo01 {public static void main(String[] args) {Singleton instance1 = new Singleton();System.out.println(instance1.getInstance());Singleton instance2 = new Singleton();System.out.println(instance2.getInstance());Singleton instance3 = new Singleton();System.out.println(instance3.getInstance());}
}
输出结果:

-
构造方法私有化


这样从语法上就不能再new对象了。 -
把获取对象的方法改为static 通过类名.方法名的方式调用。


输出结果:

我们把这种类加载的时候就完成对象初始化的创建方式称为“饿汉模式”。
由于程序在启动的时候可能需要加载很多的类。单例类,并不一定要在程序启动的时候用,为了节省计算机资源,加快程序的启动,可以让单例类在用到的时候在进行初始化。在编程中延时加载是一个褒义词。
8.1.2 懒汉模式
- 只声明这个全局变量,不初始化。
public class SingletonLazy {//定义一个类的成员变量,用static修饰,保证全局唯一private static SingletonLazy instance = null;
}
-
在 获取单例对象的时候加一个是否为空的判断,若为空则创建对象。

-
多次获取对象,打印对象结果。(单线程)
public class Demo03 {public static void main(String[] args) {SingletonLazy instance1 = SingletonLazy.getInstance();System.out.println(instance1);SingletonLazy instance2 = SingletonLazy.getInstance();System.out.println(instance2);SingletonLazy instance3 = SingletonLazy.getInstance();System.out.println(instance3);}
}
输出结果:

- 测试在多线程环境中的运行结果
public class Demo02 {public static void main(String[] args) {for (int i = 0; i < 10; i++) {Thread thread = new Thread(()->{SingletonLazy instance = SingletonLazy.getInstance();System.out.println(instance);});thread.start();}}
}
输出结果:

可以看到在多线程中出现了线程安全问题,不再是单例对象了。

分析出现线程安全问题的原因:
当t1LOAD时,instance 为NULL,执行完t1 的LOAD之后,被CPU调度到了 t2,我们假设CPU一次把 t2 的指令全部执行完,当执行完 t2 的最后一个指令STORE(将创建的instance对象写回到了主内存中)。又被CPU调度到了 t1 此时已经执行完了LOAD,已经进入了if语句,所以就会直接执行下面的NEW操作,就又创建了一个新的对象。当 t1 的指令执行到STORE,就会把在 t1 新创建的instance 写入到主内存中,会将在 t2 中创建的 instance 覆盖掉,这样就造成了线程安全问题。
给内层加锁

分析给内层加锁不能解决线程安全问题的原因:

我们假设CPU先执行完 t1 的 LOAD 和 判断操作,此时已经执行完了判断操作,并且此时 instance 为NULL,已经进入了 if 语句。但是接下来被CPU调度到了 t2 ,我们假设 t2 中的所有指令执行完,才被CPU再次调回了 t1 ,t2 中将instance对象写入到了主内存中,并释放了锁之后,此时已经进入到了 if 语句,t1 拿到了锁,就会执行下面的创建 instance 对象的操作,此时又创建了一个新的instance对象,然后被写入到了主内存中,覆盖掉了t2中创建的instance对象。此时,线程安全问题依旧存在。
给外层加锁

分析给外层加锁解决线程安全问题的原因:

给外层加锁和给内存加锁最大的不一样就是,给内层加锁是先进入 if 语句再竞争锁,还是先竞争锁再进入if 语句。
我们假设t1 先竞争到了锁,执行到了判断指令,此时 t1 已经进入到了 if 语句,然而被CPU调度到了t2,此时t2想要拿到锁,但是此时锁还被t1 拿着, t1 并没有释放锁,直到再次被CPU调度回 t1 ,直到执行完UNLOCK,此时已经创建了 instance 对象,并将其写入到了主内存中,当再次被CPU调度到 t2 时,它指向判断操作时,已经发现instance对象不为NULL,所以它就进不去if语句,就修改不了instance。所以,线程安全问题得以解决。
给外层加锁的另一个小问题:
- 当第一个线程进入getInstance 方法时,如果线程还没有初始化,则获取锁进行初始化操作,此时单例对象被第一个线程创建完成。
- 给外层加锁时,一旦有一个线程获取到了锁,那么这个线程就会创建 instance 对象,后面再竞争到锁的线程就永远不会进入 if 语句。
- 那么后面的竞争锁的行为就都是对资源的一种消耗,LOCK和UNLOCK对应的锁指令是互斥锁,比较消耗系统资源。
解决问题:
我们在加锁前再去判断一下是否需要加锁。

我们把这种叫做双重检查锁(DCL)

解决内存可见性和指令重排序问题:


DCL的方式必须要学会手写,面试中如果手写代码,必考!!!
面试中使用DCL,工作中使用“饿汉式”
8.2 阻塞队列
8.2.1 什么是阻塞队列
阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则.
阻塞队列能是⼀种线程安全的数据结构,并且具有以下特性:
• 当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素.
• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素.
阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型.
8.2.2 生产者消费者模型
⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。
⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取.
- 阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒.(削峰填⾕)
⽐如在"秒杀"场景下,服务器同⼀时刻可能会收到⼤量的⽀付请求.如果直接处理这些⽀付请求,服务器可能扛不住(每个⽀付请求的处理都需要⽐较复杂的流程).这个时候就可以把这些请求都放到⼀个阻塞队列中,然后再由消费者线程慢慢的来处理每个⽀付请求.
这样做可以有效进⾏"削峰",防⽌服务器被突然到来的⼀波请求直接冲垮.

8.2.3 标准库中的阻塞队列

public class Demo0 {public static void main(String[] args) throws InterruptedException {BlockingQueue queue = new LinkedBlockingQueue(3);queue.put(1);queue.put(2);queue.put(2);System.out.println("队列已满.....");queue.put(4);System.out.println("4不会被执行....");}
}
输出结果:

public class Demo0 {public static void main(String[] args) throws InterruptedException {BlockingQueue queue = new LinkedBlockingQueue(3);queue.put(1);queue.put(2);queue.put(2);System.out.println("队列已满.....");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());System.out.println("已经取出三个元素....");System.out.println(queue.take());System.out.println("已经取出四个元素");}
}
输出结果:

8.2.4 阻塞队列的应用场景
8.2.4.1 消息队列


问:如何判断消息是发给服务器A、服务器B还是服务器C?
答:服务器A在生产消息的时候,可以打一个标签,相当于对消息进行了分类,消费者在获取消息时,可以根据这个标签来获取。



- 阻塞队列也能使⽣产者和消费者之间解耦.
⽐如过年⼀家⼈⼀起包饺⼦.⼀般都是有明确分⼯,⽐如⼀个⼈负责擀饺⼦⽪,其他⼈负责包.擀饺⼦⽪的⼈就是"⽣产者",包饺⼦的⼈就是"消费者".
擀饺⼦⽪的⼈不关⼼包饺⼦的⼈是谁(能包就⾏,⽆论是⼿⼯包,借助⼯具,还是机器包),包饺⼦的⼈也不关⼼擀饺⼦⽪的⼈是谁(有饺⼦⽪就⾏,⽆论是⽤擀⾯杖擀的,还是拿罐头瓶擀,还是直接从超市买的).
8.2.5 异步操作

8.2.5 自定义实现阻塞队列
自定义实现的阻塞队列:
public class MyBlockingQueue {//定义一个数组来存放数据,具体的容量由构造方法中的参数决定private Integer[] elementData;//定义头尾下标private volatile int head;private volatile int tail;//定义数组中元素的个数private volatile int size = 0;//构造public MyBlockingQueue(int capacity){if (capacity <= 0){//处理输入不合法throw new RuntimeException("队列容量必须大于0");}elementData = new Integer[capacity];}// 插入---给代码块加锁public void put(Integer value) throws InterruptedException {synchronized (this){//判满if (size >= elementData.length){//阻塞队列在队列满的时候应该阻塞等待this.wait();//wait操作释放锁}//插入数据elementData[tail] = value;tail++;size++;//队列中有元素了,唤醒阻塞等待的线程synchronized (this){this.notifyAll();}//处理队尾下标if (tail >= elementData.length){tail = 0;}}}//获取数据---给方法加锁public synchronized Integer take() throws InterruptedException {//判空if (size == 0){//队列空的时候阻塞队列应该阻塞等待this.wait();}//获取数据Integer value = elementData[head];head++;size--;//队列中有空的位置了,唤醒阻塞队列的线程this.notifyAll();//处理队头下标if (head >= elementData.length){head = 0;}return value;}
}
测试加入元素:
public class Demo01 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue = new MyBlockingQueue(3);queue.put(1);queue.put(2);queue.put(3);System.out.println("已经加入三个元素....");queue.put(4);System.out.println("已经加入四个元素....");}
}
输出结果:

测试取出元素:
public class Demo02 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue queue = new MyBlockingQueue(3);queue.put(1);queue.put(2);queue.put(3);queue.take();queue.take();queue.take();System.out.println("已经取出三个元素....");queue.take();System.out.println("已经取出四个元素....");}
}
输出结果:

如果在put元素的时候,队列满了,积压了很多线程,当size–之后,就会有不止一个线程去put元素,就会出现还没有出队元素被覆盖的情况。为了解决这个问题我就需要把判满的 if 换成 while,让被唤醒之后的线程重新判断一次这个条件。

8.2.6 阻塞队列–生产者消费者模型``
public class Demo03 {public static void main(String[] args) {MyBlockingQueue queue = new MyBlockingQueue(100);//创建生产者线程Thread producer = new Thread(()->{int num = 0;while (true){try {//添加元素queue.put(num);System.out.println("生产了元素:" + num);num++;//休眠一会:10毫秒TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}});producer.start();//定义一个消费者线程Thread comsumer = new Thread(()->{//不断的从队列取出元素while (true) {try {//取出元素Integer value = queue.take();System.out.println("消费了元素:" + value);//休眠1秒TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}});comsumer.start();}
}
输出结果:

生产者先把队列生产满,生产满了之后消费一个生产一个。
8.3 定时器
8.3.1 标准库中的定时器

那么这个task任务究竟是怎样的呢?

我们追溯源码发现这个方法实现了Runnable接口。

而且里面有一个没有实现的抽象方法run()方法。我们就可以通过它来定义自己的任务。
public class Demo01 {public static void main(String[] args) {//根据JDK中提供的类,创建一个定时器Timer timer = new Timer();//向定时器中添加任务timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("该起床了.....");}}, 1000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("任务2.....");}}, 3000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("任务3.....");}}, 5000);}
}
输出结果:

执行完已有任务之后,就阻塞等待新任务。
8.3.2 实现定时器

public class MyTimer {//用一个阻塞队列来组织任务private BlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();//提供一个方法,提交任务public void schedule(Runnable runnable, long delay){//根据传入的参数,构造一个MyTaskMyTask task = new MyTask(runnable, delay);//把任务放入阻塞队列try {queue.put(task);} catch (InterruptedException e) {throw new RuntimeException(e);}}public MyTimer(){//创建扫描线程Thread thread = new Thread(()->{//不断的扫描队列中的任务while (true){//1. 取出任务try {MyTask task = queue.take();//2. 判断执行时间到了吗long currentTime = System.currentTimeMillis();if (currentTime >= task.getTime()){//时间到了,执行任务task.getRunnable().run();}else {//没有到时间,重新放回队列queue.put(task);}} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();}
}//用一个类来描述任务及任务执行的时间
class MyTask implements Comparable<MyTask>{//任务private Runnable runnable;//任务执行的时间private long time;public MyTask(Runnable runnable, long delay) {//校验任务不能为空if (runnable == null){throw new IllegalArgumentException("任务不能为空");}//时间不能为负数if (delay < 0){throw new IllegalArgumentException("时间不能为负数");}this.runnable = runnable;//计算出任务执行的具体时间this.time = delay + System.currentTimeMillis();}public Runnable getRunnable() {return runnable;}public long getTime() {return time;}@Overridepublic int compareTo(MyTask o) {//为了解决可能会溢出的问题,我们不使用相减的方式,使用比较的方式if (this.getTime() > o.getTime()){return 1;} else if (this.getTime() < o.getTime()) {return -1;}else {return 0;}//return (int) (this.time - o.getTime());//小根堆,小的在前}
}
public class Demo02 {public static void main(String[] args) {//创建一个定时器对象MyTimer timer = new MyTimer();//添加任务timer.schedule(()->{System.out.println("该起床了");},1000);timer.schedule(()->{System.out.println("任务2");},2000);timer.schedule(()->{System.out.println("任务3");},3000);timer.schedule(null, -10);}
}
输出结果:

问题1:忙等


假如当前的时间为18:52,判断出距离我们的队列中下一个要执行的任务时间还差一个小时,那么我们就会再次把这个任务放回队列中,在这一个小时中,构造方法中的while循环一直在循环执行,这个现象叫忙等,浪费了计算机的资源。
我们发现,放回队列的操作是导致忙等问题等问题的代码,为了解决这个问题,我们可以在放回队列时让程序等待一段时间,等待的时间为下一个任务的执行时间和当前时间的差。

问题2:添加新任务之后的第一个要执行的任务的时间变了

上一个问题解决了之后,在这个等待的时间里,我们可能会添加新的任务,假设我们添加了任务3,那么我们就会做不到定时执行任务。
为了解决这个问题,我们可以在当向队列中新添加任务时,统一唤醒一次线程,这样就能 保证能够扫描到新添加进去的线程,不会超时执行任务。

问题3:基于线程抢占式执行,由于CPU调度的问题产生的一系列现象
CPU调度的过程中可能会产生执行顺序的问题,或当一个线程执行到一半的时间被调度走的现象。

在执行上面这段代码我们假设该线程t1执行完MyTask task = queue.take();之后就被CPU调度走了,被调度走去执行主线程t2中的任务,我们假设主线程又添加了一个新的任务,执行下面这段代码,直执行完下面的代码,才被CPU重新调度回原来的线程t1。

线程t1得到CPU资源之后继续执行后面的代码

那么可能会出现下面的问题,由于线程调度的问题,t2先入队了新任务,执行事件中爱t1读取的任务执行时间之间,t1读的任务发现时间没有到放回队列的时候,设置的等待时间超过了新任务的执行时间,导致t2放入队列的新任务不能即使的执行。造成这个现象的原因是没有保证原子性。
为了解决上面的问题,我们需要扩大锁的范围。
//构造方法public MyTimer(){//创建扫描线程Thread thread = new Thread(()->{//不断的扫描队列中的任务while (true){//1. 取出任务try {synchronized (this){//wait和notify必须搭配synchronized使用MyTask task = queue.take();//2. 判断执行时间到了吗long currentTime = System.currentTimeMillis();if (currentTime >= task.getTime()){//时间到了,执行任务task.getRunnable().run();}else {//当前时间与执行任务时间的差long waitTime = task.getTime() - currentTime;//没有到时间,重新放回队列queue.put(task);//加入等待时间this.wait(waitTime);}}} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread.start();}

这样就解决了原子性的问题。
再来看接下来的代码:如果我们添加的任务延迟时间都是0呢?
public class Demo02 {public static void main(String[] args) throws InterruptedException {//创建一个定时器对象MyTimer timer = new MyTimer();//添加任务timer.schedule(()->{System.out.println("该起床了");},0);timer.schedule(()->{System.out.println("任务2");},0);timer.schedule(()->{System.out.println("任务3");},0);}
}
输出结果:

此时线程就又出现了问题。
在多线程环境中出现的问题,一定要使用线程查看工具去观察线程的状态。

上面显示第37行被锁定,我们就找一下第37行
当代码执行到这一行时,要从队列中取任务,但是当队列中没有任务的时候,就会阻塞等待,一直到队列中有可用元素才会执行。

- 提交任务1
- 扫描线程取出任务执行
- while循环继续执行任务,但是现在队列中没有任务可用,于是就阻塞等待。




8.4 线程池
只要面试问到多线程,必问!!!
8.4.1 线程池是什么
其实就是字面意思,一次创建很多个线程,用的时候从池子里拿一个出来,用完之后还回池子。
8.4.2 为什么要使用线程池
避免了频繁创建销毁线程的开销,提升程序的性能。
在数据库中就有一个DataSource数据源,一开始就初始化了很多个数据库连接,当需要用数据库连接的时候,从池子中获取一个连接,用完之后换回池子,并不真正的销毁连接。
线程池中的线程不停的扫描保存任务中的集合,当有任务的时候执行任务,没有任务的时候阻塞等待,但是并不销毁线程。
为什么使用线程池可提升效率?
少量创建,少量销毁。
内核态: 操作系统层面。
**用户态:**JVM层面(应用程序层)
8.4.3 标准库中的线程池

需要背一下,面试中可能会问JDK中提供了几种线程池。
在使用线程池时,我们只需要定义好任务,并提交给线程池即可,线程是池子自动创建的。
这是通过类名.方法名的方式获取对象,那么可不可以通过new的方式去获取对象?
当然可以,但是构造方法不能完整的覆盖业务的需要。
public class Student {private int id;private int age;private int classId;private String name;private String sno;//通过age 和 name 初始化一个对象public Student(int age, String name){this.age = age;this.name = name;}//通过classId 和 name 初始化一个对象public Student (int classId, String name){this.classId = classId;this.name = name;}public int getId() {return id;}public void setId(int id) {this.id = id;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public int getClassId() {return classId;}public void setClassId(int classId) {this.classId = classId;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getSno() {return sno;}public void setSno(String sno) {this.sno = sno;}
}

方法重载了,参数列表相同了,方法重载时要保证参数列表的类型和个数不同。
这个需求是真实存在的,但是语法限制,不能这么写。
//通过age 和 name 初始化一个对象public static Student createStudentByAgeAndName(int age, String name){Student student = new Student();student.setAge(age);student.setName(name);return student;}//通过classId 和 name 初始化一个对象public static Student createStudentByClassIdAndName(int classId, String name){Student student = new Student();student.setClassId(classId);student.setName(name);return student;}
这是一种工厂方法模式,根据不同的业务需求定义不同的方法获取对象。
8.4.4 自定义一个线程池
思路:
- 用Runnable描述任务
- 组织管理任务可以使用一个队列,可以用阻塞队列去实现,使用阻塞队列的好处是:当队列中没有任务的时候就等待,节省系统资源。
- 提供一个向队列中添加任务的方法。
- 创建多个线程,扫描队列中的任务,有任务的时候就取出来执行即可。
写代码的时候,要先整理思路,再动手实现。
MyThreadPool类:
public class MyThreadPool {//定义阻塞队列来组织任务BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(100);//构造方法public MyThreadPool(int threadNum){if (threadNum < 0){throw new IllegalArgumentException("线程任务必须大于0");}//创建线程for (int i = 0; i < threadNum; i++) {Thread thread = new Thread(()->{//不停的扫描队列while (true){try {//取出任务Runnable runnable = queue.take();//执行任务runnable.run();} catch (InterruptedException e) {throw new RuntimeException(e);}}});//启动线程thread.start();}}/*** 提交任务到线程池* @param runnable 具体的任务* @throws InterruptedException*/public void submit(Runnable runnable) throws InterruptedException {if (runnable == null){throw new IllegalArgumentException("任务不能为空");}//把任务加入到队列queue.put(runnable);}
}
测试类:
public class Demo01 {public static void main(String[] args) throws InterruptedException {//初始化一个自定义的线程池MyThreadPool threadPool = new MyThreadPool(3);//通过循环向线程中提交任务for (int i = 0; i <10; i++) {int taskId = i +1;threadPool.submit(()->{System.out.println("执行任务:" + taskId + Thread.currentThread().getName());});}}
}
输出结果:
执行任务:1Thread-0
执行任务:2Thread-0
执行任务:3Thread-0
执行任务:4Thread-0
执行任务:5Thread-0
执行任务:6Thread-0
执行任务:8Thread-0
执行任务:9Thread-0
执行任务:7Thread-1
执行任务:10Thread-0
8.4.5 创建系统自带的线程池
通过上面的工厂方法获取的线程池比较固定,也就是说不能进行定制,在实际的开发过程中,使用的是定制性比较强的创建线程池的方式。

面试题:说一说创建线程时的七个参数?
- 核心线程的数量
- 线程池中最大的线程数,最大线程数减去核心线程数 = 临时线程数。
- 临时线程的存活时间(一个数)。
- 临时线程的存活时间的时间单位,它和第三个参数配合在一起就是临时线程真正的存活时间
- 组织(保存)任务的队列。
- 创建线程的工厂,不关注。
- 拒绝策略。
面试题:线程池的工作原理:
实例1:
周末去吃火锅,火锅店很火,去的晚了就需要排号。
- 火锅店里有5张桌子(核心线程数)去了早了,店里没人就可以直接上桌点菜。
- 越到饭点人越来越多,这时5张桌子都坐满了,后面来的人就需要排号,最多可以排到20号(当于阻塞队列,20相当于阻塞队列的容量)。
- 排队的人越来越多,已经排到20号了(阻塞队列已经满了),在外面加了10张临时的桌子(临时线程数,线程池中总的线程数 = 核心线程数 + 临时线程数)。
- 排号的人就可以在外面的桌子上就餐。
- 时间越来越晚,排队的人都已经就餐了,外面的桌子慢慢也空下来了,老板说再等30分钟(临时线程的存活时间,临时线程的时间单位),如果再没人来就把外面的桌子收掉。
- 收掉外面的桌子,店里的5张桌子(最后又回归到了核心线程数)就可以满足顾客的就餐要求。
- 中途如果排号满了20号(阻塞队列满了),10张外面的桌子也坐满了(线程数量达到了线程池的最大个数),老板就不接待客人了(拒绝策略)。
实例2:去银行办业务
- 银行平时只开两上办理业务的窗口,相当于线程池的核心线程数。
- 当有新客户来银时,看到开放的两个容口空着,就可以直接去办理业务。
- 当两个窗口都有人在办理业务,后进来的客户就要去等待区等待。
- 随着等待的人越来越多,等待区已经满了,那么银行就叫来其他的业务员来开放其他三个窗口,一起办理业务。
- 再来银行的客户,就执行拒绝策略。
8.4.6 线程池流程图
- 添加任务,核心线程从队列中取任务去执行。
- 核心线程都在工作时,再添加的任务会进入到阻塞队列。
- 阻塞队列满了之后,会创建临时线程。
- 执行拒绝策略。
8.4.7 拒绝策略


- 直接拒绝
比如公司给分配了一个任务,我说现在我很忙,没有时间去处理这个任务,你就告诉领导说:你找别人干吧,我没时间。- 返回给调用者
比如公司给分配了一个任务,我说现在我很忙,没有时间去处理这个任务,你自己做吧。谁给我分配的任务我就把这个任务返回给谁,保证整个任务有线程执行。- 放弃目前最早等待的任务
比如公司给分配了一个任务,我说现在我很忙,没有时间去处理这个任务,老板说:最开始给你分的那个活,你可以不干了。
4. 放弃新提交的任务
放弃的任务,以后也找不回来了,所以指定拒绝策略的时候,要关注任务是不是需要必须执行,如果必须执行,就指定“返回调用者”,否则1 3 4 选一个即可,1在拒绝后会抛出异常;3,4在拒绝后不会抛出异常。
- 直接拒绝
public class Demo02 {public static void main(String[] args) throws InterruptedException {//定义一个线程池ThreadPoolExecutor threadPool =new ThreadPoolExecutor(3,5,1,TimeUnit.SECONDS,new LinkedBlockingQueue<>(5),new ThreadPoolExecutor.AbortPolicy());//通过循环向线程池中提交任务for (int i = 0; i < 100; i++) {int taskId = i + 1;threadPool.submit(()->{System.out.println("执行任务:" + taskId + ", " + Thread.currentThread().getName());});}}
}
输出结果:

- 放弃目前最早的任务

输出结果:

- 抛弃最新的任务

输出结果:

- 返回给调用者

输出结果:

根据不同的业务场景选择不同的拒绝策略
9. 总结-保证线程安全的思路
- 使用没有共享资源的模型
- 使用共享资源,只读不写的模型
- 不需要写共享资源的模型
- 使用不可变对象
- 直面线程安全(重点)
- 保证原子性
- 保证顺序性
- 保证可见性
10. 对比线程和进程
10.1 线程的优点
- 创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
- 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多
- 线程占⽤的资源要⽐进程少很多
- 能充分利⽤多处理器的可并⾏数量
- 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
- 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
- I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
10.2 进程与线程的区别
- 进程是系统进⾏资源分配和调度的⼀个独⽴单位,线程是程序执⾏的最⼩单位。
- 进程有⾃⼰的内存地址空间,线程只独享指令流执⾏的必要资源,如寄存器和栈。
- 由于同⼀进程的各线程间共享内存和⽂件资源,可以不通过内核进⾏直接通信。
- 线程的创建、切换及终⽌效率更⾼。
11. wait() 和 sleep()的区别
- 共同点,都会让线程阻塞一会儿
- 从实现使用上来说是两种不同的方式wait是Object类的方法,和锁相关,配合synchronized一起使用,调用wait之后会释放锁sleep是Thread类的方法,与锁无关.
- wait可以通过指定超时时间和通过notify方法唤醒,唤醒之后会重新竞争锁资源sleep只能通过超时时间唤醒
相关文章:
【JavaEE】-- 多线程(初阶)4
文章目录 8.多线程案例8.1 单例模式8.1.1 饿汉模式8.1.2 懒汉模式 8.2 阻塞队列8.2.1 什么是阻塞队列8.2.2 生产者消费者模型8.2.3 标准库中的阻塞队列8.2.4 阻塞队列的应用场景8.2.4.1 消息队列 8.2.5 异步操作8.2.5 自定义实现阻塞队列8.2.6 阻塞队列--生产者消费者模型 8.3 …...
WP 高级摘要插件:助力 WordPress 文章摘要精准自定义显示
wordpress插件介绍 “WP高级摘要插件”功能丰富,它允许用户在WordPress后台自定义文章摘要。 可设置摘要长度,灵活调整展示字数;设定摘要最后的显示字符, 如常用的省略号等以提示内容未完整展示;指定允许在摘要中显示…...
论文阅读 EEG-Inception
EEG-Inception: A Novel Deep Convolutional Neural Network for Assistive ERP-Based Brain-Computer Interfaces EEG-Inception是第一个集成Inception模块进行ERP检测的模型,它有效地结合了轻型架构中的其他结构,提高了我们方法的性能。 本研究的主要目…...
FFmpeg入门:最简单的音频播放器
FFmpeg入门:最简单的音频播放器 欢迎大家来到FFmpeg入门的第二章,今天只做一个最简单的FFmpeg音频播放器;同样,话不多说,先上流程图 流程图 以上流程和视频播放器的解码过程基本上是一致的; 不同点在于 S…...
物联网感应层数据采集器实现协议转换 数据格式化
数据采集器的核心功能实现涉及多个技术层面的协同工作,以下是各模块的详细实现解析: 协议转换实现 协议解析引擎:采用插件式架构,例如: P r o t o c o l P a r...
基于Linux系统的物联网智能终端
背景 产品研发和项目研发有什么区别?一个令人发指的问题,刚开始工作时项目开发居多,认为项目开发和产品开发区别不大,待后来随着自身能力的提升,逐步感到要开发一个好产品还是比较难的,我认为项目开发的目的…...
8.1.STM32_OLED
4.STM32_OLED 跟着江协科大的视频,无法点亮OLED屏幕解决办法 每个人使用的0.96寸OLED屏幕信号不一样,存在很多兼容性问题 归根结底就是驱动的问题! 本人的OLED是SSD1306,在淘宝店铺找了驱动文件后成功点亮,示例见文末 请针对自…...
Netty笔记9:粘包半包
Netty笔记1:线程模型 Netty笔记2:零拷贝 Netty笔记3:NIO编程 Netty笔记4:Epoll Netty笔记5:Netty开发实例 Netty笔记6:Netty组件 Netty笔记7:ChannelPromise通知处理 Netty笔记8…...
【算法方法总结·三】滑动窗口的一些技巧和注意事项
【算法方法总结三】滑动窗口的一些技巧和注意事项 【算法方法总结一】二分法的一些技巧和注意事项【算法方法总结二】双指针的一些技巧和注意事项【算法方法总结三】滑动窗口的一些技巧和注意事项 【滑动窗口】 数组的和 随着 右边指针 移动一定是 非递减 的,就是 …...
LabVIEW虚拟弗兰克赫兹实验仪
随着信息技术的飞速发展,虚拟仿真技术已经成为教学和研究中不可或缺的工具。开发了一种基于LabVIEW平台开发的虚拟弗兰克赫兹实验仪,该系统不仅能模拟实验操作,还能实时绘制数据图形,极大地丰富了物理实验的教学内容和方式。 …...
spring boot + vue 搭建环境
参考文档:https://blog.csdn.net/weixin_44215249/article/details/117376417?fromshareblogdetail&sharetypeblogdetail&sharerId117376417&sharereferPC&sharesourceqxpapt&sharefromfrom_link. spring boot vue 搭建环境 一、浏览器二、jd…...
清华团队提出HistoCell,从组织学图像推断超分辨率细胞空间分布助力癌症研究|顶刊精析·25-03-02
小罗碎碎念 今天和大家分享一篇2025-02-21发表于nature communications的文章,内容涉及病理空转单细胞。 从组织学图像推断细胞空间分布对癌症研究意义重大,但现有方法存在标注工作量大、分辨率或特征挖掘不足等局限。研究旨在开发一种高效准确的方法。 …...
分布式锁—2.Redisson的可重入锁一
大纲 1.Redisson可重入锁RedissonLock概述 2.可重入锁源码之创建RedissonClient实例 3.可重入锁源码之lua脚本加锁逻辑 4.可重入锁源码之WatchDog维持加锁逻辑 5.可重入锁源码之可重入加锁逻辑 6.可重入锁源码之锁的互斥阻塞逻辑 7.可重入锁源码之释放锁逻辑 8.可重入锁…...
html+js 轮播图
<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>轮播图示例</title><style>/* 基本样式…...
vue3:初学 vue-router 路由配置
承上一篇:nodejs:express js-mdict 作为后端,vue 3 vite 作为前端,在线查询英汉词典 安装 cnpm install vue-router -S 现在讲一讲 vue3:vue-router 路由配置 cd \js\mydict-web\src mkdir router cd router 我还…...
23种设计模式之《备忘录模式(Memento)》在c#中的应用及理解
程序设计中的主要设计模式通常分为三大类,共23种: 1. 创建型模式(Creational Patterns) 单例模式(Singleton):确保一个类只有一个实例,并提供全局访问点。 工厂方法模式࿰…...
Python 爬取唐诗宋词三百首
你可以使用 requests 和 BeautifulSoup 来爬取《唐诗三百首》和《宋词三百首》的数据。以下是一个基本的 Python 爬虫示例,它从 中华诗词网 或类似的网站获取数据并保存为 JSON 文件。 import requests from bs4 import BeautifulSoup import json import time# 爬取…...
C语言408考研先行课第一课:数据类型
由于408要考数据结构……会有算法题…… 所以,需要C语言来进行一个预备…… 因为大一贪玩,C语言根本没学进去……谁能想到考研还用得到呢?【手动doge(bushi) 软件用的是Clion,可以自行搜索教程下载使用。…...
03 HarmonyOS Next仪表盘案例详解(二):进阶篇
温馨提示:本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦! 文章目录 前言1. 响应式设计1.1 屏幕适配1.2 弹性布局 2. 数据展示与交互2.1 数据卡片渲染2.2 图表区域 3. 事件处理机制3.1 点击事件处理3.2 手势…...
探秘基带算法:从原理到5G时代的通信变革【四】Polar 编解码(一)
文章目录 2.3 Polar 编解码2.3.1 Polar 码简介与发展背景2.3.2 信道极化理论基础对称容量与巴氏参数对称容量 I ( W ) I(W) I(W)巴氏参数 Z ( W ) Z(W) Z(W)常见信道信道联合信道分裂信道极化 本博客为系列博客,主要讲解各基带算法的原理与应用,包括&…...
基础篇(一)强化学习是什么?从零开始理解智能体的学习过程
强化学习是什么?从零开始理解智能体的学习过程 你是否曾好奇过,人工智能是如何在复杂的环境中学会做出决策的?无论是打游戏的AI,还是自动驾驶的汽车,还是最近很火的DeepSeek它们的背后都离不开一种强大的技术——强化…...
如何直接导出某个conda环境中的包, 然后直接用 pip install -r requirements.txt 在新环境中安装
1. 导出 Conda 环境配置 conda list --export > conda_requirements.txt这将生成一个 conda_requirements.txt 文件,其中包含当前环境中所有包的列表及其版本信息。 2. 转换为 requirements.txt 文件 grep -v "^#" conda_requirements.txt | cut -d …...
基于 HTML、CSS 和 JavaScript 的智能九宫格图片分割系统
目录 1 前言 2 技术实现 2.1 HTML 结构 2.2 CSS 样式 2.3 JavaScript 交互 3 代码解析 3.1 HTML 部分 3.2 CSS 部分 3.3 JavaScript 部分 4 完整代码 5 运行结果 6 总结 6.1 系统特点 6.2 使用方法 1 前言 在当今数字化的时代,图片处理需求日益增长。…...
委托者模式(掌握设计模式的核心之一)
目录 问题: 举例: 总结:核心就是利用Java中的多态来完成注入。 问题: 今天刷面经,刷到装饰者模式,又进阶的发现委托者模式,发现还是不理解,特此记录。 举例: 老板…...
MySQL-高级查询
查询处理 排序(默认不是按主键排序的) order by 字段1[,字段2] [asc|desc] 默认是升序排序也可以指定 select 列表中列的序号进行排序如果是多个字段,那么在上一个字段排序完的基础上排序下一个 限制数量 limit 行数࿰…...
R JSON 文件
R JSON 文件 引言 在当今的数据分析和处理领域,R语言作为一种功能强大的统计计算和图形展示工具,被广泛应用于各种数据分析任务中。随着大数据时代的到来,数据的格式和结构变得越来越多样化。JSON(JavaScript Object Notation&a…...
Apache Kafka单节点极速部署指南:10分钟搭建开发单节点环境
Apache Kafka单节点极速部署指南:10分钟搭建开发单节点环境 Kafka简介: Apache Kafka是由LinkedIn开发并捐赠给Apache基金会的分布式流处理平台,现已成为实时数据管道和流应用领域的行业标准。它基于高吞吐、低延迟的设计理念,能够…...
Redis7——进阶篇(一)
前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。 基础篇: Redis(一)Redis(二)Redis(三)Redis&#x…...
点云配准技术的演进与前沿探索:从传统算法到深度学习融合(4)
4、点云配准面临的挑战与应对策略 4.1 点云配准面临的主要挑战 在点云配准的实际应用中,尽管已经取得了显著的研究成果,但仍然面临着诸多复杂而严峻的挑战,这些挑战严重制约了点云配准技术在更多领域的广泛应用和深入发展。 在自动驾驶场景…...
Linux·数据库INSERT优化
在业务中,我们经常会要对数据进行存储,对于少量数据插入时,我们可以直接使用 INSERT 插入数据,但是当我们需要插入的数据比较多时,使用 INSERT 插入的话时间消耗是很大的,具体而言单次插入600时,…...









