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

JUC并发编程(下)

✨作者:猫十二懿

❤️‍🔥账号:CSDN 、掘金 、个人博客 、Github

🎉公众号:猫十二懿

学习地址

写在最前

JUC并发编程(上)

JUC(Java Util Concurrent)学习内容框架:

JUC高并发编程内容
JUC概念
Lock接口
线程间通信
集合的线程安全
多线程锁
Callable接口
JUC三大辅助类
CountDowLatch
CyclicBarrier
Semaphore
读写锁:ReentrantReadWriteLock
阻塞队列
ThreadPool线程池
Fork/Join 框架
CompletableFuture

6、callable 接口

目前我们学习了有两种创建线程的方法-一种是通过创建 Thread 类,另一种是 通过使用 Runnable 创建线程。但是,Runnable 缺少的一项功能是,当线程终止时(即 run()方法完成时),我们无法使线程返回结果。为了支持此功能, Java 中提供了 Callable 接口。

6.1 创建线程的第三种方案—Callable 接口

Callable 接口特点:

  • 为了实现 Runnable,需要实现不返回任何内容的 run())方法,而对于 Callable,需要实现在完成时返回结果的 cal()方法。 •
  • call() 方法可以引发异常,而 run() 则不能。
  • 为实现 Callable 而必须重写 call()方法
  • 不能直接替换 runnable,因为 Thread 类的构造方法根本没有 Callable

6.2 Future 接口

当 call() 方法完成时,结果必须存储在主线程已知的对象中,以便主线程可 以知道该线程返回的结果。为此,可以使用 Future 对象。

将 Future 视为保存结果的对象,它可能暂时不保存结果,一旦Callable 返回将会进行保存。Future 基本上是主线程可以跟踪进度以及得到其他线程的结果的一种方式。要实现此接口,必须重写 5 种方法,这里列出了重要的方法,如下:

  1. public boolean cancel(boolean mayInterrupt):用于停止任务。如果尚未启动,它将停止任务。如果已启动,则仅在 mayInterrupt 为 true 时才会中断任务。
  2. public Object get() 抛出 InterruptedException,ExecutionException: 用于获取任务的结果。如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。
  3. public boolean isDone() :如果任务完成,则返回 true,否则返回 false

可以看到 Callable 和 Future 做两件事,Callable 与 Runnable 类似,因为它封装了要在另一个线程上运行的任务,而 Future 用于存储从另一个线程获得的结果。实际上,future 也可以与 Runnable 一起使用。

6.3 FutureTask

Java 库具有具体的 FutureTask 类型,该类型实现 Runnable 和 Future,并方便地将两种功能组合在一起。 可以通过为其构造函数提供 Callable 来创建 FutureTask。然后,将 FutureTask 对象提供给 Thread 的构造函数以创建 Thread 对象。因此,间接地使用 Callable 创建线程。

核心原理:(重点)

  1. 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些任务交给 Future 对象在后台完成,
  2. 当主线程将来需要时,就可以通过 Future 对象获得后台任务的计算结果或者执行状态,
  3. 一般 FutureTask 多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果,
  4. 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法,一旦计算完成,就不能再重新开始或取消计算 ,
  5. get 方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常 ,
  6. get 只计算一次,因此 get 方法放到最后。

例子说明:

  1. 我在敲代码,突然口渴了,出去买水不太合适(打断思路),我要继续敲代码,只能单独的叫一个人去帮我买水回来,然后放到我的桌子上,我就可以直接喝。(不影响主线程的情况下,单独开启一个单线程完成任务)
  2. 我口渴了,叫人去买水,但是他第一次买的水不是我想喝的,后面他又在去一趟买水,买了几趟才买到想喝的水,如果我下次还叫他去帮忙买水,那么他就可以直接买我喜欢喝的就可以了,不用再折腾那么多次。(也就是汇总一次,计算一次)
// Runnable 创建线程
class RunnableThread implements Runnable {@Overridepublic void run() {}
}
// Callable 接口 , 有返回值
class CallableThread implements Callable {@Overridepublic Integer call() throws Exception {System.out.println(Thread.currentThread().getName() + "进入callable");return 2222;}
}
public class CallableDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// Runnable 创建线程new Thread(new RunnableThread(), "Runnable创建线程").start();// Callable 接口创建线程, FutureTask未来任务FutureTask<Integer> futureTask = new FutureTask<>(new CallableThread());// 简化写法,FutureTask是一个函数式接口的类FutureTask<Integer> ft = new FutureTask<>(() -> {System.out.println(Thread.currentThread().getName() + "进入callable");return 1024;});// 创建线程new Thread(ft, "ft").start();new Thread(futureTask, "futureTask").start();// 没有计算完成就一直等待while (!ft.isDone()) {System.out.println("还在计算.....");}// 计算完成则会进行汇总 get到结果System.out.println(ft.get()); // 1024System.out.println(futureTask.get()); // 2222System.out.println(Thread.currentThread().getName() + "结束"); // main线程结束}
}

7、JUC三大辅助类

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过 多时 Lock 锁的频繁操作。这三种辅助类为:

  1. CountDownLatch: 减少计数
  2. CyclicBarrier: 循环栅栏
  3. Semaphore: 信号灯

7.1 减少计数 CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。

  • CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
  • 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

实际例子说明:

  • 6 个同学陆续离开教室后值班同学才可以关门。
/*** @author Shier 2023/2/21 22:11* 模拟教室人走完关灯效果*/
public class CountDownLatchDemo {public static void main(String[] args) {// 总共10个人,走完了才可以关灯// 使用CountDownLatch 进行初始化数据CountDownLatch countDownLatch = new CountDownLatch(10);for (int i = 10; i > 0; i--) {// 创建线程new Thread(() -> {System.out.println(Thread.currentThread().getName() + "学号学生离开教室");// 计数减一countDownLatch.countDown();}, String.valueOf(i)).start();}// 如果还没有为0 则会一直等待try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "关门走人了...");}
}

7.2 循环栅栏 CyclicBarrier

使用中 CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作

例子:

  • 收集七颗龙珠召唤神龙许愿
/*** @author Shier 2023/2/21 22:30*/
public class CyclicBarrierDemo {// 创建目标到达的值private static final int NUMBER = 7;public static void main(String[] args) {// 创建CyclicBarrier对象CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, (() -> {System.out.println("集齐完毕,开始许愿,祝你愿望成真!");}));// 创建线程for (int i = 1; i <= 7; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "星珠到手!");// 否则等待cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();}}, String.valueOf(i)).start();}}
}

7.3 信号灯 Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线 程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,获得许可之后其他的线程都处于阻塞状态,release 方法释放许可,其他线程方可获得许可证。

场景: 抢车位, 6 部汽车 3 个停车位

package com.shier.jucauxiliary;import javax.swing.plaf.TableHeaderUI;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;/*** @author Shier 2023/2/21 22:45* 6 部汽车 3 个停车位*/
public class SemaphoreDemo {public static void main(String[] args) {// 创建Semaphore 设置许可的数量Semaphore semaphore = new Semaphore(3);// 循环六次,创建六个线程for (int i = 1; i <= 6; i++) {new Thread(() -> {// 获得许可try {semaphore.acquire();System.out.println("-------" + Thread.currentThread().getName() + "号车寻找车位ing。");// 设置一个随机时间来占用车位Thread.sleep(2000);System.out.println(Thread.currentThread().getName() + "号车获得车位。");} catch (Exception e) {e.printStackTrace();} finally {// 释放许可System.out.println("*******" + Thread.currentThread().getName() + "号车离开车位。");semaphore.release();}}, String.valueOf(i)).start();}}
}

8、读写锁

8.1 读写锁介绍

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源, 就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock, 它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁

线程进入写锁的前提条件:

  1. 没有其他线程的读锁
  2. 没有其他线程的写锁

而读写锁有以下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公 平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为 读锁。

8.1.1 读锁发生死锁

image-20230222101944989

8.1.2 写锁发生死锁

当线程1 在对第一个进行写操作,线程2对第二个进行写操作,由于是可以并发操作的,所以当线程1或者线程2对另外的一个线程正在写操作的数据进行修改时需要等待对方写操作完成才可以进行,此时就有可能会出现互相操作对方的写操作进而出现死锁。

image-20230222102149497

锁的比较

image-20230222110348300

读写锁的案例实现:

/*** @author Shier 2023/2/22 10:27*/
class CacheDemo {// 创建一个map集合 volatile表示不断变化private volatile Map<String, Object> map = new HashMap<>();// 创建一个读写锁private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 写入过程public void writeMethod(String key, Object value) {// 写锁上锁readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "正在写入内容" + key);TimeUnit.MICROSECONDS.sleep(300);// 写入内容map.put(key, value);System.out.println(Thread.currentThread().getName() + "写入完成" + key);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁readWriteLock.writeLock().unlock();}}// 读取过程public Object readMethod(String key) {// 读锁上锁readWriteLock.readLock().lock();Object result = null;try {System.out.println(Thread.currentThread().getName() + "正在读取内容" + key);TimeUnit.MICROSECONDS.sleep(300);// 读取内容result = map.get(key);System.out.println(Thread.currentThread().getName() + "读取到了" + key);} catch (Exception e) {e.printStackTrace();} finally {// 释放锁readWriteLock.readLock().unlock();}return result;}
}public class ReadWriteLockDemo {public static void main(String[] args) {CacheDemo cacheDemo = new CacheDemo();// 写for (int i = 1; i <= 10; i++) {final int number = i;new Thread(() -> {cacheDemo.writeMethod(String.valueOf(number), number);},String.valueOf(i)).start();}// 读for (int i = 1; i <= 10; i++) {final int number = i;new Thread(() -> {cacheDemo.readMethod(String.valueOf(number));},String.valueOf(i)).start();}}
}

8.1.3 读写锁降级

锁降级是指将当前持有的锁从高级别锁降为低级别锁的过程。在多线程并发执行的场景中,通常情况下我们会在业务处理的过程中加锁,为了避免死锁、性能问题等问题,有时候需要在代码执行的过程中进行锁降级。

锁降级的主要目的是为了在持有高级别锁的情况下,可以继续执行需要持有低级别锁才能进行的操作,而不需要释放高级别锁再重新获取低级别锁的操作。例如,在一段代码中持有了数据库的行级锁,为了执行某些操作需要进行表级锁的操作,可以通过锁降级将行级锁降为表级锁。

锁降级的操作一般有以下几个步骤:

  1. 获取高级别锁;
  2. 执行需要高级别锁才能执行的操作;
  3. 获取低级别锁;
  4. 释放高级别锁。
  5. 使用低级别锁

在执行锁降级的过程中,需要保证高级别锁和低级别锁的顺序,先获取高级别锁再获取低级别锁,同时在释放锁的过程中也需要按照相反的顺序进行释放。如果顺序不正确,可能会导致死锁等问题。

总的来说,锁降级可以避免在执行代码的过程中频繁地获取和释放锁,提高并发性能和降低死锁等问题的发生。但是,在实际应用中需要根据具体情况进行评估和选择,避免过度使用锁降级导致代码难以维护。

大致的流程:

获取写锁
获取读锁
释放写锁
释放读锁

写锁的权限大于读锁的权限。

public class LowerLockDemo {public static void main(String[] args) {// 创建可重入锁对象ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 写锁ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();// 读锁ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();// 一般来说写锁大于读锁// 1.获取高级锁-写锁writeLock.lock();System.out.println("高级锁-写锁");// 2.获取低级锁-读锁readLock.lock();System.out.println("低级锁-读锁");// 3. 释放高级锁writeLock.unlock();// 4. 释放低级锁readLock.unlock();}
}
  • 读锁不能升级为写锁
  • 也就是低级锁不能升级为高级所

原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把 获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写 锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释 放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

8.2 悲观锁

悲观锁是一种保守的锁策略,其核心思想是在访问共享资源时,假定其他线程会对该资源进行修改,因此采取加锁的方式防止其他线程的干扰。悲观锁常见的实现方式是使用 synchronized 关键字或者 ReentrantLock 等锁对象。

当一个人进行操作时会上锁,阻塞别人,其他人就不能进行操作,只有当这个人把锁释放掉,其他人才可以进行操作,当其他人操作时也会重复以上的步骤。

悲观锁的优点:

  1. 可以保证并发访问的安全性,避免数据的脏读、幻读等问题;
  2. 悲观锁适用于写操作多、读操作少的场景,可以减少写冲突的发生,提高并发性能。

悲观锁的缺点:

  1. 悲观锁会在访问共享资源时,阻塞其他线程的访问,降低并发性能;
  2. 不支持并发操作,只能一个人一个人的进行操作,导致效率性能降低。
  3. 悲观锁对性能的影响和加锁的范围有关,加锁范围过大,会导致并发性能下降;
  4. 如果悲观锁的加锁粒度太小,会导致线程频繁加锁和释放锁,从而造成性能瓶颈。

总的来说,悲观锁虽然能够保证并发访问的安全性,但是在高并发场景下可能会导致性能问题,需要开发者在应用程序设计时进行权衡,选择适合的锁策略来提高并发性能

8.3 乐观锁

乐观锁是一种乐观的锁策略,其核心思想是假设在并发访问时,共享资源不会发生冲突,因此不会阻塞其他线程的访问,而是在更新共享资源时检查是否有其他线程对该资源进行修改。乐观锁常见的实现方式有版本号机制和CAS(Compare And Swap)算法等。

乐观锁的优点:

  1. 在并发读操作多、写操作少的场景下,乐观锁可以提高并发性能;
  2. 乐观锁不会阻塞其他线程的访问,避免了加锁的开销;
  3. 乐观锁的实现方式比较简单,实现成本低。

乐观锁的缺点:

  1. 乐观锁不能保证并发访问的安全性,存在数据的冲突、丢失等问题;
  2. 乐观锁的实现方式需要在更新共享资源时检查是否有其他线程的干扰,如果存在干扰需要进行重试,这会增加一定的开销;
  3. 乐观锁的适用场景有限,不适用于写操作比较频繁的场景。

总的来说,乐观锁在一些读操作多、写操作少的场景下可以提高并发性能,但是在一些高并发写操作的场景下可能会导致数据冲突,需要开发者在实际应用中进行评估和选择。

8.4 表锁、行锁

表锁是指对整个表进行加锁,当一个事务访问某个表时,会对整个表进行加锁,其他事务无法对该表进行更新、删除等操作。

优点:实现简单,对于一些只读的操作可以提高并发性能,

缺点:并发性能较低,会对整个表进行加锁,影响其他事务的操作。

行锁是指对表中的一行或多行进行加锁,当一个事务访问某个表时,只对需要访问的行进行加锁,其他行不受影响。

优点:并发性能较高,只对需要访问的行进行加锁,不会影响其他事务的操作

缺点:实现复杂,需要保证行级锁的粒度不会过细,否则会降低并发性能,可能会出现死锁状态。

在实际应用中,如果只有读操作,可以采用表锁提高并发性能;如果有较多的写操作,应该采用行锁避免数据冲突。在实现行锁时,需要注意锁粒度的问题,避免锁定过细或过粗。同时,为了提高并发性能,可以采用一些辅助手段,如缓存、索引等。

9、JUC阻塞队列

9.1 BlockingQueue 简介

Concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全 “传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建 高质量的多线程程序带来极大的便利。

阻塞队列,顾名思义,首先它是一个队列(先进先出), 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

image-20230222112840222

  • 当队列是空的,从队列中获取元素的操作将会被阻塞
  • 当队列是满的,从队列中添加元素的操作将会被阻塞
  • 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
  • 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

常用的队列主要有以下两种:

  • 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。 从某种程度上来说这种队列也体现了一种公平性
  • 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发 生的事件(栈)

9.1.1 多线程的阻塞

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起 的线程又会自动被唤起。

使用 BlockingQueue 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了

在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和 “消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准 备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地 解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一 发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度 大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么 生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的 数据处理完毕,反之亦然。

  • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起), 直到有数据放入队列
  • 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起), 直到队列中有空的位置,线程被自动唤醒

9.2 BlockingQueue 核心方法

image-20230222121403239

9.2.1 放入数据

  • offer(anObject):表示如果可能的话,将 anObject 加到 BlockingQueue 里,即 如果 BlockingQueue 可以容纳,则返回 true,否则返回 false.(本方法不阻塞当 前执行方法的线程)
  • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定 的时间内,还不能往队列中加入 BlockingQueue,则返回失败
  • put(anObject):把 anObject 加到 BlockingQueue 里,如果 BlockQueue 没有空间,则调用此方法的线程被阻断直到 BlockingQueue 里面有空间再继续

9.2.2 获取数据

  • poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null
  • poll(long timeout, TimeUnit unit):从 BlockingQueue 取出一个队首的对象, 如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知 道时间超时还没有数据可取,返回失败。
  • take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断 进入等待状态直到 BlockingQueue 有新的数据被加入;
  • drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定 获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加 锁或释放锁。
// 创建一个BlockingQueue对象
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);// 第一组
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("cc"));
System.out.println(blockingQueue.element());  // 查看第一个元素添加是否成功
// System.out.println(blockingQueue.add("b")); // 超出容量,报错
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());// 报错
// 第二组
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("2"));
System.out.println(blockingQueue.offer("3")); // 报错
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());// 报错// 第三组
blockingQueue.put("dd1");
blockingQueue.put("dd2");
blockingQueue.put("dd3");// 报错
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());// 报错//第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("cc"));
blockingQueue.offer("asdc", 3L, TimeUnit.SECONDS);// 报错

9.3 BlockingQueue 实现类

9.3.1 ArrayBlockingQueue(常用)

  • 由数组结构组成的有界阻塞队列

9.3.2 LinkedBlockingQueue(常用)

  • 链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列

ArrayBlockingQueue 和 LinkedBlockingQueue 是两个最普通也是最常用 的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个 类足以。

9.3.3 DelayQueue

  • 使用优先级队列实现的延迟无界阻塞队列

9.3.4 PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断通过构造函数传入的 Compator 对象来 决定),但需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。 因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费 数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁

  • 支持优先级排序的无界阻塞队列

9.3.5 SynchronousQueue

  • 不存储元素的阻塞队列,也即单个元素的队列

9.3.6 LinkedTransferQueue

  • 由链表组成的无界阻塞队列

9.3.7 LinkedBlockingDeque

  • 由链表组成的双向阻塞队列

9.4 总结

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

10、线程池

10.1 线程池简介

线程池(英语:thread pool):一种线程使用模式。

线程池是一个维护线程的池子,可以在需要时动态地创建和回收线程,从而避免了线程创建和销毁的开销,提高了线程的复用性和并发性能。

线程池通常包括以下几个组件:

  1. 任务队列:用于存放等待执行的任务。
  2. 线程池管理器:用于管理线程池,包括线程的创建、销毁、监控等。
  3. 线程工厂:用于创建线程。
  4. 线程池执行器:用于提交任务到线程池中执行。

线程池的优点包括:

  1. 降低线程创建和销毁的开销,提高了线程的复用性和并发性能。
  2. 可以控制并发线程的数量,避免因过多线程导致系统负载过高或资源耗尽的问题。
  3. 可以提供线程的可管理性,包括线程的监控、统计和异常处理等。

线程池的缺点主要在于对于任务处理时间较短的场景中,线程池的开销可能会比线程创建和销毁的开销更大。此外,线程池的并发度需要根据实际情况进行调整,如果并发度过高会导致系统负载过高,反之则会影响系统的性能。

在使用线程池时,需要根据实际情况进行配置,包括线程池的大小、任务队列的大小、拒绝策略等。同时,还需要注意线程池的并发度和资源使用情况,避免因过度使用线程池导致系统崩溃或性能下降。

10.2 线程池的使用方式

线程池可以通过调整线程池的大小、任务队列的大小、拒绝策略等参数来优化线程池的性能。同时,在使用线程池的过程中需要注意线程池的并发度和资源使用情况,避免因过度使用线程池导致系统崩溃或性能下降。

10.2.1 一池N线程 newFixedThreadPool(常用)

  1. 创建线程池对象:通过线程池工厂类的静态方法创建一个线程池对象,例如通过Executors类的静态方法创建。

    ExecutorService executor = Executors.newFixedThreadPool(10);
    
  2. 提交任务到线程池:通过线程池的execute()方法或submit()方法提交任务到线程池中执行。

    executor.execute(new Runnable() {@Overridepublic void run() {// 执行具体的任务}
    });Future<String> future = executor.submit(new Callable<String>() {@Overridepublic String call() throws Exception {// 执行具体的任务,返回结果return "result";}
    });
    
  3. 关闭线程池:在任务执行完毕后,通过线程池的shutdown()方法关闭线程池,防止新的任务提交到线程池中。

    executor.shutdown();
    

特点

  1. 线程池中的线程处于一定的量,可以很好的控制线程的并发量
  2. 线程可以重复被使用,在显示关闭之前,都将一直存在
  3. 超出一定量的线程被提交时候需在队列中等待

场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严 格限制的场景

10.2.2 newCachedThreadPool(常用)

  • 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空 闲线程,若无可回收,则新建线程
  • 可扩容线程池大小。

特点

  • 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
  • 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
  • 当线程池中,没有可用线程,会重新创建一个线程

场景: 适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景。

10.2.3 newSingleThreadExecutor(常用)

  • 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
  • 一池一线程

特点

  • 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行

场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个 线程的场景

三个线程的使用方式:

/*** @author Shier 2023/2/22 13:11*/
public class ThreadPollDemo1 {public static void main(String[] args) {// 一池N线程// 创建线程池ExecutorService executorService1 = Executors.newFixedThreadPool(3);// 一池一线程ExecutorService executorService2 = Executors.newSingleThreadExecutor();// 一池可扩容线程ExecutorService executorService3 = Executors.newCachedThreadPool();try {for (int i = 1; i <= 5; i++) {// 执行线程executorService1.execute(() -> {System.out.println(Thread.currentThread().getName() + "号线程正在执行...");});}} catch (Exception e) {e.printStackTrace();} finally {// 关闭线程池executorService1.shutdown();}}
}

10.3 线程池的七个参数

  1. corePoolSize:线程池的核心线程数
  2. maximumPoolSize:能容纳的最大线程数
  3. keepAliveTime:空闲线程存活时间
  4. unit:存活的时间单位
  5. workQueue:存放提交但未执行任务的队列
  6. threadFactory:创建线程的工厂类
  7. handler:等待队列满后的拒绝策略

线程池中,有三个重要的参数,决定影响了拒绝策略:

  1. corePoolSize - 核心线程数,也即最小的线程数
  2. workQueue - 阻塞队列
  3. maximumPoolSize - 最大线程数 当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。

当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。 总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

10.3.1 拒绝策略(重点)

  1. CallerRunsPolicy:当触发拒绝策略,只要线程池没有关闭的话,则使用调用 线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由 于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效 率上必然的损失较大 。
  2. AbortPolicy:丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常 信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执 行流程,影响后续的任务执行。
  3. DiscardPolicy: 直接丢弃,其他啥都没有
  4. DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

image-20230222134009491

  1. 在创建了线程池后,线程池中的线程数为零
  2. 当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
    1. 如 果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    2. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 队列;
    3. 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    4. 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程 池会启动饱和拒绝策略来执行。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
  4. 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    1. 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
    2. 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

自定义线程示例:

public class ThreadPollDemo2 {public static void main(String[] args) {// 自定义线程池ThreadPoolExecutor executor = new ThreadPoolExecutor(2,5,2L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(2),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());try {for (int i = 1; i <= 5; i++) {// 执行线程executor.execute(() -> {System.out.println(Thread.currentThread().getName() + "号线程正在执行...");});}} catch (Exception e) {e.printStackTrace();} finally {// 关闭线程池executor.shutdown();}}
}

11、Fork/Join 框架

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子 任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情:

  • Fork:把一个复杂任务进行分拆,大事化小
  • Join:把分拆任务的结果进行合并

11.1 任务处理

  1. 任务分割:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
  2. 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程 分别从双端队列里获取任务执行。
  3. 子任务执行完的结果都放在另外一个队列里, 启动一个线程从队列里取数据,然后合并这些数据。

11.2 Fork 方法

原理:

当我们调用 ForkJoinTask 的 fork 方法时,程序会把 任务放在 ForkJoinWorkerThread 的 pushTask 的 workQueue 中,异步地 执行这个任务,然后立即返回结果

11.3 join 方法

Join 方法的主要作用是阻塞当前线程并等待获取结果。

已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)和出 现异常(EXCEPTIONAL)

  • 如果任务状态是已完成,则直接返回任务结果
  • 如果任务状态是被取消,则直接抛出 CancellationException
  • 如果任务状态是抛出异常,则直接抛出对应的异常
class MyTask extends RecursiveTask<Integer> {// 拆分的插值不能超过10private static final Integer VALUE = 10;private int left;private int right;private int result;public MyTask(int left, int right) {this.left = left;this.right = right;}@Overrideprotected Integer compute() {// 判断差值if ((right - left) <= 10) {for (int i = left; i <= right; i++) {result += i;}} else {// 进一步拆分int middle = (left + right) / 2;// 左拆分MyTask leftTask = new MyTask(left, middle);// 右拆分MyTask rightTask = new MyTask(middle + 1, right);leftTask.fork();rightTask.fork();// 合并result = leftTask.join() + rightTask.join();}return result;}
}public class AddSum {public static void main(String[] args) throws ExecutionException, InterruptedException {MyTask myTask = new MyTask(0, 100);ForkJoinPool forkJoinPool = new ForkJoinPool();ForkJoinTask<Integer> submit = forkJoinPool.submit(myTask);Integer integer = submit.get();System.out.println(integer);// 关闭forkJoinPool.shutdown();}
}

12、CompletableFuture异步回调

使用CompletableFuture可以将耗时的操作异步执行,不会阻塞主线程,从而提高程序的性能和响应速度。同时,它还可以通过回调函数来处理计算结果,以及处理可能出现的异常情况。

12.1 有无返回值的异步调用

public class CompletableFutureDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 没有返回值的异步调用CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {System.out.println(Thread.currentThread().getName() + "*****没有返回值的异步调用测试");});runAsync.get();// 有返回值的异步调用CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> {System.out.println(Thread.currentThread().getName() + "======有返回值的异步调用测试");// int i = 1 / 0;return 200;});supplyAsync.whenComplete((t, u) -> {System.out.println("t:" + t); // 返回值System.out.println("u:" + u);// 异常值}).get();}
}
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {// 执行耗时的计算,返回结果return 1 + 2;
});future.thenAccept(result -> {// 处理计算结果System.out.println("计算结果为:" + result);
});future.exceptionally(ex -> {// 处理可能出现的异常情况System.out.println("计算出错:" + ex.getMessage());return null;
});

在这个例子中,我们使用CompletableFuture.supplyAsync方法来创建一个异步计算任务。该方法接受一个Supplier函数,用于执行耗时的计算并返回结果。然后,我们使用thenAccept方法来注册一个回调函数,该函数会在计算完成后被执行,并且会接收计算结果作为参数。最后,我们使用exceptionally方法来注册一个异常处理函数,该函数会在计算出错时被执行,并且会接收异常信息作为参数。

需要注意的是,thenAccept方法是一个消费者函数,它不会返回任何值,因此不能在回调函数中修改计算结果。如果需要在回调函数中修改计算结果,可以使用thenApply方法,该方法接受一个函数,用于将计算结果转换为另一个值,并返回转换后的结果。

rkJoinPool();
ForkJoinTask submit = forkJoinPool.submit(myTask);
Integer integer = submit.get();
System.out.println(integer);
// 关闭
forkJoinPool.shutdown();
}
}

# 12、CompletableFuture异步回调使用CompletableFuture可以将耗时的操作异步执行,不会阻塞主线程,从而提高程序的性能和响应速度。同时,它还可以通过回调函数来处理计算结果,以及处理可能出现的异常情况。## 12.1 有无返回值的异步调用```java
public class CompletableFutureDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 没有返回值的异步调用CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {System.out.println(Thread.currentThread().getName() + "*****没有返回值的异步调用测试");});runAsync.get();// 有返回值的异步调用CompletableFuture<Integer> supplyAsync = CompletableFuture.supplyAsync(() -> {System.out.println(Thread.currentThread().getName() + "======有返回值的异步调用测试");// int i = 1 / 0;return 200;});supplyAsync.whenComplete((t, u) -> {System.out.println("t:" + t); // 返回值System.out.println("u:" + u);// 异常值}).get();}
}
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {// 执行耗时的计算,返回结果return 1 + 2;
});future.thenAccept(result -> {// 处理计算结果System.out.println("计算结果为:" + result);
});future.exceptionally(ex -> {// 处理可能出现的异常情况System.out.println("计算出错:" + ex.getMessage());return null;
});

在这个例子中,我们使用CompletableFuture.supplyAsync方法来创建一个异步计算任务。该方法接受一个Supplier函数,用于执行耗时的计算并返回结果。然后,我们使用thenAccept方法来注册一个回调函数,该函数会在计算完成后被执行,并且会接收计算结果作为参数。最后,我们使用exceptionally方法来注册一个异常处理函数,该函数会在计算出错时被执行,并且会接收异常信息作为参数。

需要注意的是,thenAccept方法是一个消费者函数,它不会返回任何值,因此不能在回调函数中修改计算结果。如果需要在回调函数中修改计算结果,可以使用thenApply方法,该方法接受一个函数,用于将计算结果转换为另一个值,并返回转换后的结果。

相关文章:

JUC并发编程(下)

✨作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 学习地址 写在最前 JUC并发编程&#xff08;上&#xff09; JUC&#xff08;Java Util Concurrent&#xff09;学习内容框架&…...

API接口的基础知识

API是应用程序编程接口&#xff08;Application Programming Interface&#xff09;的缩写&#xff0c;能够起到两个软件组件之间的连接器或中介的作用。此类接口往往通过一组明确的协议&#xff0c;来表示各种原始的请求和响应。API文档可以向开发人员展示请求和响应是如何形成…...

基于Spring Boot的教务管理系统

文章目录项目介绍主要功能截图&#xff1a;登录首页学生信息管理班级信息管理教师信息管理教师评价部分代码展示设计总结项目获取方式&#x1f345; 作者主页&#xff1a;Java韩立 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题…...

网页扫描图像并以pdf格式上传到服务器端

本文描述如何通过网页驱动扫描仪、高拍仪等图像扫描设备进行图像扫描&#xff0c;扫描结果经编辑修改后以pdf压缩格式上传到后台java程序中进行服务器端落盘保存。图像扫描上传如文字描述顺序所介绍&#xff0c;先要驱动扫描设备工作&#xff0c;进行纸张数据的光学扫描操作形成…...

Airbyte入门

Airbyte 后端技术栈Java 17框架&#xff1a;JerseyAPI&#xff1a; OAS3数据库&#xff1a;PostgreSQL单元和E2E测试&#xff1a;JUnit 5编排&#xff1a;Temporal连接器技术栈连接器可以用任何语言编写。但是&#xff0c;最常见的语言是&#xff1a;Python3.9.0Java 17前端技术…...

研究人员在野外发现大量的信息窃取者 “Stealc “的样本

一个名为Stealc的新信息窃取者正在暗网上做广告&#xff0c;它可能成为其他同类恶意软件的一个值得竞争的对象。 "SEKOIA在周一的一份报告中说&#xff1a;"威胁行为者将Stealc作为一个功能齐全、随时可用的窃取者&#xff0c;其开发依赖于Vidar、Raccoon、Mars和Re…...

数据结构——复杂度讲解(2)

作者&#xff1a;几冬雪来 时间&#xff1a;2023年2月22日 内容&#xff1a;数据结构复杂度讲解 目录 前言&#xff1a; 复杂度讲解&#xff08;2&#xff09;&#xff1a; 1.空间复杂度是什么&#xff1a; 2.空间复杂度讲解&#xff1a; 结尾&#xff1a; 前言&#x…...

【LeetCode】任务调度器 [M](贪心)

621. 任务调度器 - 力扣&#xff08;LeetCode&#xff09; 一、题目 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行&#xff0c;并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间&…...

Spring代理模式——静态代理和动态代理

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

Anaconda和PyCharm的一些安装问题和命令

今天更新了Windows上的Anaconda到2.3.2&#xff0c;PyCharm到2022.3。 ——发现是纯纯的犯贱orz。出了一堆问题。在这里记录一下供后来者参考。 Anaconda安装 将.\anaconda3\Scripts 和.\anaconda3\Library\bin添加到系统环境变量中。 新建环境的目录在.\anaconda3\envs下 N…...

sql优化建议

对查询进行优化&#xff0c;应尽量避免全表扫描&#xff0c;首先应考虑在 where 及 order by 涉及的列上建立索引。 应尽量避免在 where 子句中使用!或<>操作符&#xff0c;否则将引擎放弃使用索引而进行全表扫描。 应尽量避免在 where 子句中对字段进行 null 值判断&a…...

google hacker语句

哎&#xff0c;我就是沾边&#xff0c;就是不打实战(&#xffe3;o&#xffe3;) . z Z 文章目录前言一、什么是谷歌Docker&#xff1f;二、受欢迎的谷歌docker语句谷歌docker的例子日志文件易受攻击的 Web 服务器打开 FTP 服务器SSH私钥电子邮件列表实时摄像机MP3、电影和 PDF…...

Spring AOP

Spring AOP 文章目录Spring AOP1.概念1.面向切面编程2.AOP的目的3.AOP实现的分类4.AOP 术语2. Spring AOP的特性1.能力与目标2.AOP机制1.理解SpringAOP的代理2.AOP代理类的自调用代码的粒度如何让自调用走代理&#xff1f;3.Enabling AspectJ Support3. 定义切面与切点1. 声明切…...

【消费战略方法论】认识消费者的恒常原理(一):消费者稳态平衡原理

“消费战略”是塔望咨询基于大量的战略与营销实践经验结合心理学、经济学、传播学等相关专业学科的知识应用进行提炼与创造形成的战略方法体系。消费战略强调以消费者为导向&#xff0c;进行企业、品牌战略、品牌营销的制订和落地&#xff0c;企业经营的每个环节和输出的每个动…...

python居然能语音控制电脑壁纸切换,只需60行代码

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 家在日常的电脑使用中&#xff0c;都会有自己喜爱类型的桌面 单纯的桌面有时候会让人觉得单调 今天&#xff0c;就由我带领大家只用60行代码打造一款语音壁纸切换器程序&#xff0c; 让大家能够通过语音的方式来控制电脑去…...

内存泄露定位手段(c语言hook malloc相关方式)

如何确定有内存泄露问题&#xff0c;如何定位到内存泄露位置&#xff0c;如何写一个内存泄漏检测工具&#xff1f; 1&#xff1a;概述 内存泄露本质&#xff1a;其实就是申请调用malloc/new&#xff0c;但是释放调用free/delete有遗漏&#xff0c;或者重复释放的问题。 内存…...

STM32 CAN波特率计算

STM32 CAN波特率计算简介CAN总线收发&#xff0c;中断方式接收配置代码部分reference简介 CAN通信帧共分为数据帧、远程帧、错误帧、过载帧和帧间隔&#xff0c;本文这里以数据帧为例。 显性电平对应逻辑0&#xff0c;CAN_H和CAN_L之差为2.5V左右。而隐性电平对应逻辑1&#x…...

C/C++ 中#define 的妙用,让代码更美一些

C/C 中#define 的妙用&#xff0c;让代码更美一些 flyfish 1 数值类型输出易读的字符串形式 例如使用enum定义一些错误值&#xff0c;想要将数值类型的错误&#xff0c;输出易读的字符串形式 重要的一句代码 #define MAKE_PAIR(val) std::make_pair(val, #val)可以看到 #va…...

Linux文件系统操作与磁盘管理

查看磁盘和目录的容量 使用 df 命令查看磁盘的容量 df在实验楼的环境中你将看到如下的输出内容&#xff1a; 但在实际的物理主机上会更像这样&#xff1a; 物理主机上的 /dev/sda2 是对应着主机硬盘的分区&#xff0c;后面的数字表示分区号&#xff0c;数字前面的字母 a 表示…...

【Python】批量采集原神表情包~

嗨害大家好鸭~我是小熊猫(✿◡‿◡) 最近迷上了原神&#xff0c; 不自觉中就很喜欢保存广大旅行者制作的表情包~ 真的很有意思诶~ 源码资料电子书:点击此处跳转文末名片获取 一个个保存的话&#xff0c;好像效率很低嘛… 那我就发挥我小熊猫的老本行直接给把他们全部采集下…...

C语言基本语法注释类型关键字

C 基本语法 标识符 给变量所取的名字叫变量名&#xff0c;定义变量的名字需要遵循标识符的命名规则。 标识符是用来标识变量、符号常量、数组、函数、文件等名字的有效字符序列。 标识符的命名规则&#xff1a; 1.只能由字母、数字和下划线组成&#xff08;例如&#xff1a…...

【C ++】C++入门知识(二)

C入门&#xff08;二&#xff09; 作者&#xff1a;小卢 专栏&#xff1a;《C》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 1.引用 1.1.引用的概念及应用 引用&#xff08;&&#xff09; 引用不是新定义一个变量&#xff0…...

qt qchart学习

Qt Charts主要由QChartView、QChart、QLegend图例、坐标轴(由QAbstractAxis子类实现)、**数据源(由QAbstractSeries子类实现)**等组成使用QChart的前期准备1. Qt5.9及以上版本&#xff1b;2. .pro文件中添加QT charts3. 在使用QChart的各个控件之前&#xff0c;引用头文件并必…...

手工布署 java 项目

新建一个java springboot项目 maven 这是一个非常简易的 springBoot 的项目 使用 maven 的 package 工具进行打包 把包上传到 linux 的机器上&#xff0c; 确保 linux 机器上安装了 java jdk工具&#xff0c; 并且配置好了 JAVA_HOME 注意&#xff0c;helloworld 默认的是要使…...

《设计模式》观察者模式

《设计模式》观察者模式 观察者模式是一种行为型设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者对象可以同时监听和相应被观察者对象的状态变化&#xff0c;以达到解耦和复用的目的。观察者模式的优点如下&#xff1a; 解耦&#xff1a;观察者模…...

基于SpringBoot的外卖项目(详细开发过程)

基于SpringBootMyBatisPlus的外卖项目1、软件开发整体介绍软件开发流程角色分工2、外卖项目介绍项目介绍产品展示后台系统管理移动端技术选型功能结构角色3、开发环境的搭建开发环境说明建库建表Maven项目搭建项目的目录结构pom.xmlapplication.ymlReggieApplication启动类配置…...

ChatGPT 研发传言席卷互联网公司,这会是一门好生意吗?

ChatGPT&#xff08;也称GPT-3&#xff09;是一种基于人工智能的自然语言生成模型&#xff0c;由OpenAI团队开发。它是GPT系列模型的最新版本&#xff0c;于2020年6月发布。ChatGPT的由来GPT-1是在2018年发布的第一个版本&#xff0c;使用了12亿个参数。随后&#xff0c;GPT-2在…...

获取servlet转发和响应重定向的方式是什么?

&#xff08;1&#xff09; 重定向和转发的区别 1&#xff09;重定向是浏览器发送请求并受到响应以后再次向一个新地址发请求&#xff1b;转发是服务器受到请求后为了完成响应转到一个新的地址。 2&#xff09;重定向中有两次请求对象&#xff0c;不共享数据&#xff1b;转发…...

jvm知识点

jvm面试总结 类加载机制? 如何把类加载到jvm中 ? 装载–>链接–>初始化–>使用–>卸载 装载: ClassFile–>字节流–>类加载器将字节流所代表的静态结构转化为方法区的运行时数据结构在我们的堆中生成一个代表这个类的java.lang.Class对象 链接: 验证–…...

MoveIT Noetic控制真实机械臂

文章目录 环境概述配置修改编写Action Server执行问题故障解决参考接前几篇: ROS MoveIT1(Noetic)安装总结 Solidworks导出为URDF用于MoveIT总结(带prismatic) MoveIT1 Assistant 总结 MoveIT Rviz和Gazebo联合仿真 环境 Ubuntu20.04;ROS1 Noetic;VMware...