并发编程笔记02:synchronize锁

395 阅读8分钟

一、锁机制

由JMM(java内存模型)规范的基础知识能够得知,在并发场景下(写)操作共享数据的情况下,会产生数据安全问题。常见的synchronizedLock便是JDK基于锁机制提供的解决方案。
锁机制有如下2个特性能够解决并发场景下的数据安全问题:

  • 互斥性:即在同一时间(片)只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问,从而实现了一系列复合操作的原子性
  • 可见性:能够确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

二、synchronized锁的概述

2.1 锁的分类

2.1.1 对象锁

在 Java 中,每个对象都会有一个monitor对象(属性),这个对象其实就是当前Java对象的锁,通常会被称为“内置锁”或“对象锁”。类(class)的对象(object)可以有多个,所以每个对象有其独立的对象锁,互不干扰。

2.1.2 类锁(字节码锁)

在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个Class对象,所以每个类只有一个类锁。(Class对象也是对象的一种)

2.2 作用范围

2.2.1 作用于代码块

  • 作用于对象方法中的代码块(持有对象锁,通常是this)
    public void methodName(){
      //……
      synchronized(this|object) {
        //……
      }
      //……
    }
    
  • 作用于静态方法中的代码块(持有类锁,通常是当前类)
    public static void methodName(){
      //……
      synchronized(Xxx.class) {
        //……
      }
      //……
    }
    

2.2.2 作用于方法

  • 作用于对象方法(持有对象锁)
    public synchronized void methodName(){
      //……
    }
    
  • 作用于静态方法(持有类锁)
    public synchronized static void methodName(){
      //……
    }
    

2.3 锁重入

在讲到锁的分类时说到,每个java对象都会有一个monitor对象(属性),某一线程占有这个对象的时候,先monitor的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权时monitor-1
同一线程可以对同一对象进行多次加锁,持续累加:+1、+1……,而不是要等前一次操作释放锁再继续,这就是锁的重入性

三、synchronized锁原理分析

3.1 线程堆栈分析

jdk自带了jconsole(可视化客户端)、jstack(命令行输出)等工具可供我们分析java线程的执行状况、是否死锁等。
示例代码(执行后,分别通过上述2种工具查看线程执行状态):

public class DemoThread {

    private Integer count = 0;

    public synchronized Integer getAndInc(){
        try {
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return count++;
    }

    public static void main(String[] args){
        DemoThread demoThread = new DemoThread();
        new Thread(demoThread::getAndInc).start();
        new Thread(demoThread::getAndInc).start();
        new Thread(demoThread::getAndInc).start();
    }
}

3.1.1 jconsole

3.1.2 jstack

zephyrlai@localhost ~ % jstack 51412
  ...
  "Thread-2" #12 prio=5 os_prio=31 tid=0x00007f8e6d129800 nid=0x4003 waiting for monitor entry [0x0000700007fb1000]
    java.lang.Thread.State: BLOCKED (on object monitor)
    at cn.zephyr.thread.sync.DemoThread.getAndInc(DemoThread.java:17)
    - waiting to lock <0x00000007956a8ae0> (a cn.zephyr.thread.sync.DemoThread)
    at cn.zephyr.thread.sync.DemoThread$$Lambda$3/381259350.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

  "Thread-1" #11 prio=5 os_prio=31 tid=0x00007f8e6d129000 nid=0x3f03 waiting for monitor entry [0x0000700007eae000]
    java.lang.Thread.State: BLOCKED (on object monitor)
    at cn.zephyr.thread.sync.DemoThread.getAndInc(DemoThread.java:17)
    - waiting to lock <0x00000007956a8ae0> (a cn.zephyr.thread.sync.DemoThread)
    at cn.zephyr.thread.sync.DemoThread$$Lambda$2/764977973.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

  "Thread-0" #10 prio=5 os_prio=31 tid=0x00007f8e6d09d800 nid=0x3e03 waiting on condition [0x0000700007dab000]
    java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at cn.zephyr.thread.sync.DemoThread.getAndInc(DemoThread.java:17)
    - locked <0x00000007956a8ae0> (a cn.zephyr.thread.sync.DemoThread)
    at cn.zephyr.thread.sync.DemoThread$$Lambda$1/931919113.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)
  ...

可以看到Thread-0处于休眠状态,Thread-1Thread-2处于阻塞状态

3.2 基于JVM指令分析

3.2.1 示例代码:

  public Integer getAndInc01(){
      synchronized (this){
          count++;
      }
      return count;
  }
  
  public synchronized Integer getAndInc02(){
      return count++;
  }

3.2.2 使用javap反编译示例代码

zephyrlai@localhost ~ % cd /Users/zephyrlai/zephyrWorkplace/ideaWorkplace/zephyr-arch-2020/target/classes/cn/zephyr/thread/sync/                
zephyrlai@localhost sync % javap -v DemoThread.class 
Classfile /Users/zephyrlai/zephyrWorkplace/ideaWorkplace/zephyr-arch-2020/target/classes/cn/zephyr/thread/sync/DemoThread.class
  Last modified 2020-1-16; size 2041 bytes
  MD5 checksum ceb834a30662cb5c466c7e87f3ba9c8c
  Compiled from "DemoThread.java"
public class cn.zephyr.thread.sync.DemoThread 
  ……
  public java.lang.Integer getAndInc01();
      descriptor: ()Ljava/lang/Integer;
      flags: ACC_PUBLIC
      Code:
        stack=3, locals=5, args_size=1
          0: aload_0
          1: dup
          2: astore_1
          3: monitorenter
          ……
          29: aload_1
          30: monitorexit
          31: goto          41
          34: astore        4
          36: aload_1
          37: monitorexit
          38: aload         4
          40: athrow
          41: aload_0
          42: getfield      #3                  // Field count:Ljava/lang/Integer;
          45: areturn
        ……

    public synchronized java.lang.Integer getAndInc02();
      descriptor: ()Ljava/lang/Integer;
      flags: ACC_PUBLIC, ACC_SYNCHRONIZED
      Code:
        stack=3, locals=3, args_size=1
          0: aload_0
          1: getfield      #3                  // Field count:Ljava/lang/Integer;
          4: astore_1
          5: aload_0
          6: aload_0
          7: getfield      #3                  // Field count:Ljava/lang/Integer;
          10: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
          13: iconst_1
          14: iadd
          15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          18: dup_x1
          19: putfield      #3                  // Field count:Ljava/lang/Integer;
          22: astore_2
          23: aload_1
          24: areturn
        LineNumberTable:
          line 32: 0
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0      25     0  this   Lcn/zephyr/thread/sync/DemoThread;
  ……

3.2.3 反编译分析

可以看到:

  • 对于代码块的加锁,是通过monitorentermonitorexit实现的,对应到对象的monitor对象(属性)上,则 monitorentermonitor+1,monitorexitmonitor-1,直到monitor=0;
  • 对应对象方法的加锁,是通过flags: ACC_SYNCHRONIZED标记实现的,对应到对象的monitor对象(属性)上,则 进入带有时ACC_SYNCHRONIZED标记的方法时monitor+1,退出方法时monitor-1,直到monitor=0;
  • 锁重入时,如果是同一线程时(根据线程唯一id判断),则累计+1或-1,不用再重复获取锁对象

四、JVM对synchronize锁的优化

4.1 锁的四种状态

之所以synchronized重,是因为它涉及到了操作系统用户态与核心态的转换。为了优化synchronize锁的性能,jdk1.6推出了相对于原先synchronize重量级锁的几种性能较高的锁状态,这几种锁状态消耗更少的资源,性能更好。这几种锁状态分别是: 无锁、偏向锁、轻量级锁、重量级锁。从左到右逐级变重。这4种锁状态由JVM自动升级且不可逆。

4.2 java对象头与锁状态升级

在分析这4中锁状态时,先补充一下对象头的相关知识

4.2.1 对象头

一个对象实例包含:对象头、实例变量、填充数据。前面说的的monitor对象就存储在对象头中,对象头里又分为:Mark Word、指向类的指针、数组长度(数组对象),我们这里关注Mark Word

4.2.2 锁状态升级

4.2.2.1 无锁状态:

锁标志位01,偏向锁标志位0

4.2.2.2 偏向锁:

当只有一个线程进入同步方法或同步代码块时,并不会直接获取Monitor锁,而是先判断对象头中Mark Word部分的锁标志位是否处于“01”,如果处于“01”,此时再判断线程ID是否是本线程ID,如果是则直接进入方法进行后续操作;如果不是,此时则通过CAS(无锁机制竞争)如果竞争成功,此时将线程ID设置为本线程ID,如果竞争失败,说明造成了有了较为强烈的锁竞争,偏向锁已不能满足,此时偏向锁晋级为轻量级锁。

4.2.2.3 轻量级锁:

当锁发生竞争时,持有偏向锁的线程会撤销偏向锁,转而晋级为轻量级锁(状态)。轻量级锁的核心是,不让未获取锁的线程进入阻塞状态,因为这会使得线程由用户态转为核心态,这会造成很大的性能损失,而是采用“死循环”的方式不断的获取锁,这种采用“死循环”获取的锁的方式称为——锁自旋。它不会让线程陷入阻塞,但同时仅适用于持有锁时间较短的场景。那么轻量级锁升级为重量级锁的条件就是,自旋等待的时间过长,并且又有了新的线程来竞争。

4.2.2.4 重量级锁:

这种锁,就是地地道道原原本本synchronized的本意了。线程会去抢夺对象上的一个互斥量(这个互斥量就是Monitor),每个对象都会有,就算是类也有一个Monitor互斥量(因为类在堆内存中有一个Class对象)。当一个线程获取到对象的Monitor锁时,其余线程会被阻塞挂起,并且 由用户态转为核心态

4.3 锁消除

顾名思义,即JIT在编译的时候吧不必要的锁去掉。

补充

基于Lambda表达式的线程快速创建

  • 表达式:new Thread([lambda exp])
  • 示例代码:
public class DemoThread {

    private Integer count = 0;

    public synchronized Integer getAndInc(){
        return count++;
    }

    public static void main(String[] args){
        DemoThread demoThread = new DemoThread();
        new Thread(demoThread::getAndInc).start();
    }
}

参考

synchronized凭什么锁得住?
美团技术团队-不可不说的Java“锁”事