并发编程-04 聊透synchronized锁

1、synchronized的意义

1.1 常见线程安全问题分析

前文已知,volatile关键字可以保证并发三大特性中的有序性、可见性。但更为致命的并发操作的原子性,volatile并不能完全覆盖。今天来看synchronized如何保证操作的原子性。

public class SyncDemo {

    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        //思考: counter=?
        System.out.println(counter);
    }

    public static void increment() {
        counter++;
    }

    public static void decrement() {
        counter--;
    }
}
//执行结果(每次各不同、因为是竞态条件,执行得到的结果也会不同)
-1204

Process finished with exit code 0

以上述代码中的++操作为例,看++操作的字节码如下。字节码中很明显的看到++操作指令分4步,JVM如何能让这4步操作是原子的?当单线程执行++操作尚可,当多线程同时执行呢?

0 getstatic #11 从类中获取静态字段
3 iconst_1      将int压到操作数栈
4 iadd          整数相加
5 putstatic #11 将结果设置回类中静态字段
8 return		返回

由下图可见,当多线程操作++方法,由于无法保证操作的原子性,必然导致数据的不安全。

在这里插入图片描述

1.2 临界区及竞态条件

多个线程同时对共享资源进行读写操作时,就会出现指令交错,造成线程安全问题。从而达到竞态条件。
在这里插入图片描述
为避免临界区的竞态条件发生,一般有多种手段保证:

  • 阻塞式方案:synchronized、Lock
  • 非阻塞式方案:互斥量、cas等

2、synchronized使用

前文书,synchronized是解决竞态条件的手段之一,我们的生产中也随处可见为了保证操作原子性而加上的锁。

//1、对静态方法加锁 -> 锁类对象(方法是类方法,故锁也是锁住类对象)
public synchronized static void increase1() {
    counter++;
}
//2、对实例方法加锁 -> 锁类实例对象(实例方法属于类实例 类实例#方法调用)
public synchronized void increase2() {
    counter++;
}
//3、代码块 -> 锁类实例对象
private Object lock = new Object();
public void increase3() {
    synchronized (lock){
        counter++;
    }
}
//4、代码块 -> 锁类对象
public void increase4() {
    synchronized (Object.class){
        counter++;
    }
}
//5、代码块 -> 锁任意实例对象
private String lockStr ="";
public void increase5(){
    synchronized (lockStr){
        counter++;
    }
}

对上述线程安全问题的加锁方法有很多种,但是加锁的方式不同,产生的影响自然也不同。当被锁对象在无意识的情况下,产生竞争,将会导致线程竞争的激烈程度无意识的增加。我们将在下文中展现,当多线程竞争趋于激烈时,开销增大的原因。再遇到非常激烈竞争时(始终cas失败的场景,甚至将导致线程park(),这会导致用户态到内核态的频繁切换,开销极大)
在这里插入图片描述

3、synchronized锁状态分析

3.1 对象内存布局

JDK1.5之前synchronized锁的操作很重,主要基于管程思想在JVM上的实现(重量级锁)。但随着Doug Lea在1.5针对synchronized的优化,synchronized存在多种锁状态:无锁、偏向锁、轻量级锁、重量级锁

JVM针对synchronized优化的意义何在?

  • 并发的场景很多,但大多场景中只是基于线程安全的需求加锁,实际上并发量不高。存在并发动辄就park()阻塞线程,让系统中的线程处于阻塞这样开销极大。
  • 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。
  • 多线程竞争越激烈,则JVM自适应的选择合适的锁。

既然存在多种锁状态,那么锁状态的信息又存放在哪里? 对象头的Mark word中。
在这里插入图片描述
简单分析对象内存布局中各组成部分及其含义:

  • 对象头

    • Mark word (不同锁状态的Mark word所存储的数据含义是不同的)
      32位计算机mark word占4字节32位,64位计算机markword占8字节64位。下图是64位计算机下不同锁状态markword的形态。
    锁状态56bit1bit4bit1bit2bit
    是否偏向锁锁标记位
    无锁态unused:25bit对象的hashCode:31bitunused分代年龄001
    偏向锁线程ID:54bitEpoch:2bitunused分代年龄101
    轻量级锁指向栈中锁记录的指针(ptr_to_lock_record)00
    重量级锁指向互斥锁(重量级锁)的指针(ptr_to_heavyweight_monitor)10
    GC标记11
    • Klass Pointer

      Klass Pointer指向c++源码中存放对象mate Data的指针地址。 Jdk1.8后默认开启了指针地址压缩,压缩后指针地址占用4字节。当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。

    • 数组长度

      如果对象是数组类型,则对象头中还有一块数据存放数组的长度。4字节32位,可以表达长度2^32-1长度

  • 实例数据

    存放类的属性信息,包括父类属性信息。

  • 对象填充

    前文提到过,计算机存储单元以8字节为存储行,偏于运算。所以对象头长度必须是8的倍数,不足8位时自动填充。

3.2 无锁、偏向锁、轻量级锁、重量级锁
<!-- 查看Java 对象布局、大小工具 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>    

从下面示例中演示4种锁状态,看对象头的分布。

public class SyncOopHeader {
    public static void main(String[] args) throws InterruptedException {
        //无锁
        System.out.println("无锁\n"+ClassLayout.parseInstance(new Object()).toPrintable());
        TimeUnit.SECONDS.sleep(5);

        Object obj = new Object();
        //匿名偏向
        System.out.println("匿名偏向\n"+ClassLayout.parseInstance(obj).toPrintable());

        new Thread(()->{
            synchronized (obj){
                //偏向锁
                System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
            }
            //偏向锁退出仍是偏向锁(偏向线程不变)
            System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());

            // jvm 优化
            // t1释放锁后,t1保活,则线程2变成轻量级锁(jvm认为此时是线程交替抢占锁)
            // t1释放锁后,t1不保活,则等待线程1结束后5s,线程2抢占锁,则线程2中锁仍是偏向锁(jvm认为此时仍是单线程加解锁)
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"Thread1").start();

        //等待5秒脱离线程1的影响
        TimeUnit.SECONDS.sleep(5);

        Thread t2 = new Thread(()->{
            synchronized (obj){
                //开始争抢 t1保活时,轻量级锁(线程1解锁5s后,线程2开始争抢锁)
                //开始争抢 t1不保活,偏向锁(线程1解锁5s后,线程2开始争抢锁)
                System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
            }
            System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());

        },"Thread2");
        t2.start();
        //等待5s,保持线程3和线程4单独竞争锁
        TimeUnit.SECONDS.sleep(5);

        //当t3、t4同时开抢 有1个线程持有锁,另一个线程始终无法获取到锁,则从轻量级锁膨胀至重量级锁
        new Thread(()->{
            synchronized (obj){
                //1个线程获取 另一个长时间cas失败 则从轻量级膨胀至重量级锁
                System.out.println(Thread.currentThread().getName()+"000"+"\n"+ClassLayout.parseInstance(obj).toPrintable());
            }
            System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
        },"Thread3").start();

        new Thread(()->{
            synchronized (obj){
               //1个线程获取 另一个长时间cas失败 则从轻量级膨胀至重量级锁
                System.out.println(Thread.currentThread().getName()+"000"+"\n"+ClassLayout.parseInstance(obj).toPrintable());
            }
            System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
        },"Thread4").start();
    }
}
无锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

匿名偏向
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread1
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 28 ab 22 (00000101 00101000 10101011 00100010) (581642245)
      4     4        (object header)                           ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread1释放锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 28 ab 22 (00000101 00101000 10101011 00100010) (581642245)
      4     4        (object header)                           ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread2
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           80 f0 1f 6e (10000000 11110000 00011111 01101110) (1847586944)
      4     4        (object header)                           f0 00 00 00 (11110000 00000000 00000000 00000000) (240)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread2释放锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread3000
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
      4     4        (object header)                           ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread4000
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
      4     4        (object header)                           ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread3释放锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
      4     4        (object header)                           ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread4释放锁
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  • 无锁
    JVM在开始启动时设置了默认4s长的无锁状态,减少启动时的synchronized竞争。

    轻量级锁和重量级锁在退出时,也会首先回到无锁状态。(TODO未演示出效果)

    电脑是使用小端模式,高8位地址显示在高位。

    无锁
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
  • 偏向锁(匿名偏向)

    匿名偏向
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
  • 偏向锁(偏向线程1)

    Thread1
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 d8 ca 3d (00000101 11011000 11001010 00111101) (1036703749)
          4     4        (object header)                           18 02 00 00 (00011000 00000010 00000000 00000000) (536)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
  • 轻量级锁

    Thread2
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           80 f0 1f 6e (10000000 11110000 00011111 01101110) (1847586944)
          4     4        (object header)                           f0 00 00 00 (11110000 00000000 00000000 00000000) (240)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    Thread2释放锁
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
  • 重量级锁

    Thread4000
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
          4     4        (object header)                           ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    Thread4释放锁
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

4、synchronized锁的升级与撤销详解

在这里插入图片描述

4.1 无锁状态
  • 偏向锁状态下调用#hashCode(),偏向锁无法存储hashCode值,转为无锁。
  • jvm启动默认4s延迟开启偏向锁,4s前生成的对象,默认为无锁。
  • 轻量级锁退出锁时,状态为无锁。
4.2 偏向锁状态
  • 初始化的偏向锁状态为匿名偏向
  • 当线程cas 锁对象的线程ID值时,该线程持有锁对象
  • 多次重入会记入Epoch值,用于批量重偏向和批量重偏向撤销的统计(25s内)
4.3 轻量级锁
  • 轻量级锁适用于线程交替执行synchronized代码块
  • 当前线程需要进入锁块,在持有偏向锁对象的线程未消亡时,当前线程copy markword,并将mark word CAS修改为线程栈的lock_record地址,cas成功则轻量级锁持有成功。
  • 当前线程重入轻量级锁时,线程栈帧会发生多次锁对象的压栈,线程栈的lock record中存放锁对象、mark word信息。

在这里插入图片描述

  • 轻量级锁退出锁状态后,markword信息从线程栈的lock_record中拷贝出来,再修改回markword。

  • 特别注明:在轻量级线程中,并没有很多cas,更没有cas自旋。

4.4 重量级锁
  • 管程思想

  • java中对管程思想的实现有2种方式

    ​ 管程思想中的关键因素(程序内多线程互斥访问共享资源。只有一个线程允许进入程序,其余线程排队等待。)

    • 入口等待队列
    • 条件等待队列

在这里插入图片描述

  • synchronized的管程由JVM实现

    • 入口等待队列(首次触发竞争的线程,先进入cxq排队等待)[cxq是栈帧排列 -> 先入后出,导致不公平]
    • waitSet(当持有线程触发等待条件,wait等)[需要等待满足启动的条件(notify等),才能继续争抢线程]
      在这里插入图片描述
  • AbstractQueueSynchonizer(AQS的实现)

  • 重量级锁重入

  • 膨胀过程与撤销

5、synchronized锁优化

5.1批量重偏向

Jvm针对偏向锁中Epoch的值进行统计,当25s内有20个锁对象完成线程重偏向,则后续线程将直接升级为轻量级锁。

5.2 批量撤销

当Epoch值在25s内完成40个锁对象的重偏向,则关闭偏向锁,后续新生成的锁对象直接为无锁状态。

5.3 自旋优化
5.4 锁的粗化

出现一个方法内,多次没有必要的加锁时,JVM将自动将多余的锁操作粗化,保留一个即可。

5.5 逃逸分析

当加锁代码块执行的区间无法逃逸出线程栈,此时的加锁是没有意义的。JVM将对该部分锁进行优化。

6、synchronized锁源码解析

6.1 轻量级锁源码

在这里插入图片描述

6.2 重量级锁源码(轻量级锁到重量级锁的膨胀)

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦昂志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值