目录
[说一说自己对于 synchronized 关键字的了解](#说一说自己对于 synchronized 关键字的了解)
[怎么使用 synchronized 关键字](#怎么使用 synchronized 关键字)
说一说自己对于 synchronized 关键字的了解
-
synchronized关键字解决的是多个线程之间访问资源的同步性, synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
-
在 Java 早期版本中, synchronized属于重量级锁,效率低下, 因为锁监视器(monitor)是依赖于底层的操作系统来实现的。 如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,这也是为什么早期的 synchronized 效率低的原因.
在JDK1.6 考虑不同竞争强度的场景 对synchronized进行了优化,无锁->偏向锁->轻量级锁(自旋锁)->重量级锁
怎么使用 synchronized 关键字
-
修饰非静态方法, 锁的是this(当前对象), 也就是一个对象用一把锁
-
修饰静态方法, 锁的是 类.class , 也就是类的所有对象共用一把锁
-
修饰代码块, 括号里写什么就锁什么.
synchronized原理
synchronized 的实现原理依赖于 JVM 的 Monitor(监视器锁)和对象头 Mark Word。
修饰方法和代码块的实现方式略有不同:
-
synchronized 修饰方法时,会在方法的访问标志中加上 ACC_SYNCHRONIZED 标志。线程进入方法前,JVM 检查这个标志,有的话就得先拿到对象的监视器锁才能执行。
-
synchronized 修饰代码块时,编译器会在代码块前后插入 monitorenter 和 monitorexit 字节码指令。monitorenter 就是加锁,monitorexit 就是解锁,而且编译器会自动生成异常处理的 monitorexit,保证抛异常也能正常解锁。
synchronized 代码块编译后会在代码块入口插入 monitorenter 指令,在正常出口和异常出口各插入一条 monitorexit 指令。线程执行 monitorenter 时尝试获取对象的 monitor 锁,成功则进入临界区,失败则阻塞等待。执行 monitorexit 时释放锁并唤醒等待队列中的线程。

前置知识
每个 Java 对象 在内存中分为三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)对象头中的 Mark Word:存储对象的哈希码、锁状态、GC 分代年龄等.
每个对象的Mark Word会关联一个 Monitor(监视器锁
Java的同步机制是分层抽象的。
-
在语言层面,synchronized 是一个关键字
-
在 JVM 字节码层面,synchronized 对应的是 monitorenter 和 monitorexit 两条字节码指令,表示进入临界区,告诉JVM在此处需要加锁和解锁。
-
在 JVM 逻辑层面,JVM内部通过 Monitor (监视器锁)来管理线程的竞争和等待

-
每个锁对象都关联一个Monitor,内部通过三个逻辑区域管理线程竞争
-
Owner:当前持有锁的线程
-
EntryList:等待锁的阻塞线程队列
-
WaitSet :调用
obj.wait()后进入等待状态的线程队列 -
具体流程:
-
当线程执行 monitorenter 时,JVM会检查 锁对象 的Monitor
-
若 Owner 为空,线程成为 Owner
-
若 Owner 被占用,线程进入 EntryList 阻塞
-
-
当线程执行 monitorexit 时,JVM释放锁,并唤醒 EntryList 中的线程,让它们去竞争锁
-
调用 obj.wait() 的线程会释放锁并进入 WaitSet,等待 obj.notify() 唤醒
-
- 在 操作系统 层面,需要从用户态切换到内核态,向操作系统底层申请互斥量mutex,性能开销很大,因此称其为重量级锁。
synchronized可重入如何实现
Monitor(监视器锁)中的重入次数字段, 重入一次加一次, 释放一次减一次, 减到0则释放完毕
锁升级过程
synchronized 由于涉及到操作系统层面申请互斥量mutex,还涉及到线程的阻塞和唤醒,都需要从用户态切换到内核态,性能开销很大,因此称其为重量级锁
在JDK1.6 考虑不同竞争强度的场景 对synchronized进行了优化
无锁->偏向锁->轻量级锁( 自旋锁 )->重量级锁

无锁->偏向锁
最初是无锁的状态,当一个synchronized代码块被线程首次进入时,JVM会将锁标记为偏向锁,且会在锁对象头中记录下该线程的ID。如果该线程再次请求同一把锁(即重入锁),可通过比较线程ID它会直接获得锁
偏向锁->轻量级锁
如果有其他线程来请求获取锁,此时偏向锁就会被撤销,两个线程会通过CAS的方式自旋尝试获取锁,获取锁后JVM会将锁标记为轻量级锁。获取锁的流程:
-
将锁对象头中的Mark Word复制到线程栈中的锁记录结构中。
-
尝试通过CAS操作将锁对象头的Mark Word的内容更新为指向锁记录的指针。如果这个更新操作成功,那么这个线程就成功获取了这个对象的轻量级锁。
加锁过程:线程在自己的栈帧中创建一个 Lock Record,把对象的 Mark Word 拷贝进去,然后 CAS 把对象头指向这个 Lock Record。成功就拿到锁了,失败说明有竞争,膨胀成重量级锁。
轻量级锁->重量级锁
当有较多线程短时间内请求获取锁,意味着当前竞争比较激烈,而轻量级锁使用的CAS的方式会占用CPU,因此此时会发生锁膨胀,JVM将锁标记为重量级锁,重量级锁中竞争失败的锁会陷入阻塞,不会占用CPU,而重量级锁的原理(请看上方synchronized的原理)