Java多线程面试重点-1

0. 什么是并发?什么是并行?

  • 并发:把时间分成一段一段,每个线程轮流抢占时间段。 如果时间段非常短,线程切换非常快,被称为伪并行。
  • 并行:多个线程可以同时运行。

并发与并行造成的影响?

多线程问题:线程安全,死锁等

1. 如何预防死锁?

先说明死锁发生的四个必要条件:

  • 互斥条件:同一时间只能有一个线程获取资源(因为资源有限)。
  • 不可剥夺条件:一个线程已经占有的资源,在释放之前不会被其它线程抢占。(@&@20230130)
  • 请求和保持条件:线程等待过程中不会释放已占有的资源。
  • 循环等待条件:多个线程互相等待对方释放资源。

死锁预防,破坏这四个必要条件:

  • 由于资源互斥是资源使用的固有特性,无法改变,我们不讨论
  • 破坏不可剥夺条件:
    • 一个进程不能获得所需要的全部资源时就进入等待状态,此时释放调自己持有的资源,让其它进程使用。再等待结束时,重新获取原有资源。
  • 破坏请求和持条件:
    • 方法一:静态分配,即每个进程在开始执行时就申请它所需要的全部资源。
    • 方法二:动态分配,即每个进程在申请所需要的资源时,它本身不占用系统资源。
  • 破坏循环等待条件:
    • 采用资源有序分配,将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

2. 多线程有哪几种创建方式?

  • extend Thread Override run()
  • implement Runnable Override run() and new Tread(Runnable r)
  • implement Callable Override call() and FutureTask
  • ThreadPool
  • new Tread(lambda).start()

3. 描述一下线程安全活跃态问题,竞态条件?(@&@)

活跃性问题可以分为:死锁、活锁、饥饿

活锁:**

  • 概念:线程没有被阻塞,由于某些条件没有满足,导致一直重复尝试。
  • 场景1:异步的消息队列就有可能造成活锁。在消息队列的消费端,如果没有正确的ack消息,并且执行过程中报错了,就会再次放回到消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。
  • 场景2:递归没有写退出条件。
  • 解决:尝试等待一个随机的时间就可以,会按时间轮去重试。

饥饿:**

  • 概念:线程因无法访问所需资源,而无法执行下去的情况。
  • 两种情况:
    • 线程在临界区做了无限循环或无限制等待资源的操作,让其它的线程一直不能拿到锁进入临界区,对其它线程来说,就进入了饥饿状态。(现在理解了20230130)
    • 线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源,而一直无法执行。
  • 场景:读写锁,一直在读,写锁一直等待。
  • 解决:
    • 保证资源充足,很多场景下,资源的稀缺性无法解决。
    • 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源。
    • 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短。

死锁:

  • 概念:线程在对同一把锁进行竞争时,未抢占到锁的线程会等待持有锁的线程释放锁后继续抢占,如果两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁。

竞态条件:**

  • 概念:
    • 同一个程序的多线程访问同一个资源,如果对资源的访问顺序敏感,就称存在竞态条件。
    • 代码区成为临界区。
    • 与大多数并发错误一样,竞态条件不总是会产生问题,还需要不恰当的执行时序。
  • 最常见的竞态条件:
    • 先检测后执行的操作,执行依赖于检测结果,而检测结果依赖于多个线程的执行时序,多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种的问题。一种可能的解决办法就是:在一个线程修改访问一个状态时,要防止其他线程访问修改,也就是加锁机制,保证原子性。
    • 延迟初始化(典型的赖汉单例)

4. 描述一下进程与线程区别?

进程(Process)是系统进行资源分配和调度(管理)的基本单位,进程是资源分配的最小单位,是程序一次动态执行的过程。

线程:是执行的最小单位,一般不具有资源。线程是由进程产生的。

5. 描述一下Java线程的生命周期?

总:

Java线程的生命周期有5种状态,分别是:新建、就绪、运行、阻塞和销毁。在Linux底层的线程只有:运行、阻塞和销毁。

Java线程的生命周期:

  • 新建:new出来的线程。
  • 就绪:调用线程的start()后,线程处于等待CPU分配资源的阶段,谁先抢到CPU资源,谁开始执行。
  • 运行:当就绪的线程被调度,并获得CPU资源时,便进入运行状态,run()定义了线程的操作和功能。
  • 阻塞:线程调用 sleep()、wait()就处于了阻塞状态,处于阻塞状态的线程需要调用某种机制唤醒,比如调用notify()、notifyAll()。唤醒的线程不会立刻执行run(),它们要再次等待CPU分配资源进入运行状态。
  • 销毁:线程正常执行完毕后、线程被提前强制性的终止、出现异常导致结束,那么线程就要被销毁,释放资源。

Linux底层线程三种状态:**

运行、阻塞、销毁。JVM是虚拟出来的一套操作系统,在创建Thread对象时,还没有调用Linux的线程,所以新增了一个新建状态。调用Thread.start()时,调用Linux的线程,但是不能立即执行,需要等待CPU分配资源,所以有了就绪状态。

JDK的源码的Thread状态(6种):

  • NEW:尚未启动的线程的线程状态
  • RUNNABLE:处于可运行状态的线程,它正在JVM中执行,但它可能正在等待来自操作系统(例如处理器)的其他资源。它有细分READY (准备状态)和RUNNING (可运行状态)。
  • BLOCKED:被阻塞,正在等待着锁。
  • WAITING: 等待被唤醒。例如:在对象上调用Object.wait()的线程等待另一个线程调用Object.notify()或Object.notifyAll();调用Thread.join()的线程正在等待指定的线程终止。
  • TIMED_WAITING : 具有指定等待时间的等待线程的线程状态。使用如下:
    • Thread.sleep(long)
    • Object.wait(long)
    • Thread.join(long)
    • LockSupport.parkNanos(long...)
    • LockSupport.parkUntil(long...)
  • TERMINATED:线程已完成执行。

6. 线程池设置多少线程数合适?

首先确认业务是CPU密集型还是IO密集型的,根据不同情况去判断。

CPU密集型程序:

  • 特点:I/O操作时间短, CPU运算处理多。
  • 单核CPU:处理CPU密集型程序,不太适合多线程。
  • 多核 CPU:处理CPU密集型程序,最大化的利用CPU核心数,应用并发编程来提高效率。
    • 理论上:线程数量 = CPU 核数(逻辑)
    • 实际上:线程数量 = CPU 核数(逻辑)+ 1(经验值)
      • 如果恰好某一个线程出问题而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

I/O 密集型程序:

  • 特点: I/O 操作占比很大部分,等待时间较长。线程等待时间所占比例越高,需要越多线程。
  • 公式:CPU 核数 / (1 - 阻塞系数);阻塞系数 在 0.8 ~ 0.9 之间。
  • 一般是:线程数量 = 2*CPU核数 + 1
  • 实际上:在2N+1上,进行压测逐步添加

实际情况看压测:

看系统负载:-> Linux使用uptime命令 -> 系统负载与CPU核心数量有关,理想情况下,一个核心被一个进程占用。如果4个核心,跑4个进程,此时Load是4但是也不高。

7. wait()和sleep()的区别与联系?

区别:

  • 所属类:wait()是Object的方法,sleep()是Thread的方法(拓展一下为什么在Object、Thread中)。
  • 作用范围:sleep没有释放锁,只是休眠,而wait释放了锁后等待,使得其他线程可以使用同步控制块或方法。
  • 使用范围: wait、notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
  • 异常范围:sleep必须捕获异常,而wait,notify和notifyAll不需要强制地捕获异常。

联系:

sleep、wait调用后都会暂停当前线程并让出CPU的执行时间,并且都能被唤起。

8. 描述一下notify()和notifyAll()区别?

  • notifyAll():唤醒所有,notify():随机唤醒一个。
  • 使用流程:
    • 线程调用了对象的wait(),线程进入该对象的等待池中,等待池中的线程,不会去竞争该对象的锁。
    • 如果其它线程调用了对象的notifyAll()或notify(),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。竞争成功则继续执行。

拓展:"锁池"和"等待池"的概念:

  • 锁池:线程A已经拥有了某个对象(不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(块),但是该对象的锁现在正被线程A拥有,这些其他线程就进入了该对象的锁池中。
  • 等待池:
    • 线程A调用了某个对象的wait(),线程A就会释放该对象的锁(因为wait()必须在synchronized中,在执行wait()之前,线程A就已经拥有该对象的锁),同时该线程A进入到该对象的等待池中。
    • 如果另外的一个线程调用了相同对象的notifyAll(),处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。
    • 如果另外的一个线程调用了相同对象的notify(),仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。

9. 描述一下synchronized和lock区别 ?

  • 层次:Lock是一个接口,底层使用AQS框架实现;Synch是关键字,底层通过JVM实现的(monitor对象和对象头Mark Word)。
  • 锁的获取:Lock可以使用tryLock判断有没有锁。
  • 锁的释放:Lock需要在finally中自己释放,发生异常时,如果没有主动去释放锁,会导致死锁;Synch会自动释放,发生异常时,不会死锁。
  • 响应中断:Lock等待锁过程中可以用interrupt来中断等待,而Synch只能等待锁的释放,不能响应中断。
  • 锁的状态:Lock可以通过trylock()知道有没有获取锁,而Synch不能。
  • 公平锁:Lock可以设置为公平锁与非公平锁,而Synch只能是非公平锁。
  • 性能:Lock大量同步,Synch少量同步。Lock可以提高多个线程进行读操作的效率。(可以通过ReadWriteLock实现读写分离)

对象头:(主要背一下锁标志位)

拓展Synch的加锁流程:***** 

  • HotSpot研究发现,大多数情况下,锁总是由同一线程多次获得,引入了偏向锁。
  • 偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活。
  • 装逼点(不要瞎BB):批量重偏向与批量撤销

https://blog.csdn.net/u022812849/article/details/108531031

  • 获取偏向锁流程:
    • I. 判断是否为可偏向状态:MarkWord中锁标志是否为‘01’,是否偏向锁是否为‘1’。
    • II. 如果是可偏向状态,则查看MarkWord中线程ID是否为当前线程,如果是,则进入步骤 V,否则进入步骤III。
    • III. 通过CAS操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行V;竞争失败,则执行IV。
    • IV. CAS获取偏向锁失败表示有竞争。当达到safepoint时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。
    • V. 执行同步代码。
  • 轻量级锁是自旋锁,减少CPU上下文的切换,在线程交替执行同步块时,会提高性能。
  • 轻量级锁获取过程:
    • I. 进行加锁操作时,如果没有创建锁记录(Lock Record),则会在当前线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象MarkWord复制到该锁记录中(偏向锁的时候也会生成)。
    • II. 复制成功之后,JVM使用CAS操作将对象头的MarkWord更新为指向当前线程的锁记录指针,并将锁记录里的Owner指针指向对象头的MarkWord。如果成功,则执行III,否则执行IV
    • III. 更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,即表示此对象处于轻量级锁状态。
    • (IV. 更新失败,JVM先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是则执行V,否则执行VI。
    • V. 表示锁重入;然后当前线程栈帧中增加一个锁记录:第一部分(Displaced Mark Word)为null,并指向Mark Word的锁对象,起到一个重入计数器的作用。)
    • VI. 表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数达到阈值仍未获取到锁,则升级为重量级锁。
  • 当有多个锁竞争轻量级锁则会升级为重量级锁,重量级锁正常会进入一个CXQ队列(竞争队列),有资格成为候选资源的线程进入EntryList,通过竞争获取锁资源(成为Owner)。在调用wait()之后,则会进入一个waitSet队列park等待,而当调用notify()唤醒之后,则有可能进入EntryList(看图,背图)。

  • 重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的。
  • 重量级锁加锁过程:
    • I . 分配一个ObjectMonitor对象,把Mark Word锁标志置为10,然后MarkWord存储指向ObjectMonitor对象的指针。每个需要获取锁的线程都包装成ObjectWaiter对象。
    • II . 多个线程同时执行同一段同步代码时,ObjectWaiter先进入EntryList队列,当某个线程获取到对象的monitor以后进入Owner区域,并把monitor中的owner变量设置为当前线程同时monitor中的计数器count+1;

10. 简单描述一下ABA问题?

ABA过程:

  • 有两个线程同时去修改一个变量的值,比如线程1和线程2,都将变量值从A更新成B。
  • 首先线程1获取到CPU的时间片,线程2发生阻了塞进行等待,线程1通过CAS把值从A更新成B。
  • 线程1更新完后,恰好有线程3想要把变量的值从B更新成A。
  • 线程3更新成功后,线程2获取到CPU的时间片,进行CAS,发现值是预期的A,然后有更新成了B。
  • 但是线程1并不知道,该值已经有了A->B->A这个过程,这也就是我们常说的ABA问题。

解决:

可以通过加版本号或者加时间戳解决,或者保证单向递增或者递减就不会存在此类问题。Java中也有相关引用原子类。

11. 实现一下DCL(双重检测)?

用单例的方式说明DCL,加锁前后都判断对象是否为空。为了解决并发可能导致实例两个对象。自己代码中也用到过。(凤凰、神州租车用到过)

拓展点:DCL单例需要使用volatile关键字,防止指令重排序的优化,导致拿到半初始化的对象。

具体代码如下:

public class Singleton {// volatile是防止指令重排private static volatile Singleton singleton;private Singleton() {}public static Singleton getInstance() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Signletion();} }}return singleton;}
} 

12. 实现一个阻塞队列?

三种方法:

  • 使用Synchronized中wait() + notify()
  • 使用Lock中Condition的await() + signal()
  • 使用ArrayBlockingQueue队列的put() + take()

具体见代码:

《springboot-multi-thread》 com.hanxiaozhang.threadbase1ndedition.no98producerandconsumer

13.实现多个线程顺序打印abc?

public class PrintABC {ReentrantLock lock = new ReentrantLock();Condition conditionA = lock.newCondition();Condition conditionB = lock.newCondition();Condition conditionC = lock.newCondition();volatile int value = 0;public void printABC() {new Thread(new ThreadA()).start();new Thread(new ThreadB()).start();new Thread(new ThreadC()).start();} class ThreadA implements Runnable{@Overridepublic void run() {lock.lock();try {while (value % 3 != 0) {conditionA.await();} System.out.print("A");conditionB.signal();value ++;} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}... ... 

14.多线程之间是如何通信的?

  • 通过共享变量,变量需要volatile修饰。
  • 使用wait()和notifyAll(),但是由于需要使用同一把锁,所以必须通知线程释放锁,被通知线程才能获取到锁,这样导致通知不及时。
  • 使用Condition的await()和signalAll()方法。
  • 使用CountDownLatch实现,通知线程到指定条件,调用cdl.countDown(),被通知线程进行cdl.await()。

15.描述一下Synchronized底层实现,以及和Lock的区别?

对像头可以说明锁的四种状态:

结合第9题开始说。

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

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

相关文章

ARM32开发--IIC软实现

知不足而奋进 望远山而前行 目录 文章目录 前言 开发流程 GD32F4软件I2C初始化 GD32F4软件I2C引脚功能 写操作 读操作 总结 前言 在嵌入式系统开发中,软件实现的I2C通信协议扮演着至关重要的角色。本文将深入探讨如何在GD32F4系列微控制器上实现软件I2C功能…

Springboot配置websocket,https使用 WebSocket 连接

Springboot配置websocket,https使用 WebSocket 连接 提示:本文简单介绍websocket与http的区别及如何在项目中使用websocket,以springboot项目为例 一、http协议与websocket协议区别 WebSocket 一种在单个TCP连接上进行全双工通信的协议。W…

深入浅出 Go 语言的 GPM 模型(Go1.21)

引言 在现代软件开发中,有效地利用并发是提高应用性能和响应速度的关键。随着多核处理器的普及,编程语言和框架如何高效、简便地支持并发编程,成为了软件工程师们评估和选择工具时的一个重要考量。在这方面,Go 语言凭借其创新的并…

动态规划算法设计步骤

1. 定义状态(构建记忆表): 首先,需要确定问题的状态。状态可以表示为一个包含所有可能决策的变量的集合。例如,对于一个背包问题,状态可以表示为一个包含所有物品和它们的重量的数组。 2. 初始化&#xff…

Python的Pillow(图像处理库)非常详细的学习笔记

Python的Pillow库是一个非常强大的图像处理库。 安装Pillow库: 在终端或命令行中输入以下命令来安装Pillow: pip install pillow 安装后查看是否安装成功以及当前版本 pip show Pillow 升级库: pip install pillow --upgrade 一些基…

探索Ubuntu:从入门到精通

目录 一、什么是Ubuntu? 1.1 Ubuntu的定义和背景 1.2 Ubuntu的特点 二、安装Ubuntu 2.1 下载Ubuntu安装镜像 2.2 制作启动盘 2.3 安装Ubuntu 三、初步设置和基本操作 3.1 系统更新 3.2 安装必要软件 3.3 设置和管理用户账户 四、文件和目录管理 4.1 文件管理器 …

u-boot(六) - 详细启动流程

一,u-boot启动第一阶段 1,启动流程 ENTRY(_start) //arch/arm/lib/vectors.S ----b resets //arch/arm/cpu/armv7/start.S --------b save_boot_params ------------b save_boot_params_ret //将cpu的工作模式设置为SVC32模式(即管理模式&a…

NodeClub:NodeJS构造开源交流社区

NodeClub: 连接每一个想法,NodeClub让社区更生动- 精选真开源,释放新价值。 概览 NodeClub是一个基于Node.js和MongoDB构建的社区系统,专为开发者和社区爱好者设计。它提供了一套完整的社区功能,包括用户管理、内容发…

Mongodb在UPDATE操作中使用$push向数组中插入数据

学习mongodb,体会mongodb的每一个使用细节,欢迎阅读威赞的文章。这是威赞发布的第69篇mongodb技术文章,欢迎浏览本专栏威赞发布的其他文章。如果您认为我的文章对您有帮助或者解决您的问题,欢迎在文章下面点个赞,或者关…

spark mllib 特征学习笔记 (一)

PySpark MLlib 特征处理详解 PySpark MLlib 提供了丰富的特征处理工具,帮助我们进行特征提取、转换和选择。以下是 PySpark MLlib 中常用的特征处理类及其简要介绍。 1. Binarizer Binarizer 是将连续特征二值化的转换器。 from pyspark.ml.feature import Bina…

Postman接口测试工具:全面解析与应用指南

标题:Postman接口测试工具:全面解析与应用指南 在当今的软件开发领域,接口测试是确保软件质量和稳定性的重要环节。而Postman作为一款广泛使用的接口测试工具,以其强大的功能和直观的操作界面赢得了开发者和测试人员的青睐。本文…

qt dll编写和调用

dll编写 新建项目 头文件 #ifndef LIB1_H #define LIB1_H#include "lib1_global.h"class LIB1_EXPORT Lib1 { public:Lib1(); };//要导出的函数,使用extern "C",否则名称改变将找不到函数extern "C" LIB1_EXPORT int ad…

css引入方式有几种?link和@import有什么区别?

在CSS中&#xff0c;引入外部样式表的方式主要有两种&#xff1a;<link>标签和import规则。 使用<link>标签引入外部样式表&#xff1a; <link rel"stylesheet" href"path/to/style.css">这种方式是在HTML文档的<head>部分或者…

Maven:复制到自定义的目录比如target/libs目录下

拷贝依赖包 mvn dependency:copy-dependencies&#xff0c;默认会拷到项目的 target\dependency 目录&#xff0c;想要复制到自定义的目录比如target/libs目录下&#xff0c;需要在pom.xml文件中添加设置覆盖默认设置&#xff1a; <build> <plugins> <plugin&g…

探索未来边界:前沿技术引领新纪元

目录 引言 一、人工智能与深度学习&#xff1a;智慧生活的引擎 1.医疗应用 2.智能家居 3.自动驾驶 二、量子计算&#xff1a;解锁宇宙的密钥 1.量子比特示意图 2.量子计算机实物图 3.分子模拟应用 三、生物技术&#xff1a;生命科学的革新 1.CRISPR-Cas9基因编辑图 2.合成生…

buuctf----warmup_csaw_2016

进来医院先来一套常规检查 啥保护都没,看大佬说基本栈溢出 CT一看 OK cat flag 更喜欢了 40060D 找到地址 get也来了,稳啦! 0x80-0x40 8 根据上道题的exp from pwn import * ghust remote("node5.buuoj.cn",27229) addr 0x40060D payload bA * 0x40 bB*8…

C++设计模式——Bridge桥接模式

一&#xff0c;桥接模式简介 桥接模式是一种结构型设计模式&#xff0c;用于将抽象与实现分离&#xff0c;这里的"抽象"和"实现"都有可能是接口函数或者类。 桥接模式让抽象与实现之间解耦合&#xff0c;使得开发者可以更关注于实现部分&#xff0c;调用…

[C++] 编译期间生成一个元素等差的std::initializer_list

前因 写编译器的时候需要一个常量英文字母表&#xff0c;懒得26个字母一个个打了&#xff0c;于是搓了个板子。 源代码 // c14 required #include <initializer_list> #include <type_traits>template <typename _Tp, _Tp first_elem, int item_number, type…

具有不确定性感知注意机制的肺结节分割和不确定区域预测| 文献速递-深度学习结合医疗影像疾病诊断与病灶分割

Title 题目 Lung Nodule Segmentation and UncertainRegion Prediction With an Uncertainty-Aware Attention Mechanism 具有不确定性感知注意机制的肺结节分割和不确定区域预测 01 文献速递介绍 肺结节分割在肺癌计算机辅助诊断&#xff08;CAD&#xff09;系统中至关重…

java Springboot网上音乐商城(源码+sql+论文)

1.1 研究目的和意义 随着市场经济发展&#xff0c;尤其是我国加入WTO &#xff0c;融入经济全球化潮流&#xff0c;已进入国内外市场经济发展新时期&#xff0c;音乐与市场联系越来越紧密&#xff0c;我国音乐和网上业务也进入新历史发展阶段。为了更好地服务于市场&#xff0…