多线程与高并发面试题

线程和进程的区别

进程:当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。

进程分为多实例进程(浏览器、文本文件等)和单实例进程(企业微信等)。

程序由指令数据组成,但这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。

线程:一个线程就是一个指令流,将指令流中一条条指令以一定的顺序交给CPU执行。在操作系统层面,线程是cpu调度的基本单位

一个进程之内可以分为一到多个线程。线程也可以被视为轻量级进程。

两者对比
  • 进程是正在运行的程序的实例,进程中包含了线程,每个线程执行不同的任务。
  • 不同的进程使用不同的内存空间,在当前进程下的所有进程可以共享内存空间。
  • 线程更轻量,线程上下文切换成本一般要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程,即cpu时间片的切换)。

并行和并发有什么区别?

1. 单核CPU的并发
  • 单核CPU下的线程实际上还是串行执行的
  • 操作系统有一个组件叫做任务调度器,将cpu的时间片(window下时间片最小约15毫秒)分给不同的程序使用,只是由于CPU在线程(时间片)的切换非常快,人类感觉是同时运行的。
  • 总结为一句话就是:微观上是串行的,宏观上是并行的
  • 一般将这种线程轮流使用CPU的做法称为并发。
2. 多核CPU的并行
  • 每个核都可以调度运行线程,这时候线程可以是并行的。
总结

并发(concurrent)是多个线程轮流使用一个或者多个CPU。
并行(paraller)是同一时刻,两个进程,同时运行在两个 cpu 逻辑核心上。

创建线程的方式

共有四种方式可以创建线程,分别是:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 线程池创建线程(项目中使用方式)

runnable和callable有什么区别?

  1. Runnable接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
  2. Callable接口的call()方法允许抛出异常;而Runable接口的run()方法的异常只能在内部消化,不能继续上抛。

在启动线程的时候,可以使用run方法吗?run()和start()有什么区别?

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的的代码逻辑。start方法只能被调用一次。
run():封装了要被线程执行的代码(只是一个普通的方法),可以被调用多次。

线程包括哪些状态,状态之间是如何变化的?

线程的状态可以参考JDK中的Thread类中的枚举State

public enum State {
	// 尚未启动的线程的线程状态
	NEW,
	// 可运行的线程的线程状态
	RUNNABLE,
	// 线程阻塞等待监视器锁的线程状态
	BLOCKED,
	// 等待线程的线程状态
	WAITING, 
	// 具有指定等待时间的等待线程的线程状态
	TIMED_WAITING,
	// 已终止线程的线程状态,线程已完成执行
	TERMINATED;
}

在这里插入图片描述

线程状态之间是如何变化的?

  • 创建线程对象是新建状态(new)
  • 调用了start()方法转变为可执行状态(Runnable)
  • 线程获取了CPU的执行权,执行结束是终止状态(Terminated)
  • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换到其他状态
    • 如果没有获取锁(synchronized或lock)进入阻塞状态(Blocked),获得锁再切换为可执行状态
    • 如果线程调用了wait()方法进入等待状态(Waiting),其他线程调用notify()
      唤醒后可切换为可执行状态
      如果线程调用了sleep(50)方法,进入计时等待状态(TIMED_WAITING,),到时间后可切换为可执行状态。

新建T1、T2、T3三个线程,如何保证它们按照顺序执行?

可以使用线程中的join方法解决。
在这里插入图片描述

notify()和notifyAll()有什么区别?

notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个wait线程

java中wait和sleep方法的不同?

  • 共同点:
    wait(), wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态。
  • 不同点
    1. 方法归属不同:sleep(long)是Thread的静态方法,而wait(), wait(long)都是Object的成员方法,每个对象都有
    2. 醒来时机不同:
      • 执行sleep(long) 和wait(long) 的线程都会在等待相应毫秒后醒来
      • wait(long) 和wait()还可以被notify()唤醒,wait()如果不唤醒就一直等待下去
      • 它们都可以被等待唤醒
    3. 锁特性不同
      • wait 方法的调用必须先获取wait对象的锁,而sleep则无此限制。
      • wait 方法执行后会释放对象锁,允许其他线程获得该对象锁
      • 而sleep如果在synchronize代码块中执行,并不会释放对象锁

如何停止一个正在运行的线程?

有三种方式可以停止线程:

  • 使用退出标志flag,使线程正常退出,也就是当run方法完成就线程终止。
  • 使用stop方法强行终止(不推荐)
  • 使用interrupt方法中断线程
    • 打断阻塞的线程(sleep、wait、join)的线程,线程会抛出InterruptedException异常
    • 打断正常的线程,可以根据打断状态标记决定是否退出线程。

synchronized关键字的底层原理

  • synchronized【对象锁】基本的使用方式:采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个【对象锁】时就会阻塞住。
  • 它的底层由monitor实现,monitor是jvm级别的对象(c++实现),线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
  • 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于waiting状态的线程。
    在这里插入图片描述

Monitor实现的锁属于重量级锁,说说锁升级?

首先来进行回答:

java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

  • 重量级锁:底层使用的是Monitor实现,里面设计了用户态和内核态的切换、进程上下文的切换,成本较高,性能比较低。
  • 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级锁修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。
  • 偏向锁:一段很长时间内都只被一个线程使用锁,可以使用偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的id即可,而不是开销相对较大的CAS命令。
    注:一旦锁发生了竞争,都会升级为重量级锁
  • 在JDK1.6引入了两种新型锁机制:偏向锁轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

补充1:对象的内存结构
在这里插入图片描述
补充2:MarkWord和Hotspot的实现
在这里插入图片描述

在这里插入图片描述

Synchronized的作用有哪些?

  1. 在Java中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。
  2. 在Java早期版本中,synchronized属于重量级锁,效率不高。因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮助实现,而操作系统实现线程之间的切换时需要从"用户态" -> “内核态”,这个切换时间成本是相对较高的,这也是早期 synchronized 效率一般的原因
  3. 从Java 6开始,官方对 JVM 层面对 synchronized 进行了较大优化,jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁粗化 … 来减少锁操作的开销。

谈谈对java内存模型JMM(Java Memory Model)的理解?

  • java内存模型定义了共享内存多线程读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享内存(主内存)
  • 线程和线程之间是相互隔离,线程和线程交互需要通过主内存

CAS知道吗?

  • CAS全称叫做 Compare and swap (比较和交换),CAS无锁机制是乐观锁的一种,也叫自旋锁,可以保证线程操作的原子性。CAS假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则重新再来一次,无限循环地进行对比,直到没有冲突为止。
  • 在操作共享变量时,使用自旋锁,效率更高。CAS指令需要3个操作数:

    需要更新的变量(V)(主内存)
    旧的预期值(E)(本地内存)
    新值(B)

  • CAS底层实现
    CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令

CAS指令执行时,首先比较内存位置V(主内存)处的值和E(本地内存)的值是否相等(冲突检测),如果相等,就用新值B(新值)覆盖V(更新操作),否则,就什么也不做。所以,一般循环执行CAS操作,直到成功为止。执行流程如下图:
在这里插入图片描述

怎么解决ABA问题?

加版本号、布尔类型

乐观锁和悲观锁的区别

在这里插入图片描述

请谈谈你对volatile的理解?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么就具备了两层语义:

  1. 保证线程间的可见性
    用volatile修饰共享变量,能够防止编译器等优化的发生,让一个线程对共享变量的修改对另一个线程可见。
    在这里插入图片描述

  2. 禁止进行指令重排序
    用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到防止重排序的效果。volatile原理就是加了一些屏障,使得屏障后的代码一定不会比屏障前的代码先执行,从而实现有序性。
    在这里插入图片描述

volatile使用技巧:

  • 写变量让volatile修饰的变量在代码的最后位置
  • 读变量让volatile修饰的变量在代码最开始位置

什么是AQS?

AQS基本介绍

ASQ全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。
AQS的常见实现类:

  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

Synchronized和AQS的区别:

  • 语言实现方面:Synchronized是关键字,基于C++语言实现;AQS是基于java语言实现。
  • 锁机制方面:Synchronized是一种悲观锁,可以实现自动释放;AQS也是一种悲观锁,但是需要手动开启和关闭。
  • 锁竞争方面:Synchronized锁竞争激烈时会升级为重量级锁,性能差;AQS锁竞争激烈的情况下,提供了多种解决方案。

AQS基本工作机制

  • AQS内部维护了一个先进先出的双向队列,队列中存储的是排队的线程。
  • AQS内部还有一个属性state,这个state就相当于是一个资源,默认是无锁状态0,如果队列中有一个线程修改成功将state改为1,则当前线程就等于获得了资源
  • 在对state状态进行修改的时候采用CAS操作,保证多个线程修改的情况下的原子性。
    在这里插入图片描述
多个线程共同去抢一个资源是如何保证原子性的呢?

通过CAS设置state状态,保证操作的原子性。
在这里插入图片描述

AQS是公平锁吗?还是非公平锁呢?

AQS既可以是公平锁,也可以是非公平锁。

  • 新的线程与队列中等待的线程共同抢占资源,是非公平锁
  • 新的线程到队列中等待,只让队列中的head线程获得锁,是公平锁

ReentrantLock的实现原理

ReentrantLock翻译过来就是可重入锁,表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用lock不会再阻塞。相对于Synchronized具有如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 支持重入,与Synchronized一样

ReentrantLock主要利用AQS队列+CAS来实现。
支持公平锁和非公平锁(无参默认非公平锁,因为在多线程访问的情况下,公平锁表现出较低的吞吐量)
在这里插入图片描述

Synchronized和Lock有什么区别?

  • 语法层面:

    • Synchronized是关键字,基于C++语言实现;Lock是接口,源码由jdk提供,用java语言实现。
    • 使用Synchronized时,退出同步代码块锁会自动释放,而使用lock时,需要手动调用unlock方法进行释放锁。
  • 功能层面:

    • 两者均属于悲观锁,都具备基本的互斥、同步、锁重入功能。
    • Lock提供了许多Synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
    • Lock有适合不同场景的实现,如ReentrantLock, ReentrantReadWriteLock(读写锁)
  • 性能层面

    • 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不错
    • 在竞争激烈时,Lock的实现通常会提供更好的性能。

    死锁产生的条件是什么?

一个线程需要同时获取多把锁,这时就会发生死锁
四个必要条件:

  1. 互斥
  2. 请求和保持
  3. 不可剥夺
  4. 相互获取

如何进行死锁诊断?

使用jdk自带的工具:jps和jstack

  • jps:输出JVM中运行的进程状态信息
  • jstack:查看java进程中线程的堆栈信息
  • 其他:可视化工具jconsole和visualVM故障处理工具

导致并发程序出现问题的根本原因是什么?(或Java程序中怎么保证多线程的执行安全?)

Java并发编程具有三大特性:

  • 原子性:一个线程再CPU中的操作不可暂停,也不可中断,要么执行完成,要么不执行。(原子操作就是: 不可中断的一个或者一系列操作, 也就是不会被线程调度机制打断的操作, 运行期间不会有任何的上下文切换
    • 保证原子性的操作:
      • synchronized同步加锁
      • JUC里面的lock加锁
  • 可见性:内存的可见性就是让一个线程对共享变量的修改对另一个线程可见。
    • 加volatile修饰共享变量即可。
    • 也可以使用锁解决
  • 有序性:
    指令重排:处理器为了提高程序的运行效率,可能会对输入代码进行优化,它不保证程序中每个语句的执行先后顺序同代码中的顺序一致,但是它会保证最终执行结果和代码顺序执行的结果是一致的。
    禁止指令重排:加volatile修饰

线程池的核心参数或者执行原理知道嘛?

在这里插入图片描述

在这里插入图片描述

线程池中有哪些常见的阻塞队列?

在这里插入图片描述
在这里插入图片描述

如何确定核心线程数

应用程序可以分为两种类型:

  • IO密集型任务(这类任务通常涉及大量的读取和写入操作,而CPU的计算需求相对较少):文件读写、DB读写、网络请求等
    • 设置核心线程数大小为2N+1 (N是cpu核数)
  • CPU密集型任务(这类任务通常涉及大量的计算、逻辑判断、数据处理等操作):计算型代码、Bitmap转换、Gson转换
    • 设置核心线程数大小为N+1

回答:

  1. 当前项目是高并发,任务试行时间短,可视为是在执行CPU密集型任务。设置核心线程数大小为N+1 ,减少线程上下文的切换。
  2. 当前项目并发不高,任务执行时间长,
    • IO密集型任务 ->2N+1
    • 计算密集型任务->N+1
  3. 并发高,业务执行时间长,解决这类任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器数量是第二步,至于线程池的设置,参考2

线程池的种类有哪些?

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见的有四种:

  1. 创建固定线程数的线程池,可控制最大并发数,超出的线程再队列中等待。适用于任务量已知,相对耗时的任务。
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    
    - 核心线程数与最大线程数一样,没有救急线程
    - 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE 
    
  2. 单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序(FIFO)执行。适用于按照顺序执行的任务。
    	ExecutorService exec = Executors.newSingleThreadExecutor();
    	
    	- 核心线程数与最大线程数都是1
    	- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE 
    	```
    
  3. 可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。适合任务数比较密集,但每个任务执行时间较短的情况。
    	ExecutorService exec = Executors.newCachedThreadPool();
    	
    	- 核心线程数为0
    	- 最大线程数是Integer.MAX_VALUE 
    	- 阻塞队列是SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
    	```
    
  4. 提供了“延迟”和“周期执行”功能的线程池,可以执行延时任务的线程池,支持定时及周期性任务执行。
    	ExecutorService exec = Executors.newScheduledThreadPool(2);
    	
    	- 核心线程数为2(可自由设置)
    	- 最大线程数是Integer.MAX_VALUE 
    	```
    

为什么不建议使用Executors创建线程池

在这里插入图片描述

线程池使用场景(CountDownLatch、Future)?或者 你们项目中哪里用到了多线程?(后续补充。。。)

  • CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或多个线程,等待其他线程完成某个事情后才能执行)
    • 其中构造参数用来初始化等待计数值
    • await()用来等待计数归零
    • countDown()用来让计数减一

如何控制某个方法允许并发访问线程的数量

Semaphore信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量使用场景:
通常用于那些资源有明确访问数量限制的场景,常用于限流。
使用步骤:

  1. 创建Semaphore对象,可以给一个容量。
  2. Semaphore.acquire():请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会被阻塞,直到其他线程释放了信号量)
  3. Semaphore.release():释放一个信号量,此时信号量个数+1

谈谈你对ThreadLocal的理解?

  1. ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发冲突的问题。
    (用人话说,就是ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题。)
  2. ThreadLocal同时实现了线程内的资源共享。
    第3点和第4点参照如下回答:
    在这里插入图片描述

ThreadLocal的实现原理

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现数据隔离。

你知道ThreadLocal内存泄露的问题吗?

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value线程为线程变量的副本。(弱引用:内存不够的时候,优先回收
在这里插入图片描述
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值