并发编程多线程

image-20240914152004854

1.线程和进程的区别?

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

2. 并行和并发有什么区别

并发(concurrent)是同一时间应对(dealing with)多件事情的能力,比如:多个线程轮流使用一个或多个CPU

并行(parallel)是同一时间动手做(doing)多件事情的能力,比如:4核CPU同时执行4个线程

举例:

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行

3. 创建线程的四种方式

共有四种方式可以创建线程,分别是:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程

详细创建方式参考下面代码:

继承Thread类

public class MyThread extends Thread {@Overridepublic void run() {System.out.println("MyThread...run...");}public static void main(String[] args) {// 创建MyThread对象MyThread t1 = new MyThread() ;MyThread t2 = new MyThread() ;// 调用start方法启动线程t1.start();t2.start();}}

实现runnable接口

public class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("MyRunnable...run...");}public static void main(String[] args) {// 创建MyRunnable对象MyRunnable mr = new MyRunnable() ;// 创建Thread对象Thread t1 = new Thread(mr) ;Thread t2 = new Thread(mr) ;// 调用start方法启动线程t1.start();t2.start();}}

实现Callable接口

public class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {System.out.println("MyCallable...call...");return "OK";}public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建MyCallable对象MyCallable mc = new MyCallable() ;// 创建FFutureTask<String> ft = new FutureTask<String>(mc) ;// 创建Thread对象Thread t1 = new Thread(ft) ;Thread t2 = new Thread(ft) ;// 调用start方法启动线程t1.start();// 调用ft的get方法获取执行结果String result = ft.get();// 输出System.out.println(result);}}

线程池创建线程

public class MyExecutors implements Runnable{@Overridepublic void run() {System.out.println("MyRunnable...run...");}public static void main(String[] args) {// 创建线程池对象ExecutorService threadPool = Executors.newFixedThreadPool(3);threadPool.submit(new MyExecutors()) ;// 关闭线程池threadPool.shutdown();}}

4. runnable 和 callable 有什么区别

  1. Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
  3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛

5. 线程的 run()和 start()有什么区别?

start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。

run(): 封装了要被线程执行的代码,可以被调用多次。

6. 线程包括哪些状态,状态之间是如何变化的?

image-20240919202249426

状态之间是如何变化的

image-20230503203629212

分别是

  • 新建
    • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
    • 此时未与操作系统底层线程关联
  • 可运行
    • 调用了 start 方法,就会由新建进入可运行
    • 此时与底层线程关联,由操作系统调度执行
  • 终结
    • 线程内代码已经执行完毕,由可运行进入终结
    • 此时会取消与底层线程关联
  • 阻塞
    • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
    • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
  • 等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
  • 有时限等待
    • 调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

7. 新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。

代码举例:

为了确保三个线程的顺序你应该先启动最后一个(T3 调用T2,T2调用T1),这样T1就会先完成而T3最后完成

public class JoinTest {public static void main(String[] args) {// 创建线程对象Thread t1 = new Thread(() -> {System.out.println("t1");}) ;Thread t2 = new Thread(() -> {try {t1.join();                          // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2");}) ;Thread t3 = new Thread(() -> {try {t2.join();                              // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t3");}) ;// 启动线程t1.start();t2.start();t3.start();}}

8. notify()和 notifyAll()有什么区别?

notifyAll:唤醒所有wait的线程

notify:只随机唤醒一个 wait 线程

9. 在 java 中 wait 和 sleep 方法的不同?

共同点:

  • wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态

不同点:

  • 方法归属不同

    • sleep(long) 是 Thread 的静态方法
    • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同

    • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
    • wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁(搭配synchronized一起用),而 sleep 则无此限制
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
    • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

10. 如何停止一个正在运行的线程?

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程

使用退出标志,使线程正常退出

public class MyInterrupt1 extends Thread {volatile boolean flag = false ;     // 线程执行的退出标记@Overridepublic void run() {while(!flag) {System.out.println("MyThread...run...");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 创建MyThread对象MyInterrupt1 t1 = new MyInterrupt1() ;t1.start();// 主线程休眠6秒Thread.sleep(6000);// 更改标记为truet1.flag = true ;}
}

使用stop方法强行终止

public class MyInterrupt2 extends Thread {volatile boolean flag = false ;     // 线程执行的退出标记@Overridepublic void run() {while(!flag) {System.out.println("MyThread...run...");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 创建MyThread对象MyInterrupt2 t1 = new MyInterrupt2() ;t1.start();// 主线程休眠2秒Thread.sleep(6000);// 调用stop方法t1.stop();}
}

使用interrupt方法中断线程

  • 打断阻塞线程

    Thread t1 = new Thread(() -> {System.out.println("t1 正在运行...");try {Thread.sleep(5000);  // 线程t1进入阻塞状态} catch (InterruptedException e) {e.printStackTrace();  // 中断时会抛出异常并进入这里}
    }, "t1");
    t1.start();
    Thread.sleep(500);  // 主线程等待0.5秒,保证t1已经进入阻塞状态
    t1.interrupt();  // 打断线程t1
    System.out.println(t1.isInterrupted());  // 打印t1的中断状态
    
    t1 正在运行...
    java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.itheima.basic.MyInterrupt3.lambda$main$0(MyInterrupt3.java:8)at java.lang.Thread.run(Thread.java:748)
    true
    

    解释:

    • 线程 t1sleep(5000) 时进入了阻塞状态。主线程在 0.5 秒后调用了 t1.interrupt(),导致 t1 被唤醒并抛出 InterruptedException
    • 捕获异常后,t1 结束了执行,并且我们通过 t1.isInterrupted() 打印了中断状态,该方法返回 true,表示 t1 确实被中断过。
  • 打断正常线程

    Thread t2 = new Thread(() -> {while (true) {Thread current = Thread.currentThread();boolean interrupted = current.isInterrupted();  // 检查当前线程是否被中断if (interrupted) {System.out.println("打断状态:" + interrupted);  // 打印中断状态break;  // 结束循环,线程退出}}
    }, "t2");t2.start();
    Thread.sleep(500);  // 主线程等待0.5秒后中断t2
    t2.interrupt();  // 如果此行取消注释,将打断t2
    
    打断状态:true
    

    解释:

    • 线程 t2 在循环中检测 isInterrupted() 的状态。一旦 t2.interrupt() 被调用,isInterrupted() 返回 true,并打印中断状态,随后线程 t2 退出循环。

11. 讲一下synchronized关键字的底层原理?

如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人

Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住

public class TicketDemo {static Object lock = new Object();int ticketNum = 10;public synchronized void getTicket() {synchronized (this) {if (ticketNum <= 0) {return;}System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);// 非原子性操作ticketNum--;}}public static void main(String[] args) {TicketDemo ticketDemo = new TicketDemo();for (int i = 0; i < 20; i++) {new Thread(() -> {ticketDemo.getTicket();}).start();}}}

Monitor 被翻译为监视器,是由jvm提供,c++语言实现

在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

public class SyncTest {static final Object lock = new Object();static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}}
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:

image-20230504165342501

  • monitorenter 上锁开始的地方
  • monitorexit 解锁的地方
  • 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁

monitor主要就是跟这个对象产生关联,如下图

image-20230504165833809

Monitor内部具体的存储结构:

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取

  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程

  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

具体的流程:

  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

参考回答:

  • Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】

  • 它的底层由monitor实现的,monitor是jvm级别的对象( C++实现),线程获得锁需要使用对象(锁)关联monitor

  • 在monitor内部有三个属性,分别是owner、entrylist、waitset

  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程

12. synchronized关键字的底层原理-进阶

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

  • 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

2.2.1 对象的内存结构

在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充

image-20230504172253826

我们需要重点分析MarkWord对象头

2.2.2 MarkWord

image-20230504172541922

  • hashcode:25位的对象标识Hash码

  • age:对象分代年龄占4位

  • biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁

  • thread:持有偏向锁的线程ID,占23位

  • epoch:偏向时间戳,占2位

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位

我们可以通过lock的标识,来判断是哪一种锁的等级

  • 后三位是001表示无锁
  • 后三位是101表示偏向锁
  • 后两位是00表示轻量级锁
  • 后两位是10表示重量级锁
2.2.3 再说Monitor重量级锁

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

image-20230504172957271

简单说就是:每个对象的对象头都可以设置monoitor的指针,让对象与monitor产生关联

2.2.4 轻量级锁

在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

static final Object obj = new Object();public static void method1() {synchronized (obj) {// 同步块 Amethod2();}
}public static void method2() {synchronized (obj) {// 同步块 B}
}

加锁的流程

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

image-20230504173520412

2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。

image-20230504173611219

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。

image-20230504173922343

4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。

解锁过程

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。

image-20230504173955680

3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。

image-20230504174045458

2.2.5 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现

这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

static final Object obj = new Object();public static void m1() {synchronized (obj) {// 同步块 Am2();}
}public static void m2() {synchronized (obj) {// 同步块 Bm3();}
}public static void m3() {synchronized (obj) {}
}

加锁的流程

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

image-20230504174525256

2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。

image-20230504174505031

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些

image-20230504174736226

解锁流程参考轻量级锁

2.2.6 参考回答

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

描述
重量级锁底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

一旦锁发生了竞争,都会升级为重量级锁

13. 你谈谈 JMM(Java 内存模型)

JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

image-20230504181638237

特点:

  1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
  3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

14. CAS 你知道吗?

CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizer(AQS框架)

  • AtomicXXX类

例子:

我们还是基于刚才学习过的JMM内存模型进行说明

  • 线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中

image-20230504181947319

一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功

  • 线程1操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 101 (a++)
    • 线程1拿A的值与主内存V的值进行比较,判断是否相等
    • 如果相等,则把B的值101更新到主内存中

image-20230504182129820

  • 线程2操作:V:int a = 100,A:int a = 100,B:修改后的值:int a = 99(a–)
    • 线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99)
    • 不相等,则线程2更新失败

image-20230504181827330

  • 自旋锁操作
    • 因为没有加锁,所以线程不会陷入阻塞,效率较高

    • 如果竞争激烈,重试频繁发生,效率会受影响

image-20230504182447552

需要不断尝试获取共享内存V中最新的值,然后再在新的值的基础上进行更新操作,如果失败就继续尝试获取新的值,直到更新成功

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令

image-20230504182737931

都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现

image-20230504182838426

在java中比较常见使用有很多,比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法

  • ReentrantLock中的一段CAS代码

image-20230504182958703

image-20240920113338029

15. 乐观锁和悲观锁

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

16. 请谈谈你对 volatile 的理解

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

2.5.1 保证线程间的可见性

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

一个典型的例子:永不停止的循环

package com.itheima.basic;// 可见性例子
// -Xint
public class ForeverLoop {static boolean stop = false;public static void main(String[] args) {new Thread(() -> {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}stop = true;System.out.println("modify stop to true...");}).start();foo();}static void foo() {int i = 0;while (!stop) {i++;}System.out.println("stopped... c:"+ i);}
}

当执行上述代码的时候,发现foo()方法中的循环是结束不了的,也就说读取不到共享变量的值结束循环。

主要是因为在JVM虚拟机中有一个JIT(即时编辑器)给代码做了优化。

上述代码

while (!stop) {
i++;
}

在很短的时间内,这个代码执行的次数太多了,当达到了一个阈值,JIT就会优化此代码,如下:

while (true) {
i++;
}

当把代码优化成这样子以后,及时stop变量改变为了false也依然停止不了循环

解决方案:

第一:

在程序运行的时候加入vm参数-Xint表示禁用即时编辑器,不推荐,得不偿失(其他程序还要使用)

第二:

在修饰stop变量的时候加上volatile,表示当前代码禁用了即时编辑器,问题就可以解决,代码如下:

static volatile boolean stop = false;
2.5.2 禁止进行指令重排序

用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

image-20230505082441116

在去获取上面的结果的时候,有可能会出现4种情况

情况一:先执行actor2获取结果—>0,0(正常)

情况二:先执行actor1中的第一行代码,然后执行actor2获取结果—>0,1(正常)

情况三:先执行actor1中所有代码,然后执行actor2获取结果—>1,1(正常)

情况四:先执行actor1中第二行代码,然后执行actor2获取结果—>1,0(发生了指令重排序,影响结果)

解决方案

在变量上添加volatile,禁止指令重排 序,则可以解决问题

image-20230505082835588

屏障添加的示意图

image-20230505082923729

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

其他补充

我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在int x这个变量上呢?

下面代码使用volatile修饰了x变量

image-20230505083124159

屏障添加的示意图

image-20230505083217904

这样显然是不行的,主要是因为下面两个原则:

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下
  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

所以,现在我们就可以总结一个volatile使用的小妙招:

  • 写变量让volatile修饰的变量的在代码最后位置
  • 读变量让volatile修饰的变量的在代码最开始位置

17. 什么是AQS?

全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别

synchronizedAQS
关键字,c++ 语言实现java 语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

AQS常见的实现类

  • ReentrantLock 阻塞式锁

  • Semaphore 信号量

  • CountDownLatch 倒计时锁

工作机制:

  • 在AQS中维护了一个使用了volatile修饰的state属性来表示资源的状态,0表示无锁,1表示有锁
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

image-20230505083840046

  • 线程0来了以后,去尝试修改state属性,如果发现state属性是0,就修改state状态为1,表示线程0抢锁成功
  • 线程1和线程2也会先尝试修改state属性,发现state的值已经是1了,有其他线程持有锁,它们都会到FIFO队列中进行等待,
  • FIFO是一个双向队列,head属性表示头结点,tail表示尾结点

如果多个线程共同去抢这个资源是如何保证原子性的呢?

image-20230505084451193

在去修改state状态的时候,使用的cas自旋锁来保证原子性,确保只能有一个线程修改成功,修改失败的线程将会进入FIFO队列中等待

AQS是公平锁吗,还是非公平锁?

image-20240920153705584

  • 新的线程与队列中的线程共同来抢资源,是非公平锁

  • 新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁

比较典型的AQS实现类ReentrantLock,它默认就是非公平锁,新的线程与队列中的线程共同来抢资源

18. ReentrantLock的实现原理

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断

  • 可以设置超时时间

  • 可以设置公平锁

  • 支持多个条件变量

  • 与synchronized一样,都支持重入

image-20230505091736569

ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似

构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

查看ReentrantLock源码中的构造方法:

image-20230505091827720

提供了两个构造方法,不带参数的默认为非公平

如果使用带参数的构造函数,并且传的值为true,则是公平锁

其中NonfairSync和FairSync这两个类父类都是Sync

image-20230505092151244

而Sync的父类是AQS,所以可以得出ReentrantLock底层主要实现就是基于AQS来实现的

image-20230505091833629

工作流程

image-20230505092340431

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功

  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部

  • 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程

  • 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁

19. synchronized和Lock有什么区别 ?

  • 语法层面

    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现

    • Lock 是接口,源码由 jdk 提供,用 java 语言实现

    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

  • 功能层面

    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能

    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量

      • 获取等待状态
        Lock 可以提供当前线程的锁状态信息,synchronized 无法实现。例子(通过 ReentrantLock 获取等待线程数):

        import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final ReentrantLock lock = new ReentrantLock();public void getLockInfo() {System.out.println("有多少线程在等待这个锁:" + lock.getQueueLength());}
        }
        
      • 公平锁Lock 可以设置公平锁,确保线程按请求顺序获取锁,而 synchronized 是非公平锁。

        import java.util.concurrent.locks.ReentrantLock;public class FairLockExample {private final ReentrantLock fairLock = new ReentrantLock(true);  // true 表示公平锁public void fairLockMethod() {fairLock.lock();try {// 公平锁确保线程按请求顺序执行} finally {fairLock.unlock();}}
        }
      • 可打断Lock 提供了可中断的锁机制,允许在获取锁时响应中断,而 synchronized 无法中断等待锁的线程。例子(使用 lockInterruptibly() 让线程在等待锁时能被中断):

        import java.util.concurrent.locks.ReentrantLock;public class InterruptibleLockExample {private final ReentrantLock lock = new ReentrantLock();public void lockWithInterrupt() throws InterruptedException {lock.lockInterruptibly();  // 等待锁时可以被中断try {// do something} finally {lock.unlock();}}
        }

        可打断详细解释:

        什么是“可打断”?

        假设你正在排队买票,突然接到一个紧急电话,你可能需要离开队伍,去处理电话。但如果你使用的是 synchronized,那你就不能“离开队伍”,只能一直等到轮到你为止。而使用 lockInterruptibly(),就像排队时你可以被“叫走”,即在等待的过程中可以被中断,去做别的事情。

        基本思路

        • synchronized:如果你在等待锁,你必须等到前面的任务完成释放锁,期间不能“被叫走”(不能中断)。
        • lockInterruptibly():如果你在等待锁,可以在等待过程中被“叫走”(被中断),去处理别的紧急任务。

        更简单的例子

        假设有两个小朋友要玩同一个玩具:

        1. 小朋友 A 拿到了玩具,正在玩(相当于获取了锁)。
        2. 小朋友 B 也想玩,但只能等 A 玩完了(等待锁)。
        3. B 的妈妈打电话给他,要他先回家吃饭(中断请求)。

        如果 B 使用的是 synchronized,他必须等 A 玩完才能离开,哪怕妈妈在催他回家。 但如果 B 使用的是 lockInterruptibly(),他可以在等待的时候听妈妈的话,直接离开去吃饭,不用一直等着玩具。

        代码解释

        这里用简单的代码演示“可打断”的行为:

        import java.util.concurrent.locks.ReentrantLock;public class SimpleInterruptExample {private final ReentrantLock lock = new ReentrantLock();// 模拟小朋友玩玩具的方法public void play() throws InterruptedException {// 使用 lockInterruptibly() 可以在等待时被中断lock.lockInterruptibly();try {System.out.println(Thread.currentThread().getName() + " 拿到了玩具,正在玩...");Thread.sleep(5000);  // 模拟玩耍5秒} finally {lock.unlock();  // 玩完玩具后释放锁System.out.println(Thread.currentThread().getName() + " 玩完了,离开了玩具。");}}public static void main(String[] args) {SimpleInterruptExample example = new SimpleInterruptExample();// 创建两个小朋友的线程Thread child1 = new Thread(() -> {try {example.play();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + " 被叫回家吃饭!");}}, "小朋友A");Thread child2 = new Thread(() -> {try {example.play();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + " 被叫回家吃饭!");}}, "小朋友B");// 小朋友A 先拿到玩具开始玩child1.start();// 等A开始玩之后,B也想玩,但会被叫回家吃饭(中断)try {Thread.sleep(100);  // 让小朋友A先拿到玩具} catch (InterruptedException e) {e.printStackTrace();}child2.start();// 模拟妈妈打电话让小朋友B回家(中断线程B)child2.interrupt();  // B 在等待时收到中断信号}
        }
        运行结果:

        小朋友A 拿到了玩具,正在玩…
        小朋友B 被叫回家吃饭!
        小朋友A 玩完了,离开了玩具。

        解读:
        • 小朋友A 拿到了玩具,开始玩。
        • 小朋友B 想玩,但是因为玩具被 A 拿着,只能等。这时,妈妈打电话让 B 回家, B 就被中断了,不再等玩具,直接回家(退出了等待)。
        • A 玩完了,玩具被释放,但 B 已经不需要了。

        总结

        • lockInterruptibly() 就像排队时你可以被“叫走”,去处理更紧急的事情。即使还没等到锁,也可以中途退出。
        • 如果你使用的是 synchronized,就只能一直等到锁释放,不能在等待过程中响应其他事件。
      • 可超时Lock 提供了超时机制,允许线程尝试在一定时间内获取锁,超时后可放弃等待,synchronized 没有这个功能。例子(使用 tryLock(long time, TimeUnit unit) 设定超时时间):

        import java.util.concurrent.TimeUnit;
        import java.util.concurrent.locks.ReentrantLock;public class TimeoutLockExample {private final ReentrantLock lock = new ReentrantLock();public void lockWithTimeout() throws InterruptedException {if (lock.tryLock(3, TimeUnit.SECONDS)) {  // 尝试获取锁,最多等待3秒try {// 成功获取锁后执行} finally {lock.unlock();}} else {System.out.println("超时未能获取锁");}}
        }
      • 多条件变量
        Lock 支持多个 Condition 对象,每个条件变量可以独立地管理线程的等待和唤醒,而 synchronized 只能通过 wait()notify() 对所有线程进行管理。例子(使用 Condition 实现多个条件变量):

        import java.util.concurrent.locks.Condition;
        import java.util.concurrent.locks.ReentrantLock;public class MultiConditionExample {private final ReentrantLock lock = new ReentrantLock();private final Condition condition1 = lock.newCondition();private final Condition condition2 = lock.newCondition();public void waitOnCondition1() throws InterruptedException {lock.lock();try {condition1.await();  // 等待 condition1 被唤醒} finally {lock.unlock();}}public void signalCondition1() {lock.lock();try {condition1.signal();  // 唤醒等待在 condition1 上的线程} finally {lock.unlock();}}
        }
      • ReentrantReadWriteLock:适用于读多写少的场景,支持读写锁分离。多个线程可以同时获取读锁,但写锁是独占的。例子(使用 ReentrantReadWriteLock 实现读写锁分离):

        import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();public void readMethod() {readLock.lock();try {// 多线程可以同时读取} finally {readLock.unlock();}}public void writeMethod() {writeLock.lock();try {// 写操作,只有一个线程能写} finally {writeLock.unlock();}}
        }
        
  • 性能层面

    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

20. 死锁产生的条件是什么?

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

  1. 互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享状态,即一次只能被一个进程或线程占用。

  2. 请求与保持条件(Hold and Wait):进程或线程至少需要持有一个资源,并且在等待其他资源时不释放已占有的资源。

  3. 不可剥夺条件(No Preemption):已分配给进程或线程的资源不能被强制性地剥夺,只能由持有资源的进程或线程主动释放。

  4. 循环等待条件(Circular Wait):存在一个进程或线程的资源申请序列,使得每个进程或线程都在等待下一个进程或线程所持有的资源。

例如:

t1 线程获得A对象锁,接下来想获取B对象的锁

t2 线程获得B对象锁,接下来想获取A对象的锁

代码如下:

package com.itheima.basic;import static java.lang.Thread.sleep;public class Deadlock {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {System.out.println("lock A");try {sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (B) {System.out.println("lock B");System.out.println("操作...");}}}, "t1");Thread t2 = new Thread(() -> {synchronized (B) {System.out.println("lock B");try {sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (A) {System.out.println("lock A");System.out.println("操作...");}}}, "t2");t1.start();t2.start();}
}

控制台输出结果

image-20220902171032898

此时程序并没有结束,这种现象就是死锁现象…线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁。

如何进行死锁诊断:

当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和 jstack

步骤如下:

第一:查看运行的线程

image-20220902171426738

第二:使用jstack查看线程运行的情况,下图是截图的关键信息

运行命令:jstack -l 46032

image-20220902172229567

其他解决工具,可视化工具

  • jconsole

用于对jvm的内存,线程,类 的监控,是一个基于 jmx 的 GUI 性能监控工具

打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行

  • VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈

打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行

21. 聊一下ConcurrentHashMap

ConcurrentHashMap 是一种线程安全的高效Map集合

底层数据结构:

  • JDK1.7底层采用分段的数组+链表实现

  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

(1) JDK1.7中concurrentHashMap

数据结构

image-20230505092654811

  • 提供了一个segment数组,在初始化ConcurrentHashMap 的时候可以指定数组的长度,默认是16,一旦初始化之后中间不可扩容
  • 在每个segment中都可以挂一个HashEntry数组,数组里面可以存储具体的元素,HashEntry数组是可以扩容的
  • 在HashEntry存储的数组中存储的元素,如果发生冲突,则可以挂单向链表

存储流程

image-20230505093055382

  • 先去计算key的hash值,然后确定segment数组下标
  • 再通过hash值确定hashEntry数组中的下标存储数据
  • 在进行操作数据的之前,会先判断当前segment对应下标位置是否有线程进行操作,为了线程安全使用的是ReentrantLock进行加锁,如果获取锁是被会使用cas自旋锁进行尝试
(2) JDK1.8中concurrentHashMap

在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表

采用 CAS + Synchronized来保证并发安全进行实现

  • CAS控制数组节点的添加

  • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升

image-20230505093507265

22. 导致并发程序出现问题的根本原因是什么

22. Java程序中怎么保证多线程的执行安全

Java并发编程三大特性

  • 原子性

  • 可见性

  • 有序性

(1)原子性

一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行

比如,如下代码能保证原子性吗?

image-20230505205200628

以上代码会出现超卖或者是一张票卖给同一个人,执行并不是原子性的

解决方案:

1.synchronized:同步加锁

2.JUC里面的lock:加锁

image-20230505210853493

(3)内存可见性

内存可见性:让一个线程对共享变量的修改对另一个线程可见

比如,以下代码不能保证内存可见性

image-20230505211002252

解决方案:

  • synchronized

  • volatile(推荐)

  • LOCK

(3)有序性

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

还是之前的例子,如下代码:

image-20230505211209336

解决方案:

  • volatile

23. 说一下线程池的核心参数(线程池的执行原理知道嘛)

线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数

image-20230505220514872

  • corePoolSize 核心线程数目

  • maximumPoolSize 最大线程数目 = (核心线程+临时线程的最大数目)

  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建临时线程执行任务

  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

工作流程

image-20230505220701835

1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行

2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列

3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务

如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务

4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略

拒绝策略:

在这里插入图片描述

24. 线程池中有哪些常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

比较常见的有4个,用的最多是ArrayBlockingQueue和LinkedBlockingQueue

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。

2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。

3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的

4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

ArrayBlockingQueue的LinkedBlockingQueue区别

LinkedBlockingQueueArrayBlockingQueue
默认无界,支持有界强制有界
底层是单向链表底层是数组
是懒惰的,创建节点的时候添加数据提前初始化 Node 数组
入队会生成新 NodeNode需要是提前创建好的
两把锁(头尾)一把锁

左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式

  • LinkedBlockingQueue读和写各有一把锁,性能相对较好
  • ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些

image-20230505221424359

25. 如何确定核心线程数

在这里插入图片描述

  • IO 密集型任务:建议使用 CPU 核数 * 2 的线程数。因为 IO 密集型任务在等待外部资源(如网络、磁盘等)时,CPU 处于空闲状态。增加线程数可以在某些线程等待 I/O 时,其他线程继续执行任务,充分利用 CPU 资源。
  • 计算密集型任务:建议使用 CPU 核数 + 1 的线程数。因为计算密集型任务完全占用 CPU,线程数等于 CPU 核数能最大化资源利用。多一个线程可以在偶尔的线程切换或延迟时,保持 CPU 始终有工作,减少空闲时间。

26. 线程池的种类有哪些

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种

  1. 创建使用固定线程数的线程池

    image-20230505221959259

    • 核心线程数与最大线程数一样,没有救急线程

    • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

    • 适用场景:适用于任务量已知,相对耗时的任务

    • 案例:

      public class FixedThreadPoolCase {static class FixedThreadDemo implements Runnable{@Overridepublic void run() {String name = Thread.currentThread().getName();for (int i = 0; i < 2; i++) {System.out.println(name + ":" + i);}}}public static void main(String[] args) throws InterruptedException {//创建一个固定大小的线程池,核心线程数和最大线程数都是3ExecutorService executorService = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {executorService.submit(new FixedThreadDemo());Thread.sleep(10);}executorService.shutdown();}}
      
  2. 单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

    image-20230505222050294

    • 核心线程数和最大线程数都是1

    • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

    • 适用场景:适用于按照顺序执行的任务

    • 案例:

      public class NewSingleThreadCase {static int count = 0;static class Demo implements Runnable {@Overridepublic void run() {count++;System.out.println(Thread.currentThread().getName() + ":" + count);}}public static void main(String[] args) throws InterruptedException {//单个线程池,核心线程数和最大线程数都是1ExecutorService exec = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {exec.execute(new Demo());Thread.sleep(5);}exec.shutdown();}}
      
  3. 可缓存线程池

    image-20230505222126391

    • 核心线程数为0

    • 最大线程数是Integer.MAX_VALUE

    • 阻塞队列为SynchronousQueue:不存储元素的阻塞队列。

    • 适用场景:适合任务数比较密集,但每个任务执行时间较短的情况

    • 案例:

      public class CachedThreadPoolCase {static class Demo implements Runnable {@Overridepublic void run() {String name = Thread.currentThread().getName();try {//修改睡眠时间,模拟线程执行需要花费的时间Thread.sleep(100);System.out.println(name + "执行完了");} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {//创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUEExecutorService exec = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {exec.execute(new Demo());Thread.sleep(1);}exec.shutdown();}}
      
  4. 提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。

    image-20230505222203615

    • 适用场景:有定时和延迟执行的任务

    • 案例:

      public class ScheduledThreadPoolCase {static class Task implements Runnable {@Overridepublic void run() {try {String name = Thread.currentThread().getName();System.out.println(name + ", 开始:" + new Date());Thread.sleep(1000);System.out.println(name + ", 结束:" + new Date());} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {//按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUEScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);System.out.println("程序开始:" + new Date());/*** schedule 提交任务到线程池中* 第一个参数:提交的任务* 第二个参数:任务执行的延迟时间* 第三个参数:时间单位*/scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);Thread.sleep(5000);// 关闭线程池scheduledThreadPool.shutdown();}}
      

image-20240922135929244

27. 为什么不建议用Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》,可能会导致内存溢出,因为设置的最大线程数为Integer最大值或者阻塞队列没有上限,导致内存溢出Out Of Memory

简单来说,要么是最大线程无穷大,要么是阻塞队列无穷大

image-20220821003816845

28. 线程池使用场景Future

1. 多个服务独立运行

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?

image-20230505223442924

  • 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能

  • 报表汇总

    image-20230505223536657

2. 异步调用(避免下一级方法影响上一级方法:性能考虑)

1. 开启异步功能

首先,仍然需要启用异步功能,和之前一样,在 Spring Boot 的主类上添加 @EnableAsync 注解:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;@SpringBootApplication
@EnableAsync
public class AsyncApplication {public static void main(String[] args) {SpringApplication.run(AsyncApplication.class, args);}
}
2. 创建自定义 ThreadPoolExecutor 线程池

我们将创建 ThreadPoolExecutor 并返回它作为 Executor 接口的实现。我们需要手动配置线程池的核心线程数、最大线程数、队列大小等。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Executor;@Configuration
public class AsyncConfig {@Bean(name = "taskExecutor")public Executor taskExecutor() {// 创建ThreadPoolExecutorreturn new ThreadPoolExecutor(5,  // 核心线程数10, // 最大线程数60, // 线程空闲时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue<>(100), // 任务队列new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略);}
}
3. 使用异步方法

接下来,在需要异步执行的方法上依然使用 @Async 注解,并指定线程池为我们自定义的 taskExecutor

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class AsyncService {@Async("taskExecutor")public void executeAsyncTask() {System.out.println("异步任务开始,线程名称:" + Thread.currentThread().getName());try {Thread.sleep(2000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println("异步任务结束,线程名称:" + Thread.currentThread().getName());}
}
4. 调用异步方法

在控制器中调用异步服务。和之前一样,当访问 /async-task 时,将触发异步任务,任务将在后台线程池中执行。

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class AsyncController {private final AsyncService asyncService;public AsyncController(AsyncService asyncService) {this.asyncService = asyncService;}@GetMapping("/async-task")public String triggerAsyncTask() {asyncService.executeAsyncTask();return "异步任务已经触发";}
}

29. 如何控制某个方法允许并发访问线程的数量?

Semaphore [ˈsɛməˌfɔr] 信号量,是JUC包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果

当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。

Semaphore两个重要的方法

lsemaphore.acquire(): 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)

lsemaphore.release():释放一个信号量,此时信号量个数+1

线程任务类:

public class SemaphoreCase {public static void main(String[] args) {// 1. 创建 semaphore 对象Semaphore semaphore = new Semaphore(3);// 2. 10个线程同时运行for (int i = 0; i < 10; i++) {new Thread(() -> {try {// 3. 获取许可semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}try {System.out.println("running...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("end...");} finally {// 4. 释放许可semaphore.release();}}).start();}}}

30. 谈谈你对ThreadLocal的理解

  • 面试官:谈谈你对ThreadLocal的理解

    • 第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,

    • 第二个是实现了线程内的资源共享

  • 面试官:好的,那你知道ThreadLocal的底层原理实现吗?

    • ThreadLocal的底层原理是基于每个线程都有一个独立的存储区域ThreadLocalMap)来实现的。以下是其核心机制:
      1. ThreadLocalMapThreadLocal的实际数据存储是基于ThreadLocalMap。每个线程对象(Thread)内部都有一个ThreadLocalMap,这个ThreadLocalMap是用来为当前线程存储数据的。
      2. set()方法: 当你调用ThreadLocal.set()时,ThreadLocal对象会作为key,实际的数据作为value,存储在当前线程的ThreadLocalMap中。这个Map的结构是<ThreadLocal, 值>。不同的线程有独立的ThreadLocalMap,因此相同的ThreadLocal对象在不同线程中的值是独立的。
      3. get()方法: 当调用get()时,系统会从当前线程的ThreadLocalMap中,使用ThreadLocal对象作为key查找数据。如果找到key对应的值,则返回该值;如果没有找到,则可以返回初始化的默认值。
      4. remove()方法: 调用remove()会从当前线程的ThreadLocalMap中删除这个ThreadLocal对象对应的键值对,以便释放不再需要的资源,防止内存泄漏。
  • 面试官:那关于ThreadLocal会导致内存溢出这个事情,了解吗?

    • ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。

    • 在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/429487.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

axure的下载,激活,汉化全过程,多图

1.前言 下载地址&#xff1a;https://pan.baidu.com/s/12xo1mJer2hmBK7QrYM5v-Q?pwd0107#list/path%2Fcsdn%E5%85%B1%E4%BA%AB%E6%96%87%E4%BB%B6 源文章&#xff1a;https://blog.csdn.net/iwanttostudyc/article/details/123773796?ops_request_misc%257B%2522request%25…

STM32 单片机最小系统全解析

STM32 单片机最小系统全解析 本文详细介绍了 STM32 单片机最小系统&#xff0c;包括其各个组成部分及设计要点与注意事项。STM32 最小系统在嵌入式开发中至关重要&#xff0c;由电源、时钟、复位、调试接口和启动电路等组成。 在电源电路方面&#xff0c;采用 3.3V 直流电源供…

.Net网络通信组件 - TouchSocket

文章目录 .Net网络通信组件 - TouchSocket1、新建.Net8控制台项目2、Nuget安装TouchSocket组件3、编写服务端代码4、编写客户端代码5、编写Program代码6、运行效果7、日志组件&#xff08;NLog&#xff09;参考我的另一篇博客 .Net网络通信组件 - TouchSocket 1、新建.Net8控制…

PyCharm的使用

PyCharm的入门使用教程 下载和安装PyCharm&#xff1a; 首先&#xff0c;访问JetBrains官方网站&#xff08;https://www.jetbrains.com/pycharm/&#xff09;下载PyCharm的最新版本。根据您的操作系统选择合适的版本进行下载。 安装完成后&#xff0c;打开PyCharm。 创建新…

深度学习03-神经网络02-激活函数

可以使用这个进行跳转链接​​​​​​​http://playground.tensorflow.org/#activationrelu&batchSize11&datasetspiralDatasetreg-gauss&learningRate0.01ularizationRate0.1&noise0&networkShape7,5,4,3,2&seed0.54477&showTestDatafalse&d…

【Unity设计模式】Unity MVC/MVP架构介绍,及MVC/MVP框架的简单应用

文章目录 什么是MVC&#xff1f;MVC眼花缭乱设计图MVP和MVC最经典的MVC的业务流程Unity MVC 框架示例1. 创建项目结构2. 实现模型3. 实现视图4. 实现控制器5. 使用示例 总结参考完结 什么是MVC&#xff1f; MVC自1982年被设计出来&#xff0c;至今都有着很大比重的使用率&…

HCIA--实验十八:配置全局DCHP

一、实验内容 1.需求/要求&#xff1a; 使用一台5700交换机和一台PC,实现全局DHCP的配置&#xff0c;并且自定义配置网关&#xff0c;配置DNS。 二、实验过程 1.拓扑图&#xff1a; 2.步骤&#xff1a; 1.SW1激活DHCP服务&#xff0c;创建vlan10 2.SW1给vlan10添加ip地址 …

Transformer模型-7- Decoder

概述 Decoder也是N6层堆叠的结构&#xff0c;每层被分3层: 两个注意力层和前馈网络层&#xff0c;同Encoder一样在主层后都加有Add&Norm&#xff0c;负责残差连接和归一化操作。 Encoder与Decoder有三大主要的不同&#xff1a; 第一层 Masked Multi-Head Attention: 采用…

Linux 动静态库

目录 一.静态库 1.理解静态库 a.什么是静态库&#xff1f; b.创建静态库的理论&#xff1f; 2.打包静态库 3.静态库的使用方法 a.头文件找不着 b.链接报错——库函数文件找不着 4.将静态库文件写到系统目录下 a.直接拷贝 b.建立软链接 二.动态库 1.什么是动态库&am…

通过标签实现有序:优化你的 FastAPI 生成的 TypeScript 客户端

在软件开发的世界里&#xff0c;API 客户端代码的质量直接影响着应用程序的性能和可维护性。随着项目规模的扩大&#xff0c;自动化生成的代码往往变得臃肿且难以管理。但幸运的是&#xff0c;通过一系列的优化策略&#xff0c;我们可以显著提升这些代码的优雅与效能。在本文中…

C#如何把写好的类编译成dll文件

1 新建一个类库项目 2 直接改写这个Class1.cs文件 3 记得要添加Windows.Forms引用 4 我直接把在别的项目中做好的cs文件搞到这里来&#xff0c;连文件名也改了&#xff08;FilesDirectory.cs&#xff09;&#xff0c;这里using System.Windows.Forms不会报错&#xff0c;因为前…

用Qt 对接‌百度AI平台

很多同学想利用几大模型AI弄点东西&#xff0c;但又不知道如何去介入&#xff1f;&#xff1f;最近帮同学弄点东西&#xff0c;刚好要接入到AI平台&#xff0c;就顺便研究了一下&#xff0c;并记录下来。 首先我们选择的 AI模型是百度的&#xff0c;然后注册&#xff0c;申请密…

8. 尝试微调LLM大型语言模型,让它会写唐诗

这篇文章与03. 进阶指南&#xff1a;自定义 Prompt 提升大模型解题能力一样&#xff0c;本质上是专注于“用”而非“写”&#xff0c;你可以像之前一样&#xff0c;对整体的流程有了一个了解&#xff0c;尝试调整超参数部分来查看对微调的影响。 这里同样是生成式人工智能导论&…

13年计算机考研408-数据结构

解析&#xff1a; 这个降序链表不影响时间复杂度&#xff0c;因为是链表&#xff0c;所以你想要升序就使用头插法&#xff0c;你想要降序就使用尾插法。 然后我们来分析一下最坏的情况是什么样的。 因为m和n都是两个有序的升序序列。 如果刚好m的最大值小于n的最小值&#xff0…

消息中间件---Kafka

一、什么是Kafka&#xff1f; Kafka是一个分布式流处理平台,类似于消息队列或企业消息传递系统&#xff1b; 流处理事什么呢&#xff1f; 流处理就是数据处理工作流&#xff0c;本质上是一种计算机编程范例。流处理是对接收到的新数据事件的连续处理。‌它涉及对从生产者到消…

10年408考研真题-数据结构

23.[2010统考真题]若元素 a,b,c,d,e,f 依次进栈&#xff0c;允许进栈、退栈操作交替进行&#xff0c;但不允许连续3次进行退栈操作&#xff0c;不可能得到的出栈序列是(D)。 A.dcebfa B.cbdaef C.bcaefd D.afedcb 解析&#xff1a; 直接看D选项&#xff0…

Python | Leetcode Python题解之第420题强密码检验器

题目&#xff1a; 题解&#xff1a; class Solution:def strongPasswordChecker(self, password: str) -> int:n len(password)has_lower has_upper has_digit Falsefor ch in password:if ch.islower():has_lower Trueelif ch.isupper():has_upper Trueelif ch.isdi…

微服务保护之熔断降级

在微服务架构中&#xff0c;服务之间的调用是通过网络进行的&#xff0c;网络的不确定性和依赖服务的不可控性&#xff0c;可能导致某个服务出现异常或性能问题&#xff0c;进而引发整个系统的故障&#xff0c;这被称为 微服务雪崩。为了防止这种情况发生&#xff0c;常用的一些…

pytorch实现RNN网络

目录 1.导包 2. 加载本地文本数据 3.构建循环神经网络层 4.初始化隐藏状态state 5.创建随机的数据&#xff0c;检测一下代码是否能正常运行 6. 构建一个完整的循环神经网络 7.模型训练 8.个人知识点理解 1.导包 import torch from torch import nn from torch.nn imp…

Python画笔案例-057 绘制蜘蛛网

1、绘制蜘蛛网 通过 python 的turtle 库绘制 蜘蛛网,如下图: 2、实现代码 绘制蜘蛛网,以下为实现代码: """蜘蛛网.py """ import turtledef draw_circle(pos,r):"""p