JUC-synchorized与锁原理、锁的升级与膨胀

syn-ed

是一个可重入、不公平的重量级锁synchronized使用对象锁保证了临界区代码的原子性,无论使用synchorized锁的是代码块还是方法,其本质都是锁住一个对象。

  • 同步代码块,锁住的是括号里的对象
  • 同步方法
    • 普通方法,锁住的是当前实例对象,即this
    • 静态方法,锁住的是当前类对象,即class对象
java 复制代码
// 同步代码块
synchorized(锁对象) {
}

// 普通方法
class Test{
    public synchorized test() {}
}
// 等价于
class Test{
    public void test() {
        synchronized(this) {}
    }
}

// 静态方法
class Test{
	public synchronized static void test() {}
}
// 等价于
class Test{
    public void test() {
        synchronized(Test.class) {}
	}
}

变量线程安全性

  • 成员变量和静态变量
    • 如果没有被共享,那么一定是线程安全的
    • 如果被共享:
      • 只有读操作,一定是线程安全的
      • 有读写操作,存在并发问题
  • 局部变量
    • 局部遍历是线程安全的
    • 但局部变量引用的对象则不一定
      • 如果该对象没有逃离方法的作用访问,它是线程安全的
      • 如果该对象逃离方法的作用范围,存在并发问题

常见的线程安全类

  • String
  • Integer等包装类
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类
    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的,多个线程调用它们同一个实例的某个方法时,是线程安全的。

锁原理

Monitor

监视器或管程,每一个java对象都可以关联一个Monitor对象,使用synchronized给一个对象加锁(重量级锁),该对象的对象中的Mark Word就被设置指向一个Monitor对象的指针,这其实也就是重量级锁的加锁过程。

Mark Word

java对象的对象头由Mark Word、类型指针、数组长度(如果该对象是一个数组)组成。Mark Word的长度由32bit/64bit。Mark Word里默认存储对象的HashCode、分代年龄和锁标记位

  • 32位的Mark Word

  • 64位的Mark Word:

Monitor的工作流程

一个对象对应一个Monitor对象。

  • 开始时Monitor中Owner为null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner,obj对象的Mark Word指向Monitor ,把对象原有的Mark Word存入线程栈中的锁记录
  • 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
  • Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
  • 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
  • WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)

锁升级

随着竞争的增加,只能锁升级,不能降级

java 复制代码
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	

偏向锁

在大多数情况下,锁总是由一个线程多次获得,让线程获得锁的代价更低而引入了偏向锁。就和名字一样,是偏向的:

  • 当线程第一次获得锁对象时,其进入偏向状态,该对象的后三位是101,同时使用CAS操作将线程ID记录到Mark Word。如果CAS操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程ID是自己的就表示没有竞争,就不需要再进行任何同步操作。
  • 当另一个线程也尝试获取这个锁对象时,也会使用CAS进行替换,此时一定失败,偏向状态就会结束,撤销偏向后恢复到未锁定或轻量级锁状态。

在java中是默认开启偏向锁的 ,也就是说在一个对象创建的时候,其Mark Word的后三位是101 ,其余值均为0;当调用这个对象的hashCode时,就再也无法进入偏向状态了,即后三位是001。这是因为Mark Word会被hashCode占用。

偏向锁的撤销

  • 第一点就是我们前边提过的,调用该对象的hashcode方法。
  • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用 wait/notify,需要申请 Monitor,进入 WaitSet

轻量级锁

一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),轻量级锁的使用对我们程序员来说,也是使用syn-ed来完成,只是底层的实现对我们透明的。

加锁过程

当某一个线程对一个对象进行加锁时,会在栈帧中创建一个锁记录(Lock Record),并使用==CAS将对象的Mark Word中的信息保存到该锁记录中,而Mark Word中就记录该锁记录的地址,以及锁标志位(00)。==这样就完成了加锁。但是当CAS失败的时候,此时会有两种情况导致失败:

  • 它线程已经持有了该Object的轻量级锁,我又想去获取,这时表明有竞争,进入锁膨胀过程在膨胀之前还有一个自旋的过程
  • 线程自己执行synchronized锁重入,栈帧中还会存在一条Lock Record作为重入的计数,但是每次重入都会有CAS,所以才引入了偏向锁进行优化

解锁过程(当退出synchronized代码块)

  • 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
  • 如果锁记录的值不为null,这时使用CAS将 Mark Word的值恢复给对象头
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

重量级锁

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

  • 当线程1使用CAS对一个对象obj进行加锁时,发现线程1已经持有该轻量锁,此时就会失败并导致锁膨胀
  • 然后就会为obj对象申请Monitor锁,通过obj对象头获取到持锁线程,将Monitor的 Owner置为线程0,将obj的对象头指向重量级锁地址,然后自己进入Monitor的EntryList而BLOCKED。
  • 当线程0释放锁时,使用CAS将Mark Word的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor地址找到Monitor对象,设置Owner为 null,唤醒EntryList中BLOCKED线程
相关推荐
c238567 分钟前
把 C++ 内存分配拆透:new 与 malloc 的三层血缘
开发语言·c++·算法
moonsims1 小时前
星闪在跨域无人化系统作用
开发语言·php
Iruoyaoxh2 小时前
栈和队列~
java·开发语言
Hrain-AI2 小时前
2026 企业 AI 编程智能体实战:Codex 与 Claude Code
开发语言·人工智能·kotlin
wdfk_prog2 小时前
嵌入式面试真题第 13 题:单硬件 Timer 下的高精度多路软件定时器架构设计
c语言·开发语言·面试·职场和发展·嵌入式
oort1232 小时前
吃上了自家的细糠,还挺丝滑,用起来手感还行,OortCloud发布新版AI编程平台,下载 OortCodex,Token多,免费薅
大数据·开发语言·人工智能·ai编程
slandarer3 小时前
MATLAB | 泰勒图绘制,支持各种角度范围
开发语言·数学建模·matlab·nature·顶刊·泰勒图
Xin_ye100863 小时前
第三章:内存泄漏的常见“案发现场”
c#·wpf
mct1233 小时前
c++ iconv 字符utf-8转换gb2312失败
开发语言·c++
Marst Code4 小时前
Python 3.9 已停止维护!从 3.9 到 3.14 全版本深度对比,生产环境该选哪个?
开发语言·python