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{
        @Override
        public 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题开始说。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何把路由器设备的LAN口地址为三大私网地址

要将路由器的LAN口地址配置为三大私有IP地址范围之一(10.0.0.0/8、172.16.0.0/12 或 192.168.0.0/16),我们需要访问路由器的管理界面并进行相应的设置。 下面是步骤: 连接到路由器: 连接到路由器的管理界面&#xf…

在vue中循环中调用接口-promise.all();按顺序执行异步处理

🌈🌈🌈目录 场景一 解决 场景二 解决 场景一 数组遍历中每次遍历都需要去请求getStaffCover接口,拿到该接口的结果拼接到数组的每一项,等到数组遍历完之后,拿到拼接好的数组。拼接的数组必须是最终遍历…

计算机网络 —— 应用层(应用层概述及服务方式)

计算机网络 —— 应用层(应用层概述及服务方式) 应用层服务方式C/S(客户端-服务器(C/S)模型)基本概念特点B/S(Browser/Server)基本概念特点应用场景 p2p (对等网络&#…

这三款使用的视频、图片设计工具,提供工作效率

Videograp Videograp是一款专注于视频生成的工具,特别适合需要快速剪辑和编辑视频的用户。Videograp具备以下特点: 影音比例转换:Videograp支持调整视频的分辨率和比例,使其更适合不同的播放环境和设备。 AI快剪:该工…

超市陈列艺术:不仅仅是货品摆放,更是营销策略的体现

品类管理在门店落地的最直观表现就是单品的空间陈列管理,通过陈列细节的差异体现出门店的商品定位与策略。此文分析入木三分,值得学习。 在商品陈列的空间管理领域,不仅要考虑整体的空间陈列,也要对每个商品的空间陈列位置&#…

旅游行业电商平台:数字化转型的引擎与未来发展趋势

引言 旅游行业数字化转型的背景和重要性 随着信息技术的飞速发展,数字化转型成为各行业发展的必然趋势。旅游行业,作为一个高度依赖信息和服务的领域,数字化转型尤为重要。通过数字化手段,旅游行业能够实现资源的高效配置、服务的…

Apache Doris单机快速安装(已踩坑)

官方文档:https://doris.incubator.apache.org/zh-CN/docs/get-starting/quick-start/ 环境: 操作系统:CentOS7.6 X86_64 JDK:Oracle jdk1.8.0_351 1.版本下载 从 doris.apache.org 下载相应的 Doris 安装包,并且解压…

11.QLoRA微调ChatGLM3-6B

实战 QLoRA 微调 ChatGLM3-6B 大模型 实战 PEFT 库 QLoRA ChatGLM3-6B 微调数据集 AdvertiseGen AdvertiseGen 数据集获取 使用ChatGLM3-6b Tokenizer处理数据 关于ig nore_label_id 的设置: 在许多自然语言处理和机器学习框架中, ig nore_label_id 被…

idea在空工程中添加新模块并测试的步骤

ServicesTest是空的工程,没有pom文件。现在需要在ServicesTest目录下添加新模块作为新的工程,目的是写一下别的技术功能。 原先目录结构,ServicesTest是空的工程,没有pom文件。下面的几个模块是新的工程,相互独立。 1.…

树莓派4B学习笔记8:开机自启动Python脚本_kill关闭后台脚本

今日继续学习树莓派4B 4G:(Raspberry Pi,简称RPi或RasPi) 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1: 紧接着上篇文章学习的串口通信,今日学习如何让树莓派开机…

[Algorithm][贪心][柠檬水找零][将数组和减半的最少操作次数][最大数][摆动序列]详细讲解

目录 1.柠檬水找零1.题目链接2.算法原理详解3.代码实现 2.将数组和减半的最少操作次数1.题目链接2.算法原理详解3.代码实现 3.最大数1.题目链接2.算法原理详解3.代码实现 4.摆动序列1.题目链接2.算法原理详解3.代码实现 1.柠檬水找零 1.题目链接 柠檬水找零 2.算法原理详解 …

网络安全 - ARP 欺骗原理+实验

APR 欺骗 什么是 APR 为什么要用 APR A P R \color{cyan}{APR} APR(Address Resolution Protocol)即地址解析协议,负责将某个 IP 地址解析成对应的 MAC 地址。 在网络通信过程中会使用到这两种地址,逻辑 IP 地址和物理 MAC 地址&…

短剧APP小程序开发之小程序内存管理挑战:短剧缓存与释放策略探讨(第二篇)

在上一篇帖子中,我们探讨了小程序内存管理的限制以及缓存策略的设计。本篇将进一步探讨释放策略的具体实现以及优化方案,以支持大量短剧内容的加载和播放。 释放策略的具体实现 监听内存警告:小程序提供了监听内存警告的API,开发…

【PL理论】(22) 函数式语言:多参数 | 柯里化 (Currying) : 将多参数函数实现为返回一个函数的函数

💭 写在前面:本章我们将继续讲解函数式语言,介绍多参数,着重讲解柯里化的概念,将多参数函数实现为返回一个函数的函数。 目录 0x00 多参数(Multiple Arguments) 0x01 柯里化(Curr…

Android framework的Zygote源码分析

文章目录 Android framework的Zygote源码分析linux的fork Android framework的Zygote源码分析 init.rc 在Android系统中,zygote是一个native进程,是Android系统上所有应用进程的父进程,我们系统上app的进程都是由这个zygote分裂出来的。zyg…

AI虚拟试穿技术:开启高保真、多场景、多样化服装组合的试穿应用

随着电子商务的快速发展,消费者对于在线购物体验的要求越来越高。特别是在服装领域,消费者渴望能够在购买前直观地了解服装的试穿效果。传统的虚拟试穿技术虽然已有一定的发展,但在不同场景下的高保真度和鲁棒性方面仍面临挑战。为此,我们研发了一种全新的AI虚拟试穿技术,…

电脑开机之后要很久才能进入系统?进入WinPE也是卡顿半天?

前言 小白最近接到了一张很奇怪的电脑维修单,客户说他的工作室电脑开机特别慢,开机之后特别卡顿,在使用的时候也会一卡一卡的。 这事情开始看很简单:估计就是电脑还是机械硬盘,所以开机很慢又卡顿。所以应该是把机械…

vi/vim使用命令

你是否在编辑文件时以为键盘坏了,为什么不能删除呢,为什么不能敲代码呢,等你初识vi,会觉得这个东西为什么设计得这么难用,这篇教程带你熟练得用上这款经典的工具 Vi 是在 Unix 系统上广泛使用的编辑器,Vim …

【车载音视频电脑】嵌入式AI分析车载DVR,支持8路1080P

产品特点 采用H.265 & H.264编解码,节约存储空间、传输流量; 高分辨率:支持8路1080P*15FPS/4路1080P*30FPS、720P、D1等编解码; 支持1张SATA硬盘,取用方便,满足大容量存储要求; 支持1个…

安装台式电脑网卡驱动

安装电脑网卡驱动 1. 概述2. 具体方法2.1 先确定主板型号2.2 详细操作步骤如下2.2.1 方法一2.2.2 方法二2.2 主流主板官网地址 结束语 1. 概述 遇到重装系统后、或者遇到网卡驱动出现问题没有网络时,当不知道怎么办时,以下的方法,可以作为一…