当前位置: 首页>后端>正文

3、并发编程

1、Java中线程的实现方式

  • 继承Thread类,重写run方法

  • 实现Runnable接口,重写run方法

  • 实现Callable,重写call方法,配合FutureTask

  • 基于线程池构建线程

  • 线程的原理

    • 均实现了Runnable接口

2、Java中线程的状态

  • 现象上5种

    • 新建:new

    • 就绪:调用start方法

    • 运行:CPU调度线程

    • 等待:wait、sleep、锁

    • 结束

  • Thread里面的state有6种

    • NEW:创建

    • RUNNABLE:调用start和被调用都是这个状态

    • BLOCKED:锁,在锁池

    • WAITING:在等待池

    • TIMED_WAITING:sleep

    • TERMINATED:执行结束

3、Java中如何停止线程

线程结束方式很多,最常用的就是让线程的run方法结束,无论是return结束,还是抛出异常,都可以

  • stop方法(不用,已经过时了)

    • 强制结束
  • 使用共享变量(很少会用)

    • 通过修改共享变量,结束线程里面执行的逻辑,线程也就停止执行了
  • interrupt方式:中断标记,默认false

    • interrupt():将中断标识位修改为true

    • isInterrupted:获取中断状态

    • interrupted:获取并重置中断状态

4、Java中sleep和wait方法的区别

  • sleep属于Thread中的static方法,wait属于Object类的方法

  • sleep属于TIMED_WAITING状态,自动被唤醒,wait属于WAITING,需要手动唤醒

  • sleep方法在持有锁时执行,不会释放锁,wait在执行后,会释放锁资源

  • sleep可以在持有或不持有锁时执行,wait方法必须在持有锁时才可以执行

    • wait方法会将持有锁的线程放到等待池,这个操作需要修改ObjectMonitor对象,该对象是锁对象,只有持有锁时才能操作

5、并发编程的三大特性

  • 原子性:一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到它

    • synchronized

    • CAS

    • Lock锁

  • 可见性:CPU三级缓存导致数据无法及时同步到主内存

    • volatile:强制写到主内存

    • synchronized:加锁时可以保证

    • Lock:加锁时可以保证

    • final:数据无法变更

  • 有序性:指令重排导致

    • volatile:添加内存屏障

    • happens-before

6、什么是CAS?有什么优缺点?

  • compare and swap:先查看内存中的值是否与预期的值一致,如果一致,执行替换操作。这个操作是一个原子性操作。

  • 优点:整个过程一直在循环,没有用户态切换内核态

  • 缺点:

    • 如果并发量大,自旋时间过长,会消耗CPU资源

      • 可以指定CAS一共循环多少次,如果超过,直接失败或者挂起线程
    • 会产生ABA问题:线程修改数据前,该数据已经被其它线程从A修改为B,又从B修改成A,符合条件,但是当前的数据已经不是之前该线程读取的数据了

      • 可以通过添加版本号来解决,修改前对比版本号

7、@Contended注解有什么用?

  • 解决伪共享问题:缓存行中可能存在其它线程的值,如果修改了,需要重新去主存中查,这个过程影响性能

  • 会将当前类中的属性独占一个缓存行,从而避免缓存行失效造成的性能问题

8、Java中的四种引用类型

  • 强:最常见,强引用的对象不会被垃圾回收机制回收

    • 是造成java内存泄漏的主要原因
  • 软:系统内存足够时,不会被回收;一般作为缓存使用

    • SoftRefrence创建
  • 弱:只要垃圾回收机制运行,必然回收

  • 虚:不能单独使用,必须和引用队列联合使用。主要作用是跟踪对象被垃圾回收的状态

9、ThreadLocal的内存泄漏问题

  • 原理

    • 每个Thread对象内部会存储一个成员变量,ThreadLocalMap

    • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap

    • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。

    • 每一个线程都有自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取

    • ThreadLocalMap的key是一个弱引用,每次GC必然会被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,对导致ThreadLocal对象无法被回收

  • ThreadLocal内存泄漏问题

    • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到

    • 只需要在使用完毕ThreadLocal对象后,及时的调用remove()方法,移除Entry即可

10、Java中锁的分类

  • 可重入锁、不可重入锁

    java中提供的synchronized、ReentrantLock等都是可重入锁

    • 重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的

    • 不可重入:当线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到,因为A锁被当前线程占用着,需要等待自己释放锁再次获取锁

  • 乐观锁、悲观锁

    java中提供的synchronized、ReentrantLock等都是悲观锁

    java中提供的CAS操作,就是乐观锁的一种实现

    • 悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED,WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的

      • 用户态:JVM可以自行执行的指令,不需要借助操作系统执行。

      • 内核态:JVM不可以自行执行,需要操作系统才可以执行。

    • 乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源

  • 公平锁、非公平锁

    Java中提供的synchronized只能是非公平锁

    Java中提供的ReentrantLock、ReentrantReadWriteLock可以实现公平锁和非公平锁

    • 公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队,C直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

    • 非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波

      • 拿到锁资源:插队成功

      • 没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者B取消后,才可以尝试去竞争锁资源。

  • 互斥锁、共享锁

    Java中提供的synchronized、ReentrantLock是互斥锁

    Java中提供的ReentrantReadWriteLock,有互斥锁也有共享锁

    • 互斥锁:同一时间点,只会有一个线程持有着当前互斥锁

    • 共享锁:同一时间点,当前共享锁可以被多个线程同时持有

11、synchronized在JDK1.6中的优化

  • 锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,即使写了synchronized,也不会触发

  • 锁膨胀:如果在一个循环中,频繁的获取和释放锁资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。

  • 锁升级:ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在1.6之前,如果获取不到锁,立即挂起当前线程,所以synchronized性能比较差。JDK1.6之后对synchronized做了锁升级的优化

    • 无锁、匿名偏向:当前对象没有作为锁存在。

    • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断当前指向的线程是否是当前线程。

      • 如果是,直接拿着锁资源走

      • 如果当前线程不是,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级成轻量级锁。(偏向锁状态出现了锁竞争的情况)

    • 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)

      • 如果成功获取到,拿着锁资源走

      • 如果自旋了一定次数,没有拿到锁资源,锁升级为重量级锁

    • 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程

12、synchronized实现原理

synchronized是基于对象实现的,存在于对象头的MarkWord中

  • 无锁的地址指针为空

  • 偏向锁地址指针指向当前线程

  • 轻量级锁指向线程栈中LockRecord的指针

  • 重量解锁指向互斥量(ObjectMonitor)的指针

  • ObjectMonitor:该对象存储了当前锁的所有相关信息

    • MarkWord

    • 竞争锁的线程个数

    • wait的线程个数

    • 当前线程重入锁次数

    • 当前线程

    • 等待池(wait状态)

    • 等待获取资源的线程队列

13、什么是AQS?

AQS就是AbstractQueueSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier扽等都是基于AQS实现的

  • 首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。

  • 其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象

14、AQS唤醒节点时,为何从后往前找

  • 从前往后找,可能会错过某个节点

  • 场景一:某个线程节点插入队列

    • 先把当前节点的prev指向队列的最后一个节点

    • 然后把队列的最后一个节点指向当前节点

    • 此时前一个节点的下一个节点还没有指向当前节点,极端情况下会从前往后找会漏掉当前节点

  • 场景二:某个节点取消

    • 先把下一个节点的prev指向当前节点的上一个节点

    • 此时如果从前往后,由于上一个节点的下一个节点没有指向下一个节点,会漏掉后面的节点

15、ReentrantLock和synchronized的区别

  • 使用层面

    • synchronized:锁的是对象,无需释放资源

    • ReentrantLock:锁需要创建锁对象,锁的是代码块,需要释放资源

  • 原理层面

    • synchronized:是关键字,有锁升级概念,锁升级之后不能降级,重量级锁底层基于ObjectMonitor对象,只支持非公平

    • ReentrantLock:是类,基于AQS,支持公平和非公平;因为是类,所以能实现的功能更全面

16、ReentrantReadWriteLock的实现原理

读写锁:读操作并行,写操作互斥

  • 基于state操作

    • 读锁操作:基于state的高16位

    • 写锁操作:基于state的低16位

  • ReentrantReadWriteLock是可重入锁

    • 写重入锁:读写锁中的重入方式,基本和ReentrantLock一致,没什么区别,依然是对state进行+1,只要确认持有锁资源的线程,是当前写锁线程即可。唯一区别是state范围减小

    • 读重入锁:因为读锁是共享锁。读锁再获取锁资源操作时,是要对state的高16位进行+1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁的重入次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数

    • 写锁的饥饿问题:读锁时共享锁,当有线程持有读资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。

      • 所以,读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面有需要写锁资源的线程,那么后续的读线程时无法拿到锁资源的。持有读锁的线程,指挥让写锁线程之前的读线程拿到锁资源

17、JDK中提供了哪些线程池?

  • newFixedThreadPool

    • 线程数固定,都是核心
  • newSingleThreadExecutor

    • 只有一个核心线程
  • newCachedThreadPool

    • 0核心,线程先入队列
  • newScheduleThreadPool

    • 定时任务线程池,延迟或周期性执行
  • newWorkStealingPool

    • 分治

    • 每个线程都有自己的阻塞队列

    • 如果当前线程队列里面都执行完了,会去获取其它线程的任务

18、线程池的核心参数有什么?

  • corePoolSize:核心线程数

  • maximumPoolSize:最大工作线程数

  • keepAliveTime:非核心线程在阻塞队列位置等待时间

  • unit:非核心工作线程在阻塞队列位置等待时间的单位

  • workQueue:任务在没有核心工作线程处理时,任务先扔到阻塞队列中

  • threadFactory:构建线程的线程工厂,可以设置thread的一些信息

  • handler:拒绝策略,核心、队列、工作线程都满了

    • AbortPolicy:抛异常

    • CallerRunsPolicy:将任务交给调用者处理

    • DiscardPolicy:将任务丢弃

    • DiscardOldestPolicy:将队列中最早的任务丢弃,将当前任务尝试交给线程池处理

    • 自定义Policy:根据业务,可以将任务扔到数据库,也可以做其它操作

19、线程池的状态?

  • AtomicInteger属性

    • 高3位:线程池状态

    • 低29位:线程数

  • 状态

    • RUNNING(-1):线程池new出来以后就是RUNNING

    • SHUTDOWN(0)(shutdown()):不会接收新任务,正在处理的任务正常进行,阻塞队列的任务也会做完

    • STOP(1)(shutdownNow()):不会接收新任务,正在处理任务的线程会被中断(中断标识位interrupt),阻塞队列的任务一个都不管

    • TIDYING(2):一个过渡的状态,提供了一个扩展方法,在线程处理完之后执行

    • TERMINATED(3):线程池没了

20、线程池的执行流程

  • 描述

    • 添加任务到线程池

    • 判断是否能获取或者创建核心线程,可以就正常执行,不能就扔进队列

    • 判断是否能获取或者创建非核心线程,可以就执行,不行就走拒绝策略

  • 需要判断的点

    • 任务是否为null

    • 核心线程是否可以添加

    • 线程池状态是否正常

    • 非核心线程是否可以添加

21、线程池添加工作线程的流程

主要是addWorker方法

  • 校验线程池的状态以及工作线程个数

  • 添加工作线程并且启动工作线程

22、线程池为何要构建空任务的非核心线程

  • 场景:阻塞队列有任务,没有工作线程

  • 原因:

    • 核心线程数为0

    • 核心线程手动修改了超时参数,正好超时了

23、线程池使用完毕为何必须shutdown()?

核心线程无法回收,核心线程创建指向了线程池内部类Worker,所以导致整个线程池无法被回收,导致内存泄漏

  • SHUTDOWN(0)(shutdown()):不会接收新任务,正在处理的任务正常进行,阻塞队列的任务也会做完

24、线程池的核心参数如何设置?

  • 核心线程数:这个最重要,做压测调整

  • 阻塞队列

  • 拒绝策略

25、ConcurrentHashMap在1.8中做了什么优化

  • 存储结构:数组+链表+红黑树

  • 存储操作:CAS+synchronized

    • 主要是针对数组的位置,如果数组当前位置为null,用CAS写node,如果不为null,用synchronized
  • 扩容:多线程扩容

  • 线程安全、弱一致

26、ConcurrentHashMap扩容的流程

  • 1.7

    • 基于Segment分段:每个分段相当于一个Map(只hash一次),所以扩容是类似Map扩容,每个Segment单独判断是否扩容
  • 1.8

    • 基于Node:多线程扩容,不同线程负责不同的Node节点进行扩容

27、ConcurrentHashMap读取数据的流程

  • 先查数组,再查链表或红黑树

  • 如果正在扩容,查新数组或者扩容时保留的双向链表


https://www.xamrdz.com/backend/3r51945690.html

相关文章: