问题分析

i++的JVM字节码指令

三个步骤,非原子的操作

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 将int常量1压入操作数栈
  3. iadd // 自增

i—的JVM字节码指令

三个步骤,非原子的操作

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 将int常量1压入操作数栈
  3. isub // 自减

临界区

  • 在多个线程对共享资源读写操作实发生指令交错,就会出现问题
  • 一段代码块,如果存在对共享资源的多线程读写操作,这段会发生指令交错的代码块:临界区
  • 其资源成为临界资源

    1. private static int counter = 0;//临界资源
    2. public static void increment() { //临界区
    3. counter++;
    4. }
    5. public static void decrement() {//临界区
    6. counter--;
    7. }

    竞态条件(Race Condition)

    多个线程在临界区执行,由于代码的执行序列不停而导致结果无法预测,称为发生了竞态条件
    就是代码胡乱执行了
    防止竞态的发生的手段

  • 阻塞式的解决方案: synchronized Lock

  • 非阻塞式的: 原子变量 CAS范式

注意

  1. Java互斥和同步都可以使用Synchronized实现,但是有区别
  2. 互斥:保证临界区的竞态条件发生,同一时刻只有一个线程执行临界区
  3. 同步:由于线程执行先后不同,需要一个线程等待其他线程运行到某个点

Synchronized的使用

synchronize:同步块 Java提供的一种原子性内置锁
Java对象都可以把它当做同步锁使用,
Java内置的使用者看不到的锁称为内置锁,也叫监视器锁

加锁方式:

image.png

  1. //方式一: 在方法上
  2. public static synchronized void increment() {
  3. counter++;
  4. }
  5. //方式二: 在代码块上
  6. public static void increment() {
  7. synchronized (lock){
  8. counter++;
  9. }
  10. }

Synchronized的底层原理

synchronized:JVM内置锁,基于Monitor机制实现,依赖底层系统互斥的原语mutex(互斥量)
1.5之前:重量锁,
1.5之后:优化了,锁粗化lock coarsening 锁消除,轻量级锁,偏向锁,自适应自旋技术引入,性能与lock持平

synchronized的字节指令

方法上的:
image.png
image.png
代码中的:
image.png

Monitor(管程/监视器)

管程:管理共享变量,以及对共享变量操作的过程,让他们支持并发。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分
三种不同的管程模型:

  1. Hasen模型
  2. Hoare模型
  3. HESA模型

    Mesa 模型

    image.png
    引入条件变量概念:每个条件变量对应一个等待队列。
    条件变量和等待队列的作用是解决线程之间的同步问题
    wait的正确姿势
    1. while(条件不满足) {
    2. wait();
    3. }
    唤醒的时间和获取到锁继续执行的时间不一致,
    被唤醒的线程再次执行可能条件不满足,所有循环检验条件
    MESA的wait方法引入超时参数,皮面线程进入等待队列永久阻塞

notify 和notifyAll分别何时使用
notify:

  1. 所有等待线程拥有相同的等待条件
  2. 所有线程被唤醒后,执行相同的操作
  3. 只需要唤醒一个线程

不清楚的话就用notifyAll

Java与Monitor

对MESA模型精简了
Java中的管程只有一个条件变量
image.png
monitor在Java中的实现
Java中Object 定义了 wait, notify , notifyAll依赖ObjectMonitor 底层jvm基于C++实现之

  1. ObjectMonitor() {
  2. _header = NULL; //对象头 markOop
  3. _count = 0;
  4. _waiters = 0,
  5. _recursions = 0; // 锁的重入次数
  6. _object = NULL; //存储锁对象
  7. _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
  8. _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
  9. _WaitSetLock = 0 ;
  10. _Responsible = NULL ;
  11. _succ = NULL ;
  12. _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
  13. FreeNext = NULL ;
  14. _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
  15. _SpinFreq = 0 ;
  16. _SpinClock = 0 ;
  17. OwnerIsThread = 0 ;
  18. _previous_owner_tid = 0;

image.png
获取时:当前线程插入到cxq的头部,释放时,默认策略 QMode=0 :
如果entryList为空,从cxq中按原顺序插入到 entryList并唤醒第一个线程,后来先得锁
不为空是:从entrylist去,就是先来先得

对象的内存布局

对象可分为:三块:对象头,实例数据,对齐填充

  1. 对象头:hash码,对象所属年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数据长度等
  2. 实例数据:存放类的属性数据信息,包括父类的属信息
  3. 对齐填充:虚拟机要求对象起始地址必须是8字节的整数倍,不是必须存在的

image.png

对象头的详解

对象头包括

  1. Mark Word
    对象自身的运行时数据
    如:哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,区别32位操作系统还是64位操作系统
  2. Klass Pointer

指针,指向他的类元数据的指针,可以找到这个对象是那个类的实例
32位4字节,64位开启指针压缩是4字节,否则8字节,JDK1.8默认开启压缩,

  1. 数据长度
    如果对象是个数组会记录数组的长度,4字节

image.png

JOL工具

  1. //meaven 依赖
  2. <dependency>
  3. <groupId>org.openjdk.jol</groupId>
  4. <artifactId>jolcore</artifactId>
  5. <version>0.10</version>
  6. </dependency>

使用方法
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

  1. 查看打印的信息 默认开启指针压缩的场景下
    image.png
  2. 关闭指针压缩后,对象头为16字节:-XX:-UseCompressedOops 作为了解

image.png

MarkWork如何记录锁状态的

记录markoop实现Mark Word 具体实现是Markoop.hpp

  • hash: 保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。
  • age: 保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  • biased_lock: 偏向锁标识位。由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • lock: 锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • JavaThread*: 保存持有偏向锁的线程ID。偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
  • epoch: 保存偏向时间戳。偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

    32位JVM下对象结构描述

    image.png

    64位jvm下对象结构描述

    image.png

    Mark word的锁标记的枚举

    1. enum { locked_value = 0, //00 轻量级锁
    2. unlocked_value = 1, //001 无锁
    3. monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
    4. marked_value = 3, //11 GC标记
    5. biased_lock_pattern = 5 //101 偏向锁
    6. }
    image.png

    使用Jol工具查看锁状态

偏向锁

对加锁的优化,前提假设不存在竞争,总是由同一个线程获取,消除无竞争下的锁重入,通过引入CAS
jvm启用偏向锁模式,新创建对象的Mark Word ThreadId 为0:处于偏向但未偏向任何线程,也叫做匿
名偏向状态
偏向锁延迟偏向
启动4s后才会对新建的对象开启偏向锁模式
因为有一系列复杂的活动,装载,初始化等这些过程会用到Synchronize加锁,为了减少初始化时间
默认延时加载偏向锁

  1. //关闭延迟开启偏向锁
  2. XX:BiasedLockingStartupDelay=0
  3. //禁止偏向锁
  4. XX:‐UseBiasedLocking
  5. //启用偏向锁
  6. XX:+UseBiasedLocking
  1. public class LockEscalationDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
  4. //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
  5. Thread.sleep(4000);
  6. Object obj = new Object();
  7. new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
  11. +ClassLayout.parseInstance(obj).toPrintable());
  12. synchronized (obj){
  13. log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
  14. +ClassLayout.parseInstance(obj).toPrintable());
  15. }
  16. log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
  17. +ClassLayout.parseInstance(obj).toPrintable());
  18. }
  19. },"thread1").start();
  20. Thread.sleep(5000);
  21. log.debug(ClassLayout.parseInstance(obj).toPrintable());
  22. }

调用HashCode的影响:
obj.hashCode()或System.identityHashCode(obj) 会捯饬带对象的偏向锁被撤销
原因:一个对象hashcode只会成成一次,偏向锁没有地方保存hashcode

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

当对象处于可偏向和以偏向下,调用hashcode计算会使对象无法偏向

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

image.png

偏向锁撤销之调用wait/notify

偏向锁状态执行obj.notify() 会升级为轻量级锁,调用obj.wait(timeout) 会升级为重量级锁

  1. synchronized (obj) {
  2. // 思考:偏向锁执行过程中,调用hashcode会发生什么?
  3. //obj.hashCode();
  4. //obj.notify();
  5. try {
  6. obj.wait(100);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
  11. + ClassLayout.parseInstance(obj).toPrintable());

轻量级锁

偏向锁事变,会升级为轻量级锁
轻量级锁适用的场景是线程交替执行同步块的场合,
如果多个线程访问同一把锁,轻量级会升级到重量级

  1. public class LockEscalationDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
  4. //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
  5. Thread.sleep(4000);
  6. Object obj = new Object();
  7. // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
  8. obj.hashCode();
  9. //log.debug(ClassLayout.parseInstance(obj).toPrintable());
  10. new Thread(new Runnable() {
  11. @Override
  12. public void run() {
  13. log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
  14. +ClassLayout.parseInstance(obj).toPrintable());
  15. synchronized (obj){
  16. log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
  17. +ClassLayout.parseInstance(obj).toPrintable());
  18. }
  19. log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
  20. +ClassLayout.parseInstance(obj).toPrintable());
  21. }
  22. },"thread1").start();
  23. Thread.sleep(5000);
  24. log.debug(ClassLayout.parseInstance(obj).toPrintable());
  25. }

思考: 轻量级锁是否可以降级为偏向锁?
不会,会直接为无锁,再从无锁开始新一轮加锁过程
没有所谓的锁降级一说,直接就是无锁
https://www.jianshu.com/p/9932047a89be

测试:锁升级场景

  1. @Slf4j
  2. public class LockEscalationDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
  5. //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
  6. Thread.sleep(4000);
  7. Object obj = new Object();
  8. // 思考: 如果对象调用了hashCode,还会开启偏向锁模式吗
  9. //obj.hashCode();
  10. //log.debug(ClassLayout.parseInstance(obj).toPrintable());
  11. Thread thread1 = new Thread(new Runnable() {
  12. @Override
  13. public void run() {
  14. log.debug(Thread.currentThread().getName() + "开始执行。。。\n"
  15. + ClassLayout.parseInstance(obj).toPrintable());
  16. synchronized (obj) {
  17. // 思考:偏向锁执行过程中,调用hashcode会发生什么?
  18. //obj.hashCode();
  19. log.debug(Thread.currentThread().getName() + "获取锁执行中。。。\n"
  20. + ClassLayout.parseInstance(obj).toPrintable());
  21. }
  22. log.debug(Thread.currentThread().getName() + "释放锁。。。\n"
  23. + ClassLayout.parseInstance(obj).toPrintable());
  24. }
  25. }, "thread1");
  26. thread1.start();
  27. //控制线程竞争时机
  28. Thread.sleep(1);
  29. Thread thread2 = new Thread(new Runnable() {
  30. @Override
  31. public void run() {
  32. log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
  33. +ClassLayout.parseInstance(obj).toPrintable());
  34. synchronized (obj){
  35. log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
  36. +ClassLayout.parseInstance(obj).toPrintable());
  37. }
  38. log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
  39. +ClassLayout.parseInstance(obj).toPrintable());
  40. }
  41. },"thread2");
  42. thread2.start();
  43. Thread.sleep(5000);
  44. log.debug(ClassLayout.parseInstance(obj).toPrintable());
  45. }

偏向锁—-> 轻量级锁 ——> 无锁

  1. @Slf4j
  2. public class LockEscalationDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
  5. //HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
  6. Thread.sleep(4000);
  7. Object obj = new Object();
  8. new Thread(new Runnable() {
  9. @Override
  10. public void run() {
  11. log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
  12. +ClassLayout.parseInstance(obj).toPrintable());
  13. synchronized (obj){
  14. log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
  15. +ClassLayout.parseInstance(obj).toPrintable());
  16. }
  17. log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
  18. +ClassLayout.parseInstance(obj).toPrintable());
  19. }
  20. },"thread1").start();
  21. new Thread(new Runnable() {
  22. @Override
  23. public void run() {
  24. log.debug(Thread.currentThread().getName()+"开始执行。。。\n"
  25. +ClassLayout.parseInstance(obj).toPrintable());
  26. synchronized (obj){
  27. log.debug(Thread.currentThread().getName()+"获取锁执行中。。。\n"
  28. +ClassLayout.parseInstance(obj).toPrintable());
  29. }
  30. log.debug(Thread.currentThread().getName()+"释放锁。。。\n"
  31. +ClassLayout.parseInstance(obj).toPrintable());
  32. }
  33. },"thread2").start();
  34. Thread.sleep(5000);
  35. log.debug(ClassLayout.parseInstance(obj).toPrintable());
  36. }
  37. }

偏向锁—->轻量级锁——->重量级锁———>无锁
image.png

锁升级的原理分析