java面试题(并发)

线程

1、简述线程、进程、程序的概念;以及他们之间的关系

程序:含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中

线程:是一个比进程更小的执行单位,进程执行的过程中可以产生多个线程

进程:程序的一次执行过程,系统运行程序的基本单位

系统运行一个程序即是一个进程从创建,运行到消亡的过程

2.线程的生命周期及基本状态

生命周期:新建、就绪、运行、堵塞、死亡

状态:新建、可运行、堵塞、无限等待、计时等待、死亡

3.对线程安全的理解,如何实现线程安全

线程安全的本质应该是内存安全,堆是共享内存,可以被所有线程访问

4.synchronized的实现原理

synchronized关键字解决的是多个线程之间访问资源的同步性,也称之为”同步锁“。

作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。

  1. 同步方法:隐式的,无需通过字节码指令来控制,jvm可以从方法常量池的表结构中得知一个方法是否为同步方法

  1. 同步代码块:synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象的锁,如果这个锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取对象锁一直失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

5.synchronized和ReentrantLock的区别
  1. synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。

  1. synchronized会自动加锁和释放锁;ReentrantLock需要手动加锁和释放锁

  1. synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁

  1. synchronized是关键字,ReentrantLock是类

  1. synchronized是jvm层面的锁,ReentrantLock是API层面的锁

  1. ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断

6.什么是CAS?有什么优缺点?

CAS是一种原子操作,用于实现多线程环境下的同步和并发控制。它包含三个操作数,即内存位置V,期望值A和新值B。当前仅当内存位置V的值与期望值A值相等时,才将内存值V的值更新为新值B

优缺点:

高效性:相比较传统的同步方式,CAS能够在无锁的情况下进行并发操作,避免了线程之间的互斥和阻塞,因此效率更高。

无死锁:CAS不会引起死锁,因为它不需要加锁。

确保操作的原子性:CAS可以确保并发环境下对于同一内存位置的操作具有原子性,避免数据不一致的问题。

ABA问题:当一个值从A变成B,再变成A时,如果CAS检查的时候只检查了值是否等于A,那么CAS将认为这个值没有发生变化,可能会引发一些问题。

只能保证一个共享变量的原子操作:如果需要对多个变量进行原子操作,CAS就无法满足需求。

自旋开销大:在高并发场景下,如果CAS一致失败,就会一直自旋,占用CPU资源,导致性能下降。

对CPU的缓存使用可能存在问题:由于CAS需要访问内存,所以在高并发环境下可能会引发CPU缓存一致性的问题

7.ReentrantLock是如何实现锁的公平性和非公平性的?

ReentrantLock是根据传入的参数来决定是否使用公平锁,默认使用非公平锁;

公平锁: 指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

非公平锁: 多个线程加锁时直接尝试获取锁,能抢到锁直接占有锁,抢不到才会到等待队列的队尾等待。

8.什么情况下出现死锁,如何避免

多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放;

造成死锁条件:

  1. ⼀个资源每次只能被⼀个线程使用

  1. ⼀个线程在阻塞等待某个资源时,不释放已占有资源

  1. ⼀个线程已经获得的资源,在未使用完之前,不能被强制剥夺

  1. 若干线程形成头尾相接的循环等待资源关系

只需要打破第四条件就不会造成死锁

如何避免:

  1. 保证每个线程按同样的顺序进行加锁

  1. 针对每个锁设置一个超时时间

  1. 死锁检测机制

9.volatile、ThreadLocal的使用场景

volatile:能保证可见性和一定程度的顺序性,一个共享变量被volatile修饰后,就具备了两层语义:保证了不同线程对这个变量进行操作时的可见性和禁止了指令重排序

使用场景:

实时共享变量

开销较低的读-写锁策略

单例模式双重检查

ThreadLocal:是一个线程的局部变量,会为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,将对象的可见范围限制在同一个线程内,而不会影响其它线程所对应的副本。

使用场景:

数据库连接

session管理

10.ThreadLocal什么时候会出现内存泄漏

内存泄漏:主要是因为ThreadLocal中包含了ThreadLocalMap,然而ThreadLocalMap的对象是在Thread中的,如果Thread没有结束,则ThreadLocalMap一直不会释放,假如ThreadLocalMap中设置了很多值,而且没有手动设置remove(),则可能会造成内存泄露。

当Thread一直没有结束时,Thread中的threadLocals就不会被回收,threadLocals里面存储的Entry如果不手动删除的话,就会一直存在这个threadLocals里面,所以就会出现内存泄漏的问题

11.synchronized、volatile区别
  1. synchronized修饰方法、变量、类;volatile修饰变量

  1. synchronized可能会造成线程的阻塞;volatile不会造成线程的阻塞;

  1. synchronized标记的变量可以被编译器优化;volatile标记的变量不会被编译器优化

  1. synchronized则可以保证变量的修改可见性和原子性;volatile仅能实现变量的修改可见性,不能保证原子性

  1. synchronized是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取

12.并发的三大特性

有序性:程序执行的顺序按照代码的先后顺序执行

原子性:一个或多个操作,要么全部执行要么全部不执行

可见性:当一个线程修改了共享变量的值,其他线程能够看到修改的值

13.并行、并发、串行

并发:同一时间段内,多条指令在cpu上同时执行

并行:同一时刻,多条指令在cpu上同时执行

串行:一次执行一个任务,执行完成,才可继续执行其他任务

14.多用户并发访问时如何解决

分布式是以缩短单个任务的执行时间来提升效率,而集群则是通过提高单位时间内执行的任务数来提升效率。

集群主要分为:高可用集群、负载均衡集群、科学计算集群

分布式是指将不同的业务分布在不同的地方,集群指几台服务器集中在一起,实现同一业务;分布式中每个节点都可以做集群,而集群并不一定是分布式的。

15.进程通讯的方式
  1. 管道

  1. 消息队列

  1. 信号量

  1. 内存共享

  1. 套接字

16.线程的创建方式
  1. 继承Thread接口,重写run方法

  1. 实现Runnable接口,重写run方法

  1. 使用Callable重写call方法,配合FutureTask

  1. 基于线程池构建线程

 追其底层,都是使用Runnable

17.终止线程的方式
  1. 程序正常运行结束

  1. 使用共享变量(外部定义值,通过修改变量破坏循环)

  1. 使用Interrupt方法结束线程

  1. 使用stop方法终止线程(不推荐,线程不安全)

18.java锁及分类

按照锁的重量区分:

重量级锁:synchronized

轻量级锁:lock

按照锁的是否可重入区分:

可重入锁:ReentrantLock

不可重入锁:synchrozed

按照锁的公平性区分:

公平锁:创建ReentrantLock时传入true则是公平锁

非公平锁:其余场景包括synchronized都是非公平锁

按照锁的原理区分:

乐观锁:认为多线程同时修改共享资源的概率比较低,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

悲观锁:比较悲观,认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁

按照锁的互斥性区分:

互斥锁:只有一个线程能够访问被互斥锁保护的资源;在访问共享对象之前,对其进行加锁操作。在访问完成之后进行解锁操作

共享锁:ReentrantReadWriteLock.readLock 读锁读锁不排斥其他线程的读行为,但不能写,是一种共享锁

19.ReentrantLock中tryLock()和Lock()方法的区别

Lock:阻塞加锁,无返回值,如果该线程没有获得锁就阻塞,直到获得锁才放开

tryLock:尝试加锁,可能加到也可能加不到,该方法不会阻塞线程,加到锁才返回true否则返回false;利用tryLock可以有自旋锁,自旋锁相对灵活,但对cpu消耗大,性能优于lock。

20.sleep、wait的区别
  1. sleep属于Thread类中的方法,wait属于Object类的方法

  1. sleep会自动唤醒,wait需要手动唤醒

  1. sleep持有锁时,执行不会释放锁资源;wait执行后会释放锁资源

  1. sleep可以在持有锁或不持有锁时执行;wait必须要持有锁才可以执行

21.Thread和Runnable的区别
  1. Runnable通过继承Thread实现,还可以实现多个接口

  1. Runnable是接口,而Thread是类

  1. Runnable支持多线程间的资源共享

22.守护线程的理解

也叫后台线程,为所有非守护线程提供服务;守护线程必须可以随时关闭;如果想设置守护线程,必须在start()方法之前设定;不能把正常运行的线程设置为守护线程。

如:GC垃圾回收线程

23.start和run的区别
  1. start方法来启动线程;run只是thread内的普通方法

  1. 需要并行处理的代码放在run方法中;start方法启动线程后自动调用run方法

  1. run必须为public,返回类型为void

线程池

1.为什么不能使用Executor去创建线程池

通过ThreadPoolExecutor的方式创建,可以让写的人更加明确线程池的运行规则,规避资源耗尽的风险。

使用Executor创建,可能会堆积大龄的请求或创建大量的线程,导致内存泄漏。

2.如何根据实际需求定制自己的线程池

指定线程数量:如果是cpu密集的任务,应该设置数量为cpu核心的1-2倍;如果io密集的任务,设置为两倍以上。

自定义线程工厂:给自定义的线程池起一个个性化的名字,这有助于我们在查找日志的时候精确的定位到具体的某个线程池。      选择合适的拒绝策略:一般直接抛出异常,如果任务一定要执行完成且不能丢弃则选择提交任务的线程执行线程。

3.synchronized的偏向锁、轻量级锁、重量级锁

偏向锁:在锁对象的对象头中记录当前获取该锁的线程id,该线程下次如果又来就可以直接获取

轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时锁是偏向锁;此时第二个线程来竞争锁,就会升级为轻量级锁,底层是通过自旋来实现的,不会堵塞线程

重量级锁:如果自旋次数过多还是没有获取到锁,则会升级为重量级锁,重量级锁会堵塞线程

4.线程池的原理
  1. 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用

  1. 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃

5.为什么要创建线程池?常用线程池
  1. 降低资源消耗,提高线程利用率

  1. 提高响应速度

  1. 提高线程的可控性

常用线程池:

  1. 单线程线程池

  1. 固定大小线程池

  1. 可缓存线程池

  1. 大小无限的线程池

6.简述创建线程池的几个参数
  1. 线程数量

  1. 线程最大数量

  1. 最大空闲时间

  1. 时间单位

  1. 任务队列

  1. 线程工厂

  1. 拒绝策略(当任务队列满了,再加入任务时就会执行该策略)

拒绝策略:

直接抛出异常(默认)

直接丢掉任务

将最先进入工作队列等待的任务丢弃,然后接受新的任务

谁调用了本线程池,则谁来执行该任务,保证的任务不会丢失

7.线程池的实现原理
8.线程池添加工作线程的流程
9.线程池的执行流程
  1. 创建一定数量的线程

  1. 添加任务到任务队列

  1. 线程从任务队列中获取任务并执行

  1. 执行完毕的线程返回线程池

10.线程池为何要构建空任务的非核心线程

若是核心线程数设置的为0,我们执行addWorker时,就会直接将任务添加到阻塞队列里面,此时是没有工作线程的,线程池的状态正常的情况下会添加一个空任务用于执行阻塞队列中的任务。

11.线程池中阻塞队列的作用

阻塞队列通过阻塞可以保留住当前想要继续入队的任务,因为相对于普通队列,阻塞队列具有阻塞的作用,就是让当前请求阻塞,当队列中有空地方则加入;

  1. 可以保留住想要继续入队的任务

  1. 可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源

  1. 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源

12.线程线程池复用的原理

核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法,把 run 方法当作和普通方法一样的地位去调用,相当于把每个任务的 run() 方法串联了起来,所以线程数量并不增加。

13.项目中哪些地方用到了线程池?

主业务与其他业务可以进行分割;无需等待

1、公告异步发送短信通知(发送一个任务,然后注入到线程池中异步发送)

使用步骤:

定义短信发送接口

短信发送实现类

构建线程池配置

controller模块触发

2、记录登录日志(需处理登录日志,但又不想耽误登录)

14.高并发解决方案
  1. html静态化

页面采用静态页面来实现,这个最简单的方法其实也是最有效的方法,因为效率最高、消耗最小的就是纯静态化的html页面;

2.图片服务器分离

图片是最消耗资源的,我们有必要将图片与页面进行分离,大型网站都会有独立的、甚至很多台的图片服务器。这样的架构可以降低提供页面访问请求的服务器系统压力,并且可以保证系统不会因为图片问题而崩溃

3.数据库集群、分库、分表

在面对大量访问的时候,数据库的瓶颈很快就能显现出来,这时一台数据库将很快无法满足应用,于是我们需要使用数据库集群或者库表散列

4.负载均衡

负载均衡将是大型网站解决高负荷访问和大量并发请求采用的高端解决办法

5.CDN加速

CDN的全称是内容分发网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度

猜你喜欢

转载自blog.csdn.net/qq_35056891/article/details/129652162