并发面试
CAS:
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题
解决方法: 时间戳、+1
JAVA内存模型:
缓存一致性问题:
CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致
处理器优化/指令重排序:
为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化
并发问题:并发的三个问题:原子性(处理器优化造成),可见性(缓存一致性造成),有序性(指令重排序造成) 。为了解决这些问题,定义了java内存模型,通过限制处理器优化和使用内存屏障解决问题】
模型:
栈堆等都是JVM的逻辑概念,在硬件架构中没有这些概念。 右图为硬件架构的概念
java内存模型的定义:
- 所有的变量都存储在主内存(Main Memory)中。
- 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。
- 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。
- 不同的线程之间无法直接访问对方本地内存中的变量。
就是说,每个线程有自己的localmemory用来存放需要的共享变量,线程之间不能直接通讯
如果线程1和线程2都对共享变量进行+1操作,最终的结果会是3而不是2。不会存在缓存一致性问题。
为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock
, unlock
, read
, load
,use
,assign
, store
, write
。
volatile:
volatile
修饰的共享变量,就具有了以下两点特性:1.保证不同线程对该变量的内存可见性 2.禁止指令重排序
内存可见性:
由于CPU和主存之间的速度差异,JVM定义了一种java内存模型规范,通过限制处理器优化和内存屏障来解决一致性问题。简述一下java内存模型(线程有工作线程,用来存主存中共享变量的值。执行时,先从主存取变量,保存到local,将local的变量副本值传给处理器进行操作,结果保存回local,local传回主存修改值)如果写回主存的速度慢,在此期间有其他线程修改操作,就会造成缓存不一致问题。JMM
主要就是围绕着如何在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的
原子性:
操作如果执行不可中断,要做一定做完,要么就不会执行。i=3是原子性。 像i++就不是,先读,再加
可见性:
当一个变量被 volatile
修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。通过 synchronized
和 Lock
也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是 synchronized
和 Lock
的开销都更大。
有序性:
JMM
是允许编译器和处理器对指令重排序的,但是规定了 as-if-serial
语义,即不管怎么重排序,程序的执行结果不能改变
Volatile如何满足三大特性:
对一个 volatile
域的写, happens-before
于后续对这个 volatile
域的读,那么当我读变量时,总是能读到它的最新值,这里最新值是指不管其它哪个线程对该变量做了写操作,都会立刻被更新到主存里,我也能从主存里读到这个刚写入的值
从内存语义上来看
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
Volatile能保证原子性吗?
不能保证原子性。如果对volatie变量做++操作,是读和写的两步操作,如果读的过程中被阻塞, 其他线程是不可见的。修改才会其他线程可见。、
可以借助synchronized
, Lock
以及并发包下的 atomic
的原子操作类了,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作
Volatile的底层机制:
内存屏障
1 . 重排序时不能把后面的指令重排序到内存屏障之前的位置
2 .使得本CPU的Cache写入内存
3 . 写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。
Volatile的使用?
flag状态量标记
单例模式的实现,典型的双重检查锁定(DCL)。这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给 instance
加上了 volatile
。
ThreadLocal:
ThreadLocal
通过空间换时间的方案,规避了竞争问题,因为每个线程都有属于自己的变量,变量只对当前线程可见。
1.线程如何维护自己的变量副本?
Thread类代码:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap的Entry:
Entry(ThreadLocal<?> k, Object v)
Thread
用类似 Map
的 ThreadLocal.ThreadLocalMap
数据结构来存储以 ThreadLocal
类型的变量为 Key
的数值,并用 ThreadLocal
来存取删,操作 ThreadLocalMap
。
- 当我们定义一个
ThreadLocal
变量时,其实就是在定义一个Key
- 当我们调用
set(v)
方法时,就是以当前ThreadLocal
变量为key
,传入参数为value
,向ThreadLocal.ThreadLocalMap
存数据 -
当我们调用
get()
方法时,就是以当前ThreadLocal
变量为key
,从ThreadLocal.ThreadLocalMap
取对应的数据
ThreadLocalMap的Hash冲突解决办法
采用线性探测的方式,根据 key
计算 hash
值,如果出现冲突,则向后探测,当到哈希表末尾的时候再从0开始,直到找到一个合适的位置。
这种算法也决定了 ThreadLocalMap
不适合存储大量数据。
ThreadLocalMap的扩容问题
ThreadLocalMap
初始大小为 16
,加载因子为 2/3
,当 size
大于 threshold
时,就会进行扩容。
扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的 entry
并将其插入到新的hash数组中,在扩容的时候,会把 key
为 null
的 Entry
的 value
值设置为 null
,以便内存回收,减少内存泄漏问题。
线程池:
三种创建方式:
extends Thread (java没法双重继承,这种方法尽量避免)
implements Runnable
implements Callable<>
线程池的两种类型:
守护线程:
在start()之前,thread.setDaemon(true)守护线程顾名思义是用来守护的,是给所有得非守护进程提供服务的,所以在 jvm
执行完所有的非守护进程之后, jvm
就会停止,守护线程也不会再运行,最典型的守护线程就是 java 的垃圾回收机制 ( GC
)。
非守护线程:
java 线程默认设置是非守护线程 thread.setDaemon(false)
。当主线程运行完之后,只要主线程里面有非守护线程 jvm 就不会退出,直到所有的非守护线程执行完之后 jvm 才会退出。如果把一个线程设置成守护线程,则 jvm 的退出就不会关心当前线程的执行状态。、
线程池的使用:
所以使用线程池主要有以下两个好处:
-
减少在创建和销毁线程上所花的时间以及系统资源的开销 。
-
如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!