内存模型(JMM)

JMM(java memory model):定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、原子性的规则和保障

原子性

synchronized加锁

可见性

不同线程可能在各自的 CPU 缓存中持有变量的副本,导致读到的值不一致

解决:volatile 易变关键字

他可以用来修饰成员变量和静态变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取他的值,线程操作volatile变量都是直接操作主存

synchronized也可以保证可见性,但是属于重量级操作,性能相对较低

有序性

同一个线程,JVM会在不影响正确性的前提下,调整语句的执行顺序

volatile 保证"前面的先发生,后面的后发生"

volatile修饰变量会禁止指令重排

JDK5以上版本volatile才真正有效

happens-before

happens-before规定了哪些写操作对其他线程的读操作可见,他是可见性和有序性的一套规则总结

  • 线程解锁之前对变量的写,对于接下来对m加锁的其他线程对该变量的都可见
  • 线程对volatile变量的写,对接下来 其他线程对该变量的读可见
  • 线程start前对变量的写,对该线程开始后对该变量的读可见
  • 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见
  • 对变量默认值的写,对其他线程对该变量的读可见
  • 具有传递性

变量都是指成员变量,静态变量(共享变量)

CAS

原子类 compare and swap,它体现的一种乐观锁的思想

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下

竞争激烈,重试频繁发生,效率受影响

juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,底层采用CAS技术+volatile来实现

synchronized优化

每个对象都有对象头(包含class指针和Mark Word)。Mark Word平时存储这个对象的哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针,线程ID等内容

Mark Word 中包含锁标志位用于标识当前锁状态,在不同状态下其余位的含义不同;当对象处于偏向锁时存储线程ID,处于轻量级锁时存储指向线程栈中锁记录的指针,处于重量级锁时存储指向 Monitor 的指针,这些信息在同一时刻只会存在一种,而不是同时存在。

synchronized 锁在运行过程中整体是向更高开销方向演进的,轻量级锁在竞争加剧时会膨胀为重量级锁且不会降级,而偏向锁在特定情况下可能被撤销并转为轻量级锁或无锁状态。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作,java6引入了偏向锁来进一步优化,只有第一次使用CAS将线程ID设置到对象的Mark Work头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS

  • 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要运行到安全点暂停(STW)
  • 访问对象的hashCode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,只需重置对象的threadID
  • 撤销偏向和重偏向都是批量进行的,一类为单位
  • 如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的
  • 可以使用-XX:-UseBiasedLocking禁止偏向锁

轻量级锁

一个对象虽然有多个线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化

每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

重量锁

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,说明成功概率大,多自旋几次

加锁流程

synchronized 加锁时,首先尝试偏向锁,将线程ID写入对象头;若存在竞争,则升级为轻量级锁,将对象头的 Mark Word 拷贝到线程栈中的 Lock Record,并通过 CAS 将对象头替换为指向该 Lock Record 的指针;若 CAS 失败,会判断是否为当前线程重入,若是则直接进入临界区,否则自旋或升级为重量级锁;重量级锁通过 Monitor 实现,竞争线程会进入阻塞队列;释放锁时,轻量级锁通过 CAS 恢复对象头,若失败说明已膨胀为重量级锁,由 Monitor 机制唤醒等待线程。

其他优化

  1. 减少上锁时间
    同步代码块中尽量短
  2. 减少锁的粒度
    将一个锁拆分为多个锁提高并发度(例如hash表添加元素我不需要锁整个表,只需要锁对应的桶)
  3. 锁粗化
    多次循环进入同步代码块不如同步代码块中多次循环
    另外JVM可能做如下优化,把多次append的加锁操作粗化为一次
  4. 锁消除
    JVM会进行代码的逃逸分析,例如某个加锁对象时方法内部局部变量,不会被其他线程所访问到,这时候就会被JIT忽略掉所有同步操作
  5. 读写分离
    CopyOnWriteArrayList
    ConyOnWriteSet
相关推荐
m0_463672205 小时前
mysql数据库如何进行逻辑备份与物理备份对比_优缺点分析
jvm·数据库·python
xiaoming00185 小时前
JAVA项目打包部署运维全流程(多服务、批量)
java·linux·运维
2401_867623985 小时前
SQL如何进行分组后字符串拼接_使用GROUP_CONCAT或STRING_AGG
jvm·数据库·python
kexnjdcncnxjs5 小时前
MySQL触发器无法触发的原因分析_MySQL触发器排查指南
jvm·数据库·python
拾-光6 小时前
【Git】命令大全:从入门到高手,100 个最常用命令速查(2026 版)
java·大数据·人工智能·git·python·elasticsearch·设计模式
无人不xiao6 小时前
springBoot 实现 接口进度条
java·spring boot·后端
pkowner6 小时前
若依分页问题及解决方法
java·前端·算法
2301_781571427 小时前
NumPy张量缩并怎么用_np.einsum()爱因斯坦求和约定高级索引魔法
jvm·数据库·python
largecode7 小时前
如何让电话显示店名?来电显示店铺名称,提升有效接通率
java·开发语言·spring·百度·学习方法·业界资讯·twitter
xuhaoyu_cpp_java7 小时前
SpringMVC学习(五)
java·开发语言·经验分享·笔记·学习·spring