【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 …...
测试直播postman+Jenkins所学
接口自动化 什么是接口?本质上就是一个url,用于提供数据。后台程序提供一种数据地址,接口的数据一般是从数据库中查出来的。 postman自动化实操: 一般来说公司会给接口文档,如果没有,通过拦截,…...
5人3小时复刻Manus?开源OpenManus项目全解剖,我的DeepSeek股票报告这样诞生
大家好,我是大 F,深耕AI算法十余年,互联网大厂技术岗。分享AI算法干货、技术心得。 更多文章可关注《大模型理论和实战》、《DeepSeek技术解析和实战》,一起探索技术的无限可能! OpenManus是什么 1. 项目背景 OpenManus 是由 MetaGPT 核心团队仅用 3 小时复刻而成的开源…...
【javaEE】多线程(基础)
1.❤️❤️前言~🥳🎉🎉🎉 Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的…...
0.大模型开发知识点需求综述
文章目录 一、机器学习与深度学习基础二、自然语言处理(NLP)基础三、大模型架构四、训练优化技术五、数据处理与预处理六、分布式训练与并行化策略七、微调方法与参数高效微调八、训练框架、工具与自动化流程九、评估与部署十、前沿技术与未来趋势 已更新…...
WHAT - 前端阻塞场景梳理
目录 前言场景梳理1. JavaScript 执行阻塞主线程场景优化思路具体代码示例1. 长时间运行的同步 JavaScript 代码2. 过多的主线程任务(如频繁的 setTimeout/setInterval)3. 未优化的第三方库或框架初始化逻辑总结 2. 样式计算与布局(Layout&am…...
微前端框架 Qiankun 的应用及问题分析
一、Qiankun 的核心应用场景与优势 多技术栈共存与灵活集成 Qiankun 支持主应用与子应用使用不同技术栈(如 Vue、React、Angular 等),通过 HTML Entry 方式接入子应用,无需深度改造子应用即可实现集成,降低了技术迁移成…...
bash: uwsgi: 未找到命令
针对你提出的“bash: uwsgi: command not found”问题,以下是一些可能的解决方案,请按照步骤逐一排查: 1、检查uwsgi命令是否正确: 确保你输入的命令是uwsgi,而不是uWSGI或其他变体。 2、确认uwsgi是否已安装&…...
HAL库,配置adc基本流程
1. 初始化阶段---cubemx (1) GPIO初始化 函数:HAL_GPIO_Init() 作用:配置ADC引脚为模拟输入模式。 代码示例: // 使能GPIOA时钟 __HAL_RCC_GPIOA_CLK_ENABLE();// 配置PA1为模拟输入 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStr…...
【Unity】 HTFramework框架(六十一)Project窗口文件夹锁定器
更新日期:2025年3月7日。 Github源码:[点我获取源码] Gitee源码:[点我获取源码] 索引 Project窗口文件夹锁定器框架文件夹锁定自定义文件夹锁定限制条件 Project窗口文件夹锁定器 在Project窗口中,文件夹锁定器能够为任何文件夹加…...
网络安全技术整体架构 一个中心三重防护
网络安全技术整体架构:一个中心三重防护 在信息技术飞速发展的今天,网络安全的重要性日益凸显。为了保护信息系统不受各种安全威胁的侵害,网络安全技术整体架构应运而生。本文将详细介绍“一个中心三重防护”的概念,并结合代码示…...
《AJAX:前端异步交互的魔法指南》
什么是AJAX AJAX(Asynchronous JavaScript and XML,异步 JavaScript 和 XML) 是一种用于创建异步网页应用的技术,允许网页在不重新加载整个页面的情况下,与服务器交换数据并局部更新页面内容。尽管名称中包含 XML&…...
Elasticsearch 2025/3/7
高性能分布式搜索引擎。 数据库模糊搜索比较慢,但用搜索引擎快多了。 下面是一些搜索引擎排名 Lucene是一个Java语言的搜索引擎类库(一个工具包),apache公司的顶级项目。 优势:易扩展、高性能(基于倒排索引…...
LLM论文笔记 19: On Limitations of the Transformer Architecture
Arxiv日期:2024.2.26机构:Columbia University / Google 关键词 Transformer架构幻觉问题数学谜题 核心结论 1. Transformer 无法可靠地计算函数组合问题 2. Transformer 的计算能力受限于信息瓶颈 3. CoT 可以减少 Transformer 计算错误的概率&#x…...
那年周五放学
2025年3月7日,周五,天气晴,脑子一瞬间闪过02-05年中学期间某个周五下午,17:00即将放学的场景,那种激动,那种说不上的欣喜感,放学后,先走一段316国道,再走一段襄渝铁路&am…...
002-SpringCloud-OpenFeign(远程调用)
SpringCloud-OpenFeign 1.引入依赖2.编写一个远程调用接口3.测试 1.引入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId> </dependency><dependencyManageme…...
SAP 顾问的五年职业规划
SAP 顾问的职业发展受到技术进步、企业需求变化和全球经济环境的影响,因此制定长远规划充满挑战。面对 SAP 产品路线图的不确定性,如向 S/4HANA 和 Business Technology Platform (BTP) 的转变,顾问必须具备灵活性,以保持竞争力和…...
Pandas使用stack和pivot实现数据透视
Pandas的stack和pivot实现数据透视 经过统计得到多维度指标数据非常常见的统计场景,指定多个维度,计算聚合后的指标 案例:统计得到“电影评分数据集”,每个月份的每个分数被评分多少次:(月份,分…...
图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image
图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image 文章目录 图像生成-ICCV2019-SinGAN: Learning a Generative Model from a Single Natural Image主要创新点模型架构图生成器生成器源码 判别器判别器源码 损失函数需要源码讲解的私信我 S…...
c++ 操作符重载详解与示例
c 操作符重载详解与示例 操作符重载详解一、基本规则二、必须作为成员函数重载的运算符1. 赋值运算符 2. 下标运算符 []3. 函数调用运算符 ()4. 成员访问运算符 ->5. 转型运算符 三、通常作为非成员函数重载的运算符1. 算术运算符 2. 输入/输出运算符 << >> 四、…...
在Spring Boot项目中分层架构
常见的分层架构包括以下几层: 1. Domain 层(领域层) 作用:领域层是业务逻辑的核心,包含与业务相关的实体类、枚举、值对象等。它是对业务领域的抽象,通常与数据库表结构直接映射。 主要组件: 实体类(Entity):与数据库表对应的Java类,通常使用JPA或MyBatis等ORM框架…...
upload-labs详解(1-12)文件上传分析
目录 uploa-labs-main upload-labs-main第一关 前端防御 绕过前端防御 禁用js Burpsuite抓包改包 upload-labs-main第二关 上传测试 错误类型 upload-labs-env upload-labs-env第三关 上传测试 查看源码 解决方法 重命名,上传 upload-labs-env第四关…...
无人机应用探索:玻纤增强复合材料的疲劳性能研究
随着无人机技术的快速发展,轻量化已成为其结构设计的核心需求。玻纤增强复合材料凭借高强度、低密度和优异的耐环境性能,成为无人机机身、旋翼支架等关键部件的理想选择。然而,无人机在服役过程中需应对复杂多变的环境:高空飞行时…...
计算机毕业设计Python+DeepSeek-R1大模型空气质量预测分析(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
【渗透测试】基于时间的盲注(Time-Based Blind SQL Injection)
发生ERROR日志告警 查看系统日志如下: java.lang.IllegalArgumentException: Illegal character in query at index 203: https://api.weixin.qq.com/sns/jscode2session?access_token90_Vap5zo5UTJS4jbuvneMkyS1LHwHAgrofaX8bnIfW8EHXA71IRZwsqzJam9bo1m3zRcSrb…...
学习threejs,Animation、Core、CustomBlendingEquation、Renderer常量汇总
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️Animation常量汇总1.1.1 循…...
2、数据库的基础学习(中):分组查询、连接查询 有小例子
二、分组函数 功能:用作统计使用,又称为聚合函数或者统计函数或组函数 1、分类: sum 求和、avg 平均值、max最大值、min 最小值、count 计算个数 2、参数支持哪些类型 Sum\avg 一般处理数值型数据 max、min 可以数值型也可以字符型…...
Ubuntu搭建最简单WEB服务器
安装apache2 sudo apt install apache2 检查状态 $ sudo systemctl status apache2 ● apache2.service - The Apache HTTP ServerLoaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor prese>Active: active (running) since Thu 2025-03-06 09:51:10…...
如何学习编程?
如何学习编程? 笔记来源:How To Study Programming The Lazy Way 声明:该博客内容来自链接,仅作为学习参考 写在前面的话: 大多数人关注的是编程语言本身,而不是解决问题和逻辑思维。不要试图记住语言本身…...
OpenCV计算摄影学(14)实现对比度保留去色(Contrast Preserving Decolorization)的函数decolor()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 将彩色图像转换为灰度图像。它是数字印刷、风格化的黑白照片渲染,以及许多单通道图像处理应用中的基本工具。 cv::decolor 是 OpenCV…...
K8s 1.27.1 实战系列(七)Deployment
一、Deployment介绍 Deployment负责创建和更新应用程序的实例,使Pod拥有多副本,自愈,扩缩容等能力。创建Deployment后,Kubernetes Master 将应用程序实例调度到集群中的各个节点上。如果托管实例的节点关闭或被删除,Deployment控制器会将该实例替换为群集中另一个节点上的…...
Python第十五课:机器学习入门 | 从猜想到预测
🎯 本节目标 理解机器学习两大核心范式(监督/无监督学习)掌握特征工程的核心方法论实现经典算法:线性回归与K-Means聚类开发实战项目:房价预测模型理解模型评估与调优基础 一、机器学习核心概念(学生与老师…...
python 程序一次启动有两个进程的问题(flask)
0. 背景 写了一个使用 flask 作为服务框架的程序,发现每次启动程序的时候,使用 ps 都能观察到两个 python 进程。 此外,这个程序占用了 GPU 资源,我发现有两个 python 进程,分别占用了完全相同的 GPU 显存 1. 原因 …...
使用jcodec库,访问网络视频提取封面图片上传至oss
注释部分为FFmpeg(确实方便但依赖太大,不想用) package com.zuodou.upload;import com.aliyun.oss.OSS; import com.aliyun.oss.model.ObjectMetadata; import com.aliyun.oss.model.PutObjectRequest; import com.zuodou.oss.OssProperties;…...
MyBatis-Plus 与 Spring Boot 的最佳实践
在现代 Java 开发中,MyBatis-Plus 和 Spring Boot 的结合已经成为了一种非常流行的技术栈。MyBatis-Plus 是 MyBatis 的增强工具,提供了许多便捷的功能,而 Spring Boot 则简化了 Spring 应用的开发流程。本文将探讨如何将 MyBatis-Plus 与 Spring Boot 进行整合,并分享一些…...
python-51-使用最广泛的数据验证库Pydantic
文章目录 1 Pydantic2 models2.1 基本模型应用2.1.1 实例化2.1.2 访问属性2.1.3 修改属性2.2 嵌套模型【Optional】3 Fields3.1 Field()函数3.2 带注释的模式Annotated3.3 默认值3.3.1 default参数3.3.2 default_factory3.4 字段别名3.5 数字约束3.6 字符串约束3.7 严格模式4 A…...
Linux - 网络基础(应用层,传输层)
一、应用层 1)发送接收流程 1. 发送文件 write 函数发送数据到 TCP 套接字时,内容不一定会立即通过网络发送出去。这是因为网络通信涉及多个层次的缓冲和处理,TCP 是一个面向连接的协议,它需要进行一定的排队、确认和重传等处理…...
ADB、Appium 和 大模型融合开展移动端自动化测试
将 ADB、Appium 和 大模型(如 GPT、LLM) 结合,可以显著提升移动端自动化测试的智能化水平和效率。以下是具体的实现思路和应用场景: 1. 核心组件的作用 ADB(Android Debug Bridge): 用于与 Android 设备通信,执行设备操作(如安装应用、获取日志、截图等)。Appium: 用…...
【Pandas】pandas Series unstack
Pandas2.2 Series Computations descriptive stats 方法描述Series.argsort([axis, kind, order, stable])用于返回 Series 中元素排序后的索引位置的方法Series.argmin([axis, skipna])用于返回 Series 中最小值索引位置的方法Series.argmax([axis, skipna])用于返回 Series…...
rv1126交叉编译opencv+ffmpeg+x264
文章目录 🌕交叉编译x264🌙创建build_x264.sh(放在下载的x264目录下)🌙编译过程🌙查看编译后的so文件是否是arm版的 🌕下载编译ffmpeg🌙下载ffmpeg🌙创建编译脚本🌙创建ffmpeg编译路…...
【C++】ImGui:VSCode下的无依赖轻量GUI开发
本教程将手把手带您用纯原生方式构建ImGui应用,无需CMake/第三方库。您将全程明了自己每个操作的意义,特别适合首次接触GUI开发的新手。 环境配置 安装VSCode 作用:轻量级代码编辑器,提供智能提示操作: 官网下载安装…...
BUU44 [BJDCTF2020]ZJCTF,不过如此1 [php://filter][正则表达式get输入数据][捕获组反向引用][php中单双引号]
题目: 我仿佛见到了一位故人。。。也难怪,题目就是ZJCTF 按要求提交/?textdata://,I have a dream&filenext.php后: ......不太行,好像得用filephp://filter/convert.base64-encode/resourcenext.php 耶?那 f…...
Jetpack Compose — 入门实践
一、项目中使用 Jetpack Compose 从此节开始,为方便起见,如无特殊说明,Compose 均指代 Jetpack Compose。 开发工具: Android Studio 1.1 创建支持 Compose 新应用 新版 Android Studio 默认创建新项目即为 Compose 项目。 注意:在 Language 下拉菜单中,Kotlin 是唯一可…...
通过着装人体剪影预测关键点,以获取人体的二维尺寸数据。复现过程包括获取或生成3D人体数据集、生成轮廓图像、训练模型等步骤
根据文献《1_Clothes Size Prediction from Dressed-Human Silhouettes》复现方法,主要通过着装人体剪影预测关键点,以获取人体的二维尺寸数据。复现过程包括获取或生成3D人体数据集、生成轮廓图像、训练模型等步骤。 以下是进行复现的大致步骤…...
力扣HOT100之哈希:49. 字母异位词分组
这道题自己先想了一遍,定义了一个比较字符串的函数,用二重循环和一个数组来实现字符串的比较,若两个字符串是异位词,那么就返回true,否则返回false,在主函数中,同样用一个二重循环来遍历向量中的…...
基于单片机的智慧音乐播放系统研究
标题:基于单片机的智慧音乐播放系统研究 内容:1.摘要 随着科技的飞速发展,人们对音乐播放系统的智能化和个性化需求日益增长。本研究的目的是设计并实现一个基于单片机的智慧音乐播放系统。采用单片机作为核心控制单元,结合音频解码模块、存储模块和人机…...
pytest框架 核心知识的系统复习
1. pytest 介绍 是什么:Python 最流行的单元测试框架之一,支持复杂的功能测试和插件扩展。 优点: 语法简洁(用 assert 替代 self.assertEqual)。 自动发现测试用例。 丰富的插件生态(如失败重试、并发执…...
nginx 代理 redis
kubernetes 发布的redis服务端口为 31250 通过命令查询 [miniecs-88500735 /]$ minikube service redis --url http://192.168.49.2:31250[rootecs-88500735 /]# vi /etc/nginx/nginx.conf配置nginx.conf stream {upstream redis {server 192.168.49.2:31250;}server {liste…...
什么是:分布式贝叶斯推断
什么是:分布式贝叶斯推断 分布式贝叶斯推断(Distributed Bayesian Inference)是一种在分布式计算环境下进行贝叶斯统计推断的方法,旨在利用多节点或多设备的并行计算能力,高效处理大规模数据或复杂模型。其核心思想是将数据、模型或计算过程分解到多个节点上,通过协作完…...
C# 命名空间(Namespace)详解
在C#中,命名空间(Namespace)是一种封装和组织代码的方式,它允许将相关的类、接口、结构体和枚举等类型组织在一起,以避免命名冲突,并提供了一种逻辑上的分组方式。命名空间的使用有助于提高代码的可读性、可…...