Java并发编程经典题目整理(2022.03.11 更新)

13 篇文章 0 订阅

目录

并行是指,多个处理器或者多核处理器同时执行处理多个任务,注意是同时执行。从CPU层面上来看,并发就是大量的任务在同一个CPU核上交替的按CPU提供的时间片运行。

举个栗子:

  • 并行:两个打饭窗口,两个队(一个窗口一个有条理)。
  • 并发:一个打饭窗口,两个队(交替)。

也就是说并发这样的情况如果是同时发生在多个CPU核上,那么就很有可能出现并行问题。

所以我们在使用多线程时:考虑的一般都是,高并发环境下,程序内部可能发生并行执行而导致某些错误的问题

线程和进程的区别?
  • 进程是操作系统中执行的一段程序,线程是CPU调度资源的单位。
  • 进程包含线程,类比我们的浏览器,他在操作系统层面就是一个进程,然后浏览器中的各个可以使用的设置就是浏览器这个进程中的线程。
  • 一个进程可以有多个线程,一个线程对应一个进程。
守护线程是什么?

我们用的最多的守护线程估计就是 JVM 的垃圾回收线程了,它在后台运行并且在满足一定条件下会进行垃圾回收,也可以按时回收。

所以守护线程,守护线程它是在后台运行的一种特殊进程,它能周期性的执行一些任务,独立于控制终端,也能等待处理某些发生的事件(垃圾回收)。

换句话来说,守护线程它拥有能够结束自己生命周期的能力,直接进行一个我命由我不由天

守护线程的作用?

我们拿 JVM 的垃圾回收线程来说,当我们的 JVM 要被关闭的时候,如果此时正在进行着垃圾回收,并且得持续一段时间,那么如果垃圾回收线程不是守护线程的话,整个 JVM 就得等到我们这个垃圾回收结束之后才能关闭。

但是如果垃圾回收线程它是守护线程,那么它的垃圾回收和JVM关闭就类似于异步,你可以先走,我自己清理完了我自己走

也避免了有些时候 JVM 突然崩溃,但是内存中又有对象垃圾,这个时候守护线程就可以再 JVM 崩溃之后,独立的清理完毕这些垃圾然后自我销毁。

什么是线程的死锁?
  • 我们通常来说的死锁就是两个或者两个以上的线程在执行的过程中,他们由于资源竞争或者由于彼此通信造成的一种阻塞的现象,这种现象很常见,但是如果是没有外力干扰就会无限制的阻塞的话,这种情况就是死锁。
  • 比如线程A持有锁A,线程B持有锁B,此时线程A在锁A内请求锁B,线程B在锁B内请求锁A,造成无限期的阻塞。
那么死锁形成的必要条件有哪些呢?
  1. 互斥条件:意思就是,这个临界资源一定是一个互斥锁,在一段时间内,这个资源只能被一个进程占用,如果此时还有其进程请求正在被占用的这个临界资源,就只能等待到当前占用该资源的进程释放锁。
  2. 占有且等待条件:这个好理解,就是一个进程已经占有了一个互斥条件的临界资源,没释放,并且又去请求另一个临界资源,但是此时请求的另一个临界资源被占用,所以此时这个进程就等待,并且还占有资源。
  3. 不可抢占条件:互斥锁的临界资源,一个进程占用,别的进程只能等待释放,而不能抢夺。
  4. 循环等待条件:比如线程A持有锁A,线程B持有锁B,此时线程A在锁A内请求锁B,线程B在锁B内请求锁A,造成无限期的阻塞。

以上四个条件就是死锁产生的必要条件。

那么我们如何避免死锁呢?

首先我们可以避免一个线程同时拥有多个锁,或者避免一个线程在所内同时占用多个资源,尽量去保证每个锁只占用一个资源;尝试使用定时锁,就是时间一到就会解锁的锁。

线程创建有哪几种方式?

在 Java 中一共有四种:

  • 继承 Thread 类,重写它的 run 方法。
  • 实现 Runnable 接口。
  • 实现 Callable 接口。
  • 使用线程池。
线程的状态说一下?
  • 新建(new):新的线程对象被创建。
  • 就绪(可运行状态,runnable):当线程对象调用 start() 方法之后进入该状态,等待调度。
  • 运行(running):就绪状态的线程(就绪状态是前提)获得了 CPU 时间片,开始执行代码逻辑。
  • 阻塞(blocking):处于运行状态的线程由于某种原因暂时放弃了对 CPU 的使用权,停止执行,进入阻塞态,等到进入就绪态才有机会再次获得 CPU 时间片运行。
    • 阻塞分三种情况:
      1. 等待阻塞:运行状态中的线程执行了 wait() 方法,JVM 会把这个线程放入等待队列中,使得当前线程进入到等待阻塞状态。
      2. 同步阻塞:在线程获取 synchronized 同步锁失败时(别的线程占用中),JVM 会把这个线程放入锁池中,线程会进入同步阻塞状态。
      3. 其它阻塞:通过调用线程的 sleep() 、join() 方法 或者发出了 I/O 请求的时候,线程会进入到阻塞的状态,等到这些操作完成之后,线程会再次进入就绪状态继续准备被 CPU 调度。
  • 死亡(dead):线程运行结束,也就是 run()、main() 等方法执行完毕。
说一说你知道的与线程同步以及线程调度相关的方法?
  • wait():使调用线程处于阻塞状态,并且释放所持有的锁。
  • sleep():使调用线程处于阻塞状态(睡眠),不释放锁。
  • notify():随机唤醒一个正在等待状态(阻塞)的线程,是 JVM 来控制确定唤醒谁,而且与优先级无关。
  • notifyAll():唤醒所有等待状态的线程,但是这个方法不是给对象的锁给所有线程,而是让他们竞争,只有获得锁的线程才能进入就绪状态。
那么你是如何调用 wait() 方法的?为什么?

一般都是在 while 循环中不断轮询调用的,因为处于等待状态中的线程可能会收到错误的警报和伪唤醒,如果不在循环中检查等待条件,程序就可能会在没有满足结束条件的情况下退出;

我们的 wait() 方法也是在循环中调用,因为当线程或许到 CPU 开始执行时,其它条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。

两个线程以上如果不在循环里就很容易出问题了。

假设生产者消费者模型,两个消费者,一个生产者。

两个消费者进入消费逻辑:

if (list.size() == 0) { 
    // 两个消费者由于 size() == 0 进入等待状态
    wait();  
}

int v = list.remove(0);

此时假设生产者把执行了 list.add() 操作,并且随机唤醒了其中一个消费者,那么由于不是循环,所以这个消费者就执行 list.remove(0) 操作,之后别的线程不小心又唤醒了此时另一个消费者线程,由于不是循环,这个消费者线程往下走消费执行 list.remove(0),但是集合早就为空了,所以出现了异常。

这就是为什么我们要用循环的原因。

为什么 wait()、notify()、notifyAll() 必须在同步方法或者同步块中被调用?

这个就是设计层面的问题了,反正当一个线程需要调用对象的 wait() 方法,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并且进入等待状态;notify()、notifyAll() 方法同样需要锁的加持,所以他们就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

interrupted() 和 isInterrupted() 方法的区别?
  • **interrupt:**用于中断正在运行的线程,调用该方法的线程的状态将被设置为中断状态。
  • **interrupted:**是静态方法,查看当前中断信号是 true 还是 false 并且清楚中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就是返回 false 了。
  • **isInterrupted:**与 interrupt 最大的差别就是可以返回当前中断信号是 true 还是 false。
sleep() 和 wait() 的区别?
  • sleep 来自 Thread 类,wait 来自 Object;
  • sleep 不会释放锁,wait 会释放锁;
  • sleep 到期会自动苏醒,wait 得被主动叫醒。
  • wait 只能在同步代码块中使用。
notify() 和 notifyAll() 有什么区别?
  • 前者是由虚拟机叫醒一个线程,后者是全部叫醒。
  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁
  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
请你解释一下 volatile?

volatile 是一个关键字,是一个轻量级的线程同步机制。

我们至少要答出两点:

  • 它保证了可见性。
  • 它保证了有序性,也就是禁止指令重排序。
请你说一下 volatile 他是怎么保证可见性的?

首先,我们得了解 JMM 内存模型,因为一般情况下,我们的线程工作的内存环境都是基于 JMM 内存模型的,JMM 内存模型使每个线程有一个私有的工作内存,工作内存中的内容来自于主存,工作内存互相隔离互不干扰。

所以这样的内存模型的加持下,我们的可见性(变量之类的)在没有经过处理之前,它都是线程之间不可见的,所以我们这里说的可见性,就指的是,某个变量,要对所有先的线程可见。

但是,如果直接在工作内存层面来进行可见性的处理的话,是比较复杂的。

所以我们的 volatile 他是直接对被修饰的变量在主存中就直接禁止被线程复制到自己的工作空间中,让线程使用的时候,直接从主内存拿取,这样所有线程都得访问一个位于主内存中的变量,所以解决了不可见的问题。

volatile 具体是怎么保证可见的呢?

推送到主存的过程中会经过CPU数据总线,其它线程怎么可见呢?

  • 最主要就是体现在 CPU 数据总线这个地方。
  • 使用了CPU的总线嗅探技术,除了这个技术以外还是用了 MESI 缓存一致性协议,他们共同为 volatile 保证了可见性。
CPU嗅探总线的过程中,它怎么知道 volatile 修饰的呢?

汇编层面的问题,当我们这个变量被 volatile 修饰,并且被一个线程修改要推到主存的过程,它对应的汇编指令码会在里面加一个 lock 指令。

这个 lock 指令,在汇编代码层面有两次含义:

  • 将我们这条信息修改推送到主存。
  • lock 指令过总线的时候,CPU 会嗅探带 lock 指令的汇编指令。
volatile 如何禁止指令重拍序呢?

首先我们要禁止的指令重排序是发生在编译阶段的,因为编译阶段,编译器会把我们的.java 文件的内容编译成JVM执行的指令,编译器的优化功能可能会按照它自己的逻辑打乱一些上下不相关的JVM的指令的执行顺序,这在单线程情况下是可以的,但是在多线程情况下,可能就会引发乱子。

所以我们多线程下要考虑的指令重排序,就是在编译阶段的指令重排序。volatile就是在这个阶段介入的。

那么 volatile 它做了什么?

它主要就是加了内存屏障:

  • volatile在写前加屏障store-store 写写不能进行重排序。

  • volatile在写后加屏障store-load 写读不能进行重排序。

  • volatile在读后加屏障load-load 读读不能进行重排序。

  • volatile在读后加屏障load-store 读写不能进行重排序(全能型屏障,开销大,基本不用)。

说一说 volatile 对于原子性的理解?

volatile 在特殊情况下是可以保证原子性的,但是前提是在只有在编译过后还是单条 JVM 指令的情况下

i++ 来说,之前反编译看过,它虽然是只有一条代码,但是 JVM 为其生成了三个指令,**I LOAD,I ADD, I STORE**我们的 volatile 此时它的作用域就只在 I LOAD 前面加 store store 屏障,在 I STORE 后面加 store load 屏障,并不是每个步骤都加上了屏障

我们还是尽量不要回答到 JVM 指令,不然就是在给自己挖坑。

synchronized 关键字作用在不同的地方有什么区别?

作用在普通方法上

  • 他不是得new一个当前Class模板的类对象,才能调用,所以此时锁的就是这个对象

作用在静态方法上

  • synchronized 此时锁的是当前静态代码块所在类的Class,没错,就是你在方法区中的 Class 模板。
  • 为什么是作用在 Class 上呢?因为,我们是不是直接可以通过类名来调用静态方法,而不需要我们去创建对象,这就意味着我们没对象可以锁了!那我们锁啥?
  • 就只剩下了我们的 Class 。
  • 那这个 Class 是从哪来的呢?他是经过java -c 编译之后,形成的二进制字节码文件,然后进行七步类加载过程,最终我们的.class 文件会放到方法区中,只不过它会从静态的二进制文件,转换为我们 JVM 虚拟机中的方法区的运行时数据文件,并且常量池会从静态常量池转化为运行时常量池,并且还要在堆里面为我们的 Class 开放一个入口,这个 Class 在这,也就是模板。

作用在同步代码块上

  • 我们可以指定锁,我们可以在成员变量中 new Object 作为锁,我们也可以用 this 关键字作为锁。
synchronized 和 volatile 到底有什么区别?
  • 前者适用于写多读少,后者适用于写少都多的场景。
  • volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
synchronized 和 Lock 有什么区别?
  • 前者可以作用到方法上,后者只能所用在代码块上。
  • 前者加锁解锁是隐式的,后者是显式的。
  • 前者不需要手动释放,后者需要手动释放。
  • 前者是 JVM 层面的,后者是编码层面的。
  • 前者不可以知道有没有获取锁,后者可以知道。
  • 前者是不公平锁,后者可以是公平也可以是非公平锁。
  • 前者只能简单加锁,阻塞加锁,后者可以超时加锁,非阻塞加锁。

码云仓库同步笔记,可自取欢迎各位star指正:https://gitee.com/noblegasesgoo/notes

如果出错希望评论区大佬互相讨论指正,维护社区健康大家一起出一份力,不能有容忍错误知识。
										—————————————————————— 爱你们的 noblegasesgoo
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值