【高频面试题】多线程篇

文章目录

一、线程的基础知识

1.线程与进程的区别

程序由指令数据组成,这些指令要运行,数据要读写,就必须将指令加载至 CPU数据加载至内存

当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

二者对比

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

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

在这里插入图片描述

现在都是多核CPU,在多核CPU下

  1. 并发同一时间应对多件事情的能力多个线程轮流使用一个或多个CPU
  2. 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

3.创建线程的方式有哪些?

  1. 继承Thread类
    在这里插入图片描述

  2. 实现runnable接口
    在这里插入图片描述

  3. 实现Callable接口
    在这里插入图片描述

  4. 线程池创建线程(项目常用)
    在这里插入图片描述


3.1.Runnable 和 Callable 有什么区别?

参考回答:

  • Runnable 接口run方法没有返回值
  • Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化(try catch),不能继续上抛
    **加粗样式**

3.2.run()和 start()有什么区别?

  • 方法性质不同:run 是一个普通方法,而 start 是开启新线程的方法
  • 执行速度不同:调用 run 方法立即执行任务,调用 start 方法是将线程的状态改为就绪状态,不会立即执行。(调用start()方法的时候线程不是立即执行,而是进入到一个等待状态,等待CPU进行调度)
  • 调用次数不同:run 方法可以被重复调用,而 start 方法只能被调用一次。
  • start 方法之所以不能被重复调用的原因是,线程的状态是不可逆的,Thread 在 start的实现源码中做了判断,如果线程不是新建状态 NEW,则会抛出非法线程状态异常 IllegalThreadStateException。 ​

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

4.1.线程包括哪些状态?

新建(NEW)、
可运行(RUNNABLE)、
阻塞(BLOCKED)、
等待( WAITING )、
时间等待(TIMED_WALTING)、
终止(TERMINATED)

4.2.状态之间是如何变化的?

  • 创建线程对象是新建状态
  • 调用了start()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
    其他状态:
    如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
    如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
    如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
    在这里插入图片描述

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

可以使用线程中的join方法解决

t.join()
阻塞调用此方法的线程进入timed_waiting
直到线程t执行完成后此线程再继续执行
在这里插入图片描述


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

notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个 wait 线程


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

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

  1. 方法归属不同

sleep(long) 是 Thread 的静态方法
而 wait(),wait(long) 都是 Object 的成员方法每个对象都有

  1. 醒来时机不同

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

  1. 锁特性不同(重点)

wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)

也就是只要执行到wait()方法不管wait方法有没有执行完都会立即释放锁,别的线程就可以拿到锁进而执行自己的业务。

而 sleep 如果在 synchronized 代码块中执行并不会释放对象锁(我放弃 cpu,你们也用不了)

也就是只有sleep()方法执行结束了才会去释放锁,别的线程才能拿到锁,执行自己的业务


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

有三种方式可以停止线程

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

也就是给线程配置一个退出运行状态标志,能让run方法执行完成后正常退出

在这里插入图片描述

  1. 使用stop方法强行终止(不推荐,方法已作废)
    在这里插入图片描述

  2. 使用interrupt方法中断线程(原理和第一种是一样的)

打断阻塞的线程( sleep,wait,join )的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态来标记是否退出线程

current.isInterrupted()默认是false
只有在线程调用interrupt();方法才会将current.isInterrupted()设置为true
在这里插入图片描述


2.线程中并发安全

9.synchronized关键字的底层原理

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

synchronized锁是基于monitor锁实现的
例如这一段代码加锁之后进行反编译查看class字节码信息:
在这里插入图片描述
在这里插入图片描述

synchronized锁修饰方法代码块时底层实现上是一样的,但是在修饰方法时,不需要JVM编译出的字节码完成加锁操作,而synchronized在修饰代码块时,是通过编译出来的字节码生成的monitorenter和monitorexit指令来实现的。

Monitor(监视器):结构包括三部分

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

  1. Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  2. EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  3. WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
    在这里插入图片描述
    可参考链接【Java并发】synchronized关键字的底层原理-锁的升级

10.谈谈 JMM(Java内存模型)

JMM(Java Memory Model)Java内存模型,定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性

通俗的来说,就是保证不同的线程对共享内存的值进行改变是的透明的(也就是当两个线程同时拿到共享内存的一个变量,其中一个线程对变量进行了更改,那么此时就会和共享内存做一个同步操作,然后共享内存会对所有拿到这个变量的线程做出一个同步操作)

在这里插入图片描述
总结:

  1. JMM了共享内存中多线程程序读写操作的行为规范,保证读写指令的正确性
  2. JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  3. 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存

11.什么是CAS?

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

CAS 底层实现
依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令
在这里插入图片描述
乐观锁和悲观锁

  • CAS
    是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,改了反正要同步到主内存的,别的线程CAS失败可以进行自旋在拷贝一份主内存的共享变量数据,再执行自己的业务。
  • synchronized
    是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

总结:

  • CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
  • CAS使用到的地方很多:AQS框架、AtomicXXX类
  • 在操作共享变量的时候使用的自旋锁,效率上更高一些
  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

CAS缺点:

  1. ABA问题:

如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了

ABA问题的解决思路就是使用版本号。

在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

  1. 循环时间长开销大:

如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大且没必要的开销。

可以破坏掉for死循环,当超过一定时间或者一定次数时,return退出。

  1. 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并一个共享变量来操作。


12.请谈谈你对 volatile 的理解

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

12.1.保证线程间的可见性

用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改另一个线程可见
例如下面的代码:

在这里插入图片描述
解决方案一(不推荐):在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)

解决方案二:在修饰stop变量的时候加上volatile,当前告诉 jit不要对 volatile 修饰的变量做优化

12.1.禁止进行指令重排序

用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
例如下面的代码在并发环境中,由于存在多个线程同时访问共享变量的情况,可能会导致可见性问题和指令重排等影响程序正确性的行为。
在这里插入图片描述
解决办法就是在变量上添加volatile禁止指令重排序,则可以解决问题
在这里插入图片描述
当然也不能随便加volatile关键字,要根据实际情况去加。如果都禁用了,指定效率肯定不高。


13.什么是AQS?

全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架

所谓抽象,其实目的就是把具体的逻辑交给子类去实现,这样就可以实现不同的特性的锁:
例如:AQS常见的实现类
ReentrantLock 阻塞式锁
Semaphore 信号量
CountDownLatch 倒计时锁

AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
在AQS内部还有一个属性state,这个state就相当于是一个资源默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
参考链接:【Java并发】什么是AQS?


14.ReentrantLock的实现原理

ReentrantLock表示支持重新进入的锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞
ReentrantLock主要利用CAS+AQS队列来实现
支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁

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

参考链接:【Java并发】ReentrantLock的实现原理


15.synchronized和Lock有什么区别 ?

语法层面
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现(API)
使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁

功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如
公平锁(参考ReentrantLock)
可打断
在这里插入图片描述在这里插入图片描述

可超时
在这里插入图片描述

也就是使用tryLock()时加入时间参数,如果超过这个时间拿不到锁,就自动放弃抢锁(放弃阻塞),反之在规定时间能抢到锁,那就正常执行自己的逻辑

多条件变量
在这里插入图片描述

Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)

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


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

产生的四个必要条件如下:(缺一不可)

  1. 互斥条件:一个资源同一时间能且只能被一个线程访问;
  2. 不可掠夺:当资源被一个线程占用时,其他线程不可抢夺该资源;
  3. 请求与等待:当资源被一个线程占用时,其他线程只能等待资源的释放再拥有;
  4. 循环等待:指的是若干线程形成头尾相接的情况,将所有资源都占用导致的整体死锁或局部死锁。

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

前三条其实就是作为锁的条件,第四条(循环等待)就是造成死锁的主要原因

循环等待也就是双方的锁都锁住了对方,并且都在等待对方的解锁,造成死循环(类似springbean的循环依赖)
在这里插入图片描述
如何进行死锁诊断?

  1. 命令查看(jps+jstack)
  2. jconsole工具
  3. VisualVM:故障处理工具

参考链接:【Java并发】如何进行死锁诊断?


17.聊一下ConcurrentHashMap

ConcurrentHashMap 是一种线程安全的高效Map集合
底层数据结构:

  • JDK1.7底层采用分段数组+链表实现
  • JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。

JDK1.7中ConcurrentHashMap:
采用分段数组+链表实现
在这里插入图片描述
Segment数组的每一个元素都存储这一个HashEntry 数组的地址值,并且segment数组定义好了就不能扩容了。

向ConcurrentHashMap添加元素的流程
在这里插入图片描述
底层使用了ReentrantLock锁保证并发下的线程安全。

但是这种方式效率并不高,每一次添加元素进去都要枷锁,解锁。效率不高

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

  1. 采用 CAS + Synchronized来保证并发安全进行实现
  2. CAS控制数组节点的添加(CAS操作保证一个共享变量的原子操作
  3. synchronized只锁定当前链表红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题 , 效率得到提升
    在这里插入图片描述
    总结:
    1. 底层数据结构:
    JDK1.7底层采用分段的数组+链表实现
    JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
    2. 加锁的方式
    JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
    JDK1.8采用CAS添加新节点,采用synchronized锁定链表红黑二叉树的首节点,相对Segment分段锁粒度更细性能更好

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

Java并发编程三大特性

1. 原子性(加锁)
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
在这里插入图片描述

  1. 可见性(共享变量加volatile关键字)
    内存可见性:让一个线程对共享变量的修改对另一个线程可见
    在这里插入图片描述

  2. 有序性(共享变量加volatile关键字)—会在读、写共享变量时加入不同的屏障
    指令重排:指令重排虽然在单个线程内保持了语义一致性,但在多线程环境下,并发读写操作的顺序可能被改变,从而引发问题。例如,在多线程中对共享变量进行读取和写入操作时,如果指令重排改变了读取和写入的顺序,并且没有适当的同步机制来保证顺序性,就可能导致线程读取到失效的数据或产生不符合预期的结果。
    在这里插入图片描述


3.线程池

资源管理:线程池可以有效地管理系统中的线程资源。线程创建和销毁的开销比较大,如果在每个任务执行时都手动创建和销毁线程,会产生较高的开销。而线程池可以在程序启动时预先创建一定数量的线程,并对其进行复用,避免了线程频繁的创建和销毁,从而提高了系统性能和资源利用率。

控制线程数量:线程池可以限制同时执行的线程数量,可以通过设置线程池的大小来控制并发度。这样可以避免线程数量过多导致系统负载过重,以及线程数量过少导致资源浪费。线程池会自动管理线程的调度和执行,保证线程数量在合理范围内。

提高响应速度:线程池可以提高任务的响应速度。当有新的任务到达时,线程池中的空闲线程可以立即执行任务,而不需要等待新线程的创建。这样可以降低任务的等待时间,提高整体的响应性能。

避免资源竞争:线程池可以避免由于资源竞争而引起的性能问题。线程池可以通过适当的同步机制来管理共享资源的访问,避免多个线程同时对共享资源进行修改而导致的竞争和冲突。

统一管理和监控:线程池可以提供统一的管理和监控接口,方便对线程的状态、执行情况、异常处理等进行统一管理和监控。可以通过线程池的API来获取线程池中线程的状态或取消执行中的任务等操作。

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

在这里插入图片描述

在这里插入图片描述

  • corePoolSize 核心线程数目
  • maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队队列满会创建救急线程执行任务
  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
  • 拒绝策略

1.AbortPolicy:直接抛出异常,默认策略
2.CallerRunsPolicy:用调用者所在的线程来执行任务;(调用主线程来完成任务)
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前(待在队列最久的任务)的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;

线程池线程执行流程:
在这里插入图片描述

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

workQueue - 当没有空闲核心线程时新来任务会加入到此队列排队队列满会创建救急线程执行任务
阻塞队列有四种:(1,2常用,3,4了解即可)
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO(先进先出)。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

ArrayBlockingQueue的LinkedBlockingQueue区别其实也就是数组和链表的区别以及有界无界和出入队列锁的数量不同
在这里插入图片描述
注意:

  1. LinkedBlockingQueue建议一般设置有界(虽然也可以无界,但不推荐)

在这里插入图片描述

  1. LinkedBlockingQueue他的出队和入队是两把不同的锁,相比ArrayBlockingQueue一把锁。灵活度高

在这里插入图片描述

  1. LinkedBlockingQueue因为是链表,只有在任务加入到队列时才会创建node任务节点。而ArrayBlockingQueue在队列创建之初就初始化长度了。

21.如何确定核心线程数

CPU核数指的是物理核心的数量,而线程数包括物理核心和虚拟核心的总数。在多核处理器中,线程数通常等于核心数,而在带有超线程技术的处理器中,线程数可以大于核心数。

高并发、任务执行时间短 ( CPU核数+1 ),减少线程上下文的切换

并发不高、任务执行时间长

IO密集型的任务 (CPU核数 * 2 + 1)(java程序通常是这种)
计算密集型任务 ( CPU核数+1 )

并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)

22.线程池的种类有哪些

  1. newFixedThreadPool(固定线程数线程池):创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
    在这里插入图片描述
    适用于任务量已知,相对耗时的任务

无需救急,人人有份,相当于KFC知道今天会有多少个顾客来点餐,那KFC事先备好餐品的数量。

  1. newSingleThreadExecutor(单线程化线程池):创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行

在这里插入图片描述适用于按照顺序执行的任务

KFC只有一个窗口,排队取餐,来一个人点完单不做,先滚后面排队去,到你了才给你做餐

  1. newCachedThreadPool(可缓存线程池):创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
    在这里插入图片描述
    适合任务数比较密集,但每个任务执行时间较短的情况

KFC有0元领鸡腿活动,鸡腿做很快,来一个单先做好这个单的鸡腿,在接下一个单。并且做鸡腿的都是临时员工

  1. newScheduledThreadPool(“延迟”和“周期执行”):可以执行延迟任务的线程池,支持定时及周期性任务执行

相当于提前预定,到了预定时间就去做餐,或者接到单了,但是先不做,可以摸鱼晚点做

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

参考阿里开发手册《Java开发手册-嵩山版》
在这里插入图片描述
其实就是,使用Executors创建的线程池要么就是阻塞队列太长了,要么就是允许创建的线程数量最大化,都会导致堆积问题,导致堆内存溢出错误(OMM)

推荐使用(根据实际业务情况,定制化线程池)
使用 ThreadPoolExecutor 类来手动创建线程池,并根据实际需求进行配置,以更好地控制线程池的行为。通过自定义线程池的参数,例如核心线程数、最大线程数、队列容量和拒绝策略等,可以更好地适应不同的业务场景,并避免上述潜在问题。
在这里插入图片描述


4.使用场景

24.线程池使用场景(你们项目中哪里用到了线程池)

  • 批量导入:使用了线程池+CountDownLatch批量把数据库中的数据导入到了ES(任意)中,避免OOM
  • 数据汇总:调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
  • 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间

24.1.多线程使用场景一( es数据批量导入)

CountDownLatch原理
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行

  1. 其中构造参数用来初始化等待计数值
  2. await() 用来等待计数归零
  3. countDown() 用来让计数减一
    在这里插入图片描述
    通俗的来说,就是线程每处理一个任务就将计数值-1(调用countDown()方法建议),直到计数值减为0,此时调用await()方法的线程才会往下执行。

例如 es数据批量导入
现在我需要将50w的数据从数据库同步到es索引库中,一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多防止内存溢出
在这里插入图片描述
在这里插入图片描述
通俗的来说,比如要将数据库50w的数据导入到es索引库中,一次性导入肯定不行(omm),那么可以通过线程池搭配CountDownLatch来分批次导入es,提前固定每页条数2000条,计算50w的数据一共有多少页,设置CountDownLatch的计数值为总页数,然后从线程池拿到核心线程一页2000的数据(分批次)的导入到ES当中,然后循环处理,直到计数值减为0(页数),说明已经导入完成。

24.2.多线程使用场景二(数据汇总)

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

一般的处理方式就是让一个线程去按顺序查询订单、商品、物流信息,然后再封装到一起返回在这里插入图片描述
这样存在一个问题就是耗时等于微服务执行总长。

若采用线程池,采用多线程异步的方式,则耗时就等于耗时最长的那一个微服务。
在这里插入图片描述
实际场景例如:报表汇总
四个模块代表四个微服务,可以通过线程池+future(线程中可以使用future对象的get方法可以得到每一个线程的返回结果)获得每一个微服务返回的数据,在整体封装到一起汇总返回
在这里插入图片描述

在这里插入图片描述

24.3.多线程使用场景三(异步调用)

案例:

在搜索文章的时候,保存用户的历史搜索记录

在这里插入图片描述

这个时候,在搜索文章的时候,只需要正常返回文章数据,而保存用户历史搜索记录这个事情交给线程池异步开启一个线程去做。

所以在调用这个保存历史记录的方法,会在调用的时候,从线程池中开启一个线程去异步完成任务,

这样整体的效率会变的更好(用户在搜索的时候只关心搜索到的内容)
在这里插入图片描述

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

Semaphore 信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量
使用场景:
通常用于那些资源有明确访问数量限制的场景,常用于限流

举例:停车场常只有3个车位,每停一辆车,车位-1,全部停满后,别的车就不能停了,只能别的车开走了,才能往里面停,也就达到限流的效果。
在这里插入图片描述
代码:

在这里插入图片描述
总结:
在多线程中提供了一个工具类Semaphore信号量。在并发的情况下,可以控制方法的访问量
创建Semaphore对象,可以给一个容量
acquire()可以请求一个信号量,这时候的信号量个数-1
release()释放一个信号量,此时信号量个数+1

26.谈谈你对ThreadLocal的理解

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享,也就是ThreadLocal使得线程与线程产生了隔离,互不影响

在这里插入图片描述

实现原理和内存泄露问题参考链接:【Java并发】ThreadLocal的实现原理&源码解析

总结:

  1. ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
  2. ThreadLocal 同时实现了线程内的资源共享
  3. 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
    a)调用 set 方法,就是以 ThreadLocal 自己作为 key资源对象作为 value,放入当前线程的 ThreadLocalMap 集合
    b)调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
    c)调用 remove 方法,就是以 ThreadLocal 自己作为 key移除当前线程关联的资源值
  4. ThreadLocal内存泄漏问题
    ThreadLocalMap 中的 key 是弱引用值为强引用key 会被GC 释放内存,关联 value 的内存并不会释放。建议主动 remove 释放 key,value

更新中-------------
参考来自黑马程序员

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值