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

2020-09-28  本文已影响0人  lilykeke

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

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

synchronized 语法

synchronized(对象){
    临界区
}

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

2. Monitor 概念

2.1 对象头结构

32位虚拟机


64位虚拟机


2.2 Monitor (监视器)结构

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

Monitor.jpg

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){
    }
}
轻量级锁 (1).jpg

2.4.3 锁膨胀

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

锁膨胀 (1).jpg

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 重量级锁 重量锁指针 阻塞

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
偏向状态

首先回忆一下对象头格式

一个对象创建时:

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

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

撤销-调用对象hashCode

调用了对象的hashCode,但偏向锁的对象Mark Word 中存储的是线程id,如果调用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指令
上一篇 下一篇

猜你喜欢

热点阅读