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功能…

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

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

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

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

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技术文章,欢迎浏览本专栏威赞发布的其他文章。如果您认为我的文章对您有帮助或者解决您的问题,欢迎在文章下面点个赞,或者关…

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…

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

目录 引言 一、人工智能与深度学习:智慧生活的引擎 1.医疗应用 2.智能家居 3.自动驾驶 二、量子计算:解锁宇宙的密钥 1.量子比特示意图 2.量子计算机实物图 3.分子模拟应用 三、生物技术:生命科学的革新 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桥接模式

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

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

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

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

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

不想搭集群,直接用spark

为了完成布置的作业,需要用到spark的本地模式,根本用不到集群,就不想搭建虚拟机,hadoop集群啥的,很繁琐,最后写作业还用不到集群(感觉搭建集群对于我完成作业来说没有什么意义)&…

Cisco Packet Tracer实验(二)

二、用交换机构建 LAN 构建物件如下: 四个PC 两个交换机 一个Multi Switch多功能拓展控制器 连线必须是这个直线!!!不是虚线 最后实现效果如下: 全部的线是绿的,就表示是通的。 尝试一下,看PC…

SolidWorks对设计电脑硬件配置要求是怎么样的

SolidWorks,作为达索系统(Dassault Systemes)旗下的子公司,一直以其出色的机械设计软件解决方案而著称。它是基于Parasolid内核开发,是单核三维设计软件,面上使用比较多的版本有SolidWorks2022、SolidWorks…

Golang | Leetcode Golang题解之第149题直线上最多的点数

题目&#xff1a; 题解&#xff1a; func maxPoints(points [][]int) (ans int) {n : len(points)if n < 2 {return n}for i, p : range points {if ans > n-i || ans > n/2 {break}cnt : map[int]int{}for _, q : range points[i1:] {x, y : p[0]-q[0], p[1]-q[1]if…

4. 案例研究-接口程序

4. 案例研究-接口程序 本章通过一个案例研究, 来展示设计互相配合的函数的过程.4.1 turtle 模块 创建一个文件mypolygon.py, 并输入如下代码:import turtle bob turtle.Turtle() print(bob)# 这一句的作用是让画板停留, 等手动点击x关闭画板, 程序才结束. # 否则程序执行完毕…

8.12 面要素符号化综述

文章目录 前言面要素介绍总结 前言 本章介绍如何使用矢量面要素符号化说明&#xff1a;文章中的示例代码均来自开源项目qgis_cpp_api_apps 面要素介绍 地理空间的要素分为点、线和面&#xff0c;对应的符号也分三类&#xff1a;Marker Symbol、Line Symbol和Fill Symbol&…

c#中上传超过30mb的文件,接口一直报404,小于30mb的却可以上传成功

在一次前端实现上传视频文件时,超过30mb的文件上传,访问接口一直报404,但是在Swagger中直接访问接口确是正常的,且在后端控制器中添加了限制特性,如下 但是却仍然报404,在apifox中请求接口也是报404, 网上说: 在ASP.NET Core中,配置请求过来的文件上传的大小限制通常…

生命在于学习——Python人工智能原理(3.4)

三、深度学习 7、过拟合与欠拟合 过拟合和欠拟合是所有机器学习算法都要考虑的问题。 &#xff08;1&#xff09;基本定义 a、欠拟合 欠拟合是指机器学习模型无法完全捕获数据集中的复杂模式&#xff0c;导致模型在新数据上的表现不佳&#xff0c;这通常是由于模型过于简单…