当前位置:   article > 正文

史上最全的Java并发面试题(珍藏版),2024年最新java面试话术_java 并发面试题

java 并发面试题

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文


CopyOnWriteArrayList的特性是针对读操作,不做处理,和普通的ArrayList性能一样。而在写操作时,会先拷贝一份,实现新旧版本的分离,然后在拷贝的版本上进行修改操作,修改完后,将其更新至就版本中。

那么他的使用场景就是:一个需要在多线程中操作,并且频繁遍历。其解决了由于长时间锁定整个数组导致的性能问题,解决方案即写时拷贝。

另外需要注意的是CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

线程安全

====

什么叫线程安全?servlet是线程安全吗?


线程安全就是说多线程访问同一代码,不会产生不确定的结果。

在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。

线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

同步有几种实现方法?


1.同步方法

即有synchronized关键字修饰的方法。

由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

2.同步代码块

即有synchronized关键字修饰的语句块。

被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

3.使用特殊域变量(volatile)实现线程同步

a.volatile关键字为域变量的访问提供了一种免锁机制,

b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,

c.因此每次使用该域就要重新计算,而不是使用寄存器中的值

d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

4.使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

5.使用局部变量实现线程同步

volatile有什么用?能否用一句话说明下volatile的应用场景?


作用是:作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,即不是从寄存器里取备份值,而是去该地址内存存储的值。

一句话说明volatile的应用场景:


对变量的写操作不依赖于当前值且该变量没有包含在具有其他变量的不变式中。

请说明下java的内存模型。


Java内存模型的逻辑视图

史上最全的Java并发面试题(珍藏版)

为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是内存模型。

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。

它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

内存模型解决并发问题主要采用两种方式

  • 限制处理器优化

  • 使用内存屏障

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现

  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现

  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

并发容器和框架

=======

如何让一段程序并发的执行,并最终汇总结果?

使用CyclicBarrier 在多个关口处将多个线程执行结果汇总, CountDownLatch 在各线程执行完毕后向总线程汇报结果。

CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。

CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。

从api上理解就是CountdownLatch有主要配合使用两个方法countDown()和await(),countDown()是做事的线程用的方法,await()是等待事情完成的线程用个方法,这两种线程是可以分开的(下面例子:CountdownLatchTest2),当然也可以是同一组线程;CyclicBarrier只有一个方法await(),指的是做事线程必须大家同时等待,必须是同一组线程的工作。

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

/**

  • 各个线程执行完成后,主线程做总结性工作的例子

  • @author xuexiaolei

  • @version 2019年4月16日

*/

public class CountdownLatchTest2 {

private final static int THREAD_NUM = 10;

public static void main(String[] args) {

CountDownLatch lock = new CountDownLatch(THREAD_NUM);

ExecutorService exec = Executors.newCachedThreadPool();

for (int i = 0; i < THREAD_NUM; i++) {

exec.submit(new CountdownLatchTask(lock, “Thread-”+i));

}

try {

lock.await();

}

catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“大家都执行完成了,做总结性工作”);

exec.shutdown();

}

static class CountdownLatchTask implements Runnable{

private final CountDownLatch lock;

private final String threadName;

CountdownLatchTask(CountDownLatch lock, String threadName) {

this.lock = lock;

this.threadName = threadName;

}

@Override public void run() {

System.out.println(threadName + " 执行完成");

lock.countDown();

}

}

}

CyclicBarrier例子:

import java.util.concurrent.*;

/**

  • @author xuexiaolei

  • @version 2019年4月16日

*/

public class CyclicBarrierTest {

private final static int THREAD_NUM = 10;

public static void main(String[] args) {

CyclicBarrier lock = new CyclicBarrier(THREAD_NUM, new Runnable() {

@Override public void run() {

System.out.println(“这阶段大家都执行完成了,我总结一下,然后开始下一阶段”);

}

}

);

ExecutorService exec = Executors.newCachedThreadPool();

for (int i = 0; i < THREAD_NUM; i++) {

exec.submit(new CountdownLatchTask(lock, “Task-”+i));

}

exec.shutdown();

}

static class CountdownLatchTask implements Runnable{

private final CyclicBarrier lock;

private final String threadName;

CountdownLatchTask(CyclicBarrier lock, String threadName) {

this.lock = lock;

this.threadName = threadName;

}

@Override public void run() {

for (int i = 0; i < 3; i++) {

System.out.println(threadName + " 执行完成");

try {

lock.await();

}

catch (BrokenBarrierException e) {

e.printStackTrace();

}

catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

}

如何合理的配置java线程池?如CPU密集型的任务,基本线程池应该配置多大?IO密集型的任务,基本线程池应该配置多大?用有界队列好还是×××队列好?任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量

虽然Exectors可以生成一些很常用的线程池,但毕竟在什么情况下使用还是开发者最清楚的。在某些自己很清楚的使用场景下,java线程池还是推荐自己配置的。下面是java线程池的配置类的参数,我们逐一分析一下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler

  • corePoolSize - 池中所保存的线程数,包括空闲线程。

  • maximumPoolSize - 池中允许的最大线程数。

  • keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

  • unit - keepAliveTime 参数的时间单位。

  • workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。用BlocingQueue的实现类都可以。

  • threadFactory - 执行程序创建新线程时使用的工厂。自定义线程工厂可以做一些额外的操作,比如统计生产的线程数等。

  • handler - 饱和策略,即超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。策略有:Abort终止并抛出异常,Discard悄悄抛弃任务,Discard-Oldest抛弃最老的任务策略,Caller-Runs将任务退回给调用者策略。

至于线程池应当配置多大的问题,一般有如下的经验设置:

  • 如果是CPU密集型应用,则线程池大小设置为N+1。

  • 如果是IO密集型应用,则线程池大小设置为2N+1。

用有界队列好还是×××队列好?这种问题的答案肯定是视情况而定:

  • 有界队列有助于避免资源耗尽的情况发生。但他带来了新的问题:当队列填满后,新的任务怎么办?所以有界队列适用于执行比较耗资源的任务,同时要设计好相应的饱和策略。

  • ×××队列和有界队列刚好相反,在资源无限的情况下可以一直接收新任务。适用于小任务,请求和处理速度相对持平的状况。

  • 其实还有一种同步移交的队列 SynchronousQueue ,这种队列不存储任务信息,直接将任务提交给线程池。可以理解为容量只有1的有界队列,在特殊场景下有特殊作用,同样得设计好相应的饱和策略。

如何使用阻塞队列实现一个生产者和消费者模型?请写代码

下面这是一个完整的生产者消费者代码例子,对比传统的wait、nofity代码,它更易于理解。

ProducerConsumerPattern.java如下:

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.logging.Level;

import java.util.logging.Logger;

public class ProducerConsumerPattern {

public static void main(String args[]){

//Creating shared object

BlockingQueue sharedQueue = new LinkedBlockingQueue();

//Creating Producer and Consumer Thread

Thread prodThread = new Thread(new Producer(sharedQueue));

Thread consThread = new Thread(new Consumer(sharedQueue));

//Starting producer and Consumer thread

prodThread.start();

consThread.start();

}

}

生产者,Producer.java如下:

class Producer implements Runnable {

private final BlockingQueue sharedQueue;

public Producer(BlockingQueue sharedQueue) {

this.sharedQueue = sharedQueue;

}

@Override

public void run() {

for (int i=0; i<10; i++){

try {

System.out.println("Produced: " + i);

sharedQueue.put(i);

}

catch (InterruptedException ex) {

Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex);

}

}

}

}

消费者,Consumer.java如下所示:

class Consumer implements Runnable{

private final BlockingQueue sharedQueue;

public Consumer (BlockingQueue sharedQueue) {

this.sharedQueue = sharedQueue;

}

@Override

public void run() {

while(true){

try {

System.out.println("Consumed: "+ sharedQueue.take());

}

catch (InterruptedException ex) {

Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex);

}

}

}

}

Java中的锁

=======

如何实现乐观锁(CAS)?如何避免ABA问题?


分享

首先分享一份学习大纲,内容较多,涵盖了互联网行业所有的流行以及核心技术,以截图形式分享:

(亿级流量性能调优实战+一线大厂分布式实战+架构师筑基必备技能+设计思想开源框架解读+性能直线提升架构技术+高效存储让项目性能起飞+分布式扩展到微服务架构…实在是太多了)

其次分享一些技术知识,以截图形式分享一部分:

Tomcat架构解析:

算法训练+高分宝典:

Spring Cloud+Docker微服务实战:

最后分享一波面试资料:

切莫死记硬背,小心面试官直接让你出门右拐

1000道互联网Java面试题:

Java高级架构面试知识整理:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
效存储让项目性能起飞+分布式扩展到微服务架构…实在是太多了)

其次分享一些技术知识,以截图形式分享一部分:

Tomcat架构解析:

[外链图片转存中…(img-3Qg0lW37-1713107594400)]

算法训练+高分宝典:

[外链图片转存中…(img-f17BM4fB-1713107594400)]

Spring Cloud+Docker微服务实战:

[外链图片转存中…(img-ZOAeemIP-1713107594401)]

最后分享一波面试资料:

切莫死记硬背,小心面试官直接让你出门右拐

1000道互联网Java面试题:

[外链图片转存中…(img-u350uu8N-1713107594401)]

Java高级架构面试知识整理:

[外链图片转存中…(img-oyzlJ11Q-1713107594401)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-Pk4lvuSj-1713107594402)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/808873
推荐阅读
相关标签
  

闽ICP备14008679号