多线程和并发(二):锁的四种状态和锁升级

1. 一个例子

1.1 多个线程访问共享资源的问题

两个线程对初始值为0 的静态变量一个做自增,一个做自减,各做5000次,结果是0 吗?

public class Test {
    static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }

        }, "t2");
        t1.start();
        t2.start();
        try{
            t1.join();
            t2.join();
        }catch (InterruptedException e){

        }
        System.out.println("count is " + count);
    }
}

执行结果:
count is 1198

问题:为什么count 的结果不是0?每次执行的结果都是不一样的?

1.2 问题分析

前一节的结果可能是正数、负数、0 ,为什么呢?

因为Java中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码分析

例如,对于i++而言,(i为静态变量),实际会产生如下JVM字节码指令:

 0: getstatic     #2                      // Field i:I   获取静态变量i的值
 3: iconst_1                              //准备常量1              
 4: iadd                                  // 自增
 5: putstatic     #2                      // Field i:I  将修改后的值存入静态变量i

对于i--也是类似

8: getstatic     #2                      // Field i:I   获取静态变量i的值
11: iconst_1                             //准备常量1     
12: isub                                 // 自减
13: putstatic     #2                     // Field i:I   将修改后的值存入静态变量i

而Java的内存模型中,要完成静态变量的自增,自减需要在主存和工作内存中进行数据交换

1.2.1 临界区

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读取共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时经常发生指令交错,就会出现问题

一段代码块如果存在对共享资源的多线程读写操作,称这段代码块为临界区

1.2.2 竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之发生了竞态条件

1.5 synchronized 解决方案

* 应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案: synchronized Lock
  • 非阻塞式的解决方案: 原子变量

synchronized ,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区代码,不用担心线程上下文切换

注意
虽然java中互斥和同步都可以采用synchronized 关键字来完成,但他们还是有区别的

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点

synchronized 语法

synchronized(对象){
    临界区
}

synchronized 实际上用对象锁保证了临界区代码的原子性,临界区的代码对外是不可分割的,不会被线程切换所打断

2. Monitor 概念

2.1 对象头结构

32位虚拟机


  • 普通对象


    对象头 .jpg
  • 数组对象


    对象头1.jpg
  • 其中Mark Word 结构为


    32位-markword.jpg

64位虚拟机


  • 普通对象
    Class Pointer 未开启指针压缩的情况下:


    64-object_header.jpg
  • 数组对象


    64-object_header (1).jpg
  • Mark Word 结构


    64-MarkWord.jpg

2.2 Monitor (监视器)结构

每个Java对象都可以关联一个Monitor对象,如果使用synchronized 给对象上锁(重量级)之后,该对象头的Mark Word中的指针就被设置指向Monitor 对象。

Monitor.jpg
  • 刚开始Monitor中Owner为null
  • 当 Thread-2 执行 synchronized(obj) 就会将Monitor 的所有者Owner 置为Thread-2,Monitor 只能有一个Owner
  • 当Thread-2上锁的过程中,如果Thread-3, Thread-4, Thread-5 也来执行synchronized(obj), 就会进入EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒EntryList 中等待的线程来竞争锁,竞争时是非公平的。
  • 图中WaitSet 中的Thread-0,Thread-1是之前获取过锁,但条件不满足而进入WAITING状态的线程,后面讲wait-notify 时会分析。

2.3 原理之 synchronized

static final Object lock = new Object();
static int counter = 0;

public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

将上面代码反编译为字节码指令

  Last modified 2020-9-26; size 703 bytes
  MD5 checksum 2317ab368b56c4a43172b2f0915c3c11
  Compiled from "SynchronizedTest.java"
public class com.lily.threadpool.SynchronizedTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#28         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#29         // com/lily/threadpool/SynchronizedTest.lock:Ljava/lang/Object;
   #3 = Fieldref           #5.#30         // com/lily/threadpool/SynchronizedTest.counter:I
   #4 = Class              #31            // java/lang/Object
   #5 = Class              #32            // com/lily/threadpool/SynchronizedTest
   #6 = Utf8               lock
   #7 = Utf8               Ljava/lang/Object;
   #8 = Utf8               counter
   #9 = Utf8               I
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/lily/threadpool/SynchronizedTest;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               StackMapTable
  #22 = Class              #20            // "[Ljava/lang/String;"
  #23 = Class              #31            // java/lang/Object
  #24 = Class              #33            // java/lang/Throwable
  #25 = Utf8               <clinit>
  #26 = Utf8               SourceFile
  #27 = Utf8               SynchronizedTest.java
  #28 = NameAndType        #10:#11        // "<init>":()V
  #29 = NameAndType        #6:#7          // lock:Ljava/lang/Object;
  #30 = NameAndType        #8:#9          // counter:I
  #31 = Utf8               java/lang/Object
  #32 = Utf8               com/lily/threadpool/SynchronizedTest
  #33 = Utf8               java/lang/Throwable
{
  static final java.lang.Object lock;
    descriptor: Ljava/lang/Object;
    flags: ACC_STATIC, ACC_FINAL

  static int counter;
    descriptor: I
    flags: ACC_STATIC

  public com.lily.threadpool.SynchronizedTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lily/threadpool/SynchronizedTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field lock:Ljava/lang/Object;  <-lock引用(synchronized开始)
         3: dup
         4: astore_1                         //lock引用 -> slot 1 (存储到临时变量slot1)
         5: monitorenter                   //将lock对象Mark Word置为Monitor指针
         6: getstatic     #3                  // Field counter:I    <-i
         9: iconst_1                           //准备常数1
        10: iadd                                //+1
        11: putstatic     #3                  // Field counter:I  ->i
        14: aload_1                          //<-lock引用  (拿到slot1 中存储的临时变量)
        15: monitorexit                      //将lock对象Mark Word 重置,唤醒EntryList
        16: goto          24
        19: astore_2                         //e -> slot2   (发生异常的时候)
        20: aload_1                          // <- lock 引用
        21: monitorexit                     //将lock对象Mark Word 重置,唤醒EntryList
        22: aload_2                          // <- slot2 (e)
        23: athrow                            //throw e
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 9: 0
        line 10: 6
        line 11: 14
        line 12: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #4                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: putstatic     #2                  // Field lock:Ljava/lang/Object;
        10: iconst_0
        11: putstatic     #3                  // Field counter:I
        14: return
      LineNumberTable:
        line 5: 0
        line 6: 10
}
SourceFile: "SynchronizedTest.java"

2.4 synchronized 原理进阶

2.4.1 偏向锁

(虚拟机必须确保打开偏向锁,如果该对象写入了hashcode 则不能使用偏向锁)

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word 里是否存储着指向当前线程的偏向锁

2.4.2 轻量级锁

使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,语法仍然是synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object lock = new Object();

public void method1(){
    synchronized (lock){
        method2();
    }
}

private void method2() {
    synchronized (lock){
    }
}
  • 创建锁记录(Lock-Record)对象,每个线程的栈帧中都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word


    轻量级锁.jpg
  • 让锁记录表中的object Reference 指向锁的对象,并尝试用cas 替换Object 中的Mark Word ,将Mark Word的值存入锁记录。

轻量级锁 (1).jpg
  • 如果CAS替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁。


    轻量级锁 (2).jpg
  • 如果CAS失败,有两种情况:
    1.如果是其他线程已经持有了该object的轻量级锁,这时表明有竞争,进入锁膨胀的过程。
    2.如果是自己执行了synchronized的锁重入,那么再添加一条Lock Record 作为重入的计数


    轻量级锁 (3).jpg
  • 当退出synchronized 代码块(解锁)时,如果有取值为null的锁记录,表示由重入,这时重置锁记录,表示重入计数减一。

  • 当退出synchronized 代码块(解锁)时,若锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象。

  • 解锁情况:
    1.成功,则解锁成功
    2.失败,说明轻量级锁进行了锁膨胀,或已经升级为重量级锁,进入重量级锁解锁流程

2.4.3 锁膨胀

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

  • 当Thread-1进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。


    轻量级锁 (4).jpg
  • 这时Thread-1加轻量级锁失败,进入锁膨胀过程。(轻量级锁升级为重量级锁)
    1.即为Object对象申请Monitor锁,让Object 指向重量级锁地址
    2.然后Thead-1进入Monitor的EntryList BLOCKED

锁膨胀 (1).jpg
  • 轻量级锁没有阻塞的说法,重量级锁才有阻塞
  • 当Thread-0 退出同步代码块解锁的时候,使用CAS将Mark Word 的值恢复给Object对象头,失败,会进入重量级解锁过程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒 EntryList 的BLOCKED 线程。

2.4.4 自旋优化

自旋优化适合多核cpu

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

注意:使用自旋避免线程发生阻塞,发生阻塞的线程会有线程上下文切换,消耗资源。

自旋重试成功的情况:

线程1(cpu1上) 对象Mark 线程2(cpu2上)
10 重量级锁
访问同步块,获取Moniotr 10 重量级锁 重量锁指针
成功(加锁) 10 重量级锁 重量锁指针
执行同步块 10 重量级锁 重量锁指针
执行同步块 10 重量级锁 重量锁指针 访问同步块,获取Monitor
执行同步块 10 重量级锁 重量锁指针 自旋重试
执行完毕 10 重量级锁 重量锁指针 自旋重试
成功(解锁) 01 无锁 自旋重试
10 重量级锁 重量锁指针 成功(加锁)
01 无锁 执行同步块

自旋重试失败的情况:

线程1(cpu1上) 对象Mark 线程2(cpu2上)
10 重量级锁
访问同步块,获取Moniotr 10 重量级锁 重量锁指针
成功(加锁) 10 重量级锁 重量锁指针
执行同步块 10 重量级锁 重量锁指针
执行同步块 10 重量级锁 重量锁指针 访问同步块,获取Monitor
执行同步块 10 重量级锁 重量锁指针 自旋重试
执行同步块 10 重量级锁 重量锁指针 自旋重试
执行同步块 10 重量级锁 重量锁指针 自旋重试
执行同步块 10 重量级锁 重量锁指针 阻塞
  • 在java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次的自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
  • 自旋会占用cpu时间,单核cpu自旋就是浪费,多核cpu自旋才能发挥优势。
  • java 7之后不能控制是否开启自旋功能

2.4.5 偏向锁

轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作。

java 6中引入了偏向锁来做进一步优化,只有第一次使用CAS将线程ID设置到对象的Mark Word中,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
例如:

static final Object obj = new Object();
public void m1(){
    synchronized (obj){
        m2();
    }
 }

private void m2() {
    synchronized (obj){
        m3();
    }
}
private void m3() {
    synchronized (obj){
    }
}

轻量级锁重入


轻量级锁重入.jpg

偏向锁重入


偏向锁重入.jpg
偏向状态

首先回忆一下对象头格式

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为0x05 ,即最后3位为101,这时它的thread 、epoch 、age 都为0
  • 偏向锁是默认延时的,不会在程序启动的时候立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0 来禁止延时。
  • 如果没有开启偏向锁,那么对象创建后,markword 值为0x01 ,即最后三位为001,这时它的hashcode,age 都为0,第一次用到hashcode 时才会赋值。

注意:处于偏向锁的对象解锁后,线程id扔存储在对象头中。

调了对象的hashCode()方法后,禁用偏向锁了。为什么?因为Mark Word 中存不下了
轻量级锁的hashcode存在锁记录表中。 重量级的hashcode存在Monitor对象中

撤销-调用对象hashCode

调用了对象的hashCode,但偏向锁的对象Mark Word 中存储的是线程id,如果调用hashCode 会导致偏向锁被撤销。

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

在调用hashCode 之后使用偏向锁,记得去掉 -XX:-UseBiasedLocking

撤销-其他线程使用对象

当有其他线程使用偏向锁时,会将偏向锁升级为轻量级锁

撤销- 调用wait / notify

因为wait /notify 会将锁升级为重量级锁

批量重偏向

如果对象虽然被多个线程访问,但是没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。
当撤销偏向锁阈值超多20次后,jvm会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程。

批量撤销 (重新看一遍)

当撤销偏向锁阈值超过40次后,jvm会这样觉得,自己确实偏向错了,根本就不应该偏向。于是整个类的所有对象都会变为不可偏向的。新建的对象也是不可偏向的

锁消除

JIT (Just_In_Time Compiler):即时编译器 - 将热点代码直接编译成机器语言
只有单一线程访问加锁代码时,会优化成不加锁。提高执行效率

总结

synchronized 底层实现概括:

  1. java 代码:synchronized
  2. 字节码层面:monitorenter monitorexit
  3. 执行过程中自动升级
  4. 更底层 lock comxchg指令
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342