由浅入深了解synchronized关键字

简介

本文主要介绍synchronized的用法,功能,原理,以及最后通过实验数据验证。

用法

使用synchronized关键字对方法或者代码块进行修饰,来实现多线程下加锁的目的,具体用法如下:

介绍代码功能
修饰静态方法public synchronized static void add() {}对类对象进行加锁
修饰实例方法public synchronized void add() {}对类实例进行加锁
修饰代码块-类对象synchronized (Obj.class){}对类对象进行加锁
修饰代码块-类实例Obj obj = new Obj(); synchronized (obj){}对类实例进行加锁

原理

基础介绍

上章节提到synchronized可以修饰实例对象或者类对象,不管类对象还是实例对象,都是继承自java.lang.Object,每个Object对象都有一个Monitor属性,Monitor属性是用于控制对象的访问权限(也就是加锁)。synchronized关键字会操作Monitor,通过汇编指令可以看出。如果有synchronized关键字都会有monitorenter和monitorexit指令。

 monitorenter
 ......
 monitorexit

Monitor是通过C++实现,信息保存在对象头部中的MarkWord,后面会详细介绍。如果对这部分源码感兴趣的朋友可以下载OpenJDK的源码进行阅读。

锁膨胀流程介绍

synchronized锁的升级分为四个过程:无锁->偏向锁->轻量级锁->重量级锁,具体如下图所示(该图来自https://wiki.openjdk.java.net/display/HotSpot/Synchronization)
在这里插入图片描述
按上图的描述,可以看出有区分是否支持偏向锁。下面只说明支持偏向锁的情况:

  1. 分配对象时,默认为匿名向锁;
  2. 当对象第一次被线程获取时,获取线程信息记录到对象的MarkWord中;
  3. 当另一个线程要获取锁时,偏向模式就会结束,会根据对象状态进行重偏向(rebias),未锁定,轻量锁;
  4. 多线程同时尝试获取锁,各个线程通过自旋(也就是递归加锁recursive lock)方式都可以获取锁,膨胀为轻量级锁;
  5. 当存在线程通过自旋锁获取失败时,膨胀为重量级锁。

Mark Word内容介绍

java中对象头由Mard Word、Klass Pointer(类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例)。接下来重点介绍Mark Word的内容:

Mark Word (64bit,其中的数字指长度)锁状态
unused:25 + hashcode:31 + unused:1 + age:4 + biased_lock:1 + lock:2正常无锁(01)
thread:54 + epoch:2 + unused:1 + age:4 + biased_lock:1 + lock:2偏向锁(01)
ptr_to_lock_record:62 + lock:2轻量级锁(00)
ptr_to_heavyweight_monitor:62 + lock:2重量级锁(10)

验证

需要使用jol-core.jar包,添加依赖:

 <dependency>
   <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

加锁对象如下:

public class Obj {

    public int sum = 0;

    public synchronized void add(final long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sum++;
    }

}

偏向锁

synchronizedi修饰类对象时,不支持偏向锁。

偏向锁VM参数查看命令

  • Window:java -XX:+PrintFlagsFinal -version | findstr BiasedLocking
  • Linux:java -XX:+PrintFlagsFinal -version | grep BiasedLocking
C:\Users\guoyu.huang>java -XX:+PrintFlagsFinal -version | findstr BiasedLocking
     intx BiasedLockingBulkRebiasThreshold          = 20                                  {product}
     intx BiasedLockingBulkRevokeThreshold          = 40                                  {product}
     intx BiasedLockingDecayTime                    = 25000                               {product}
     intx BiasedLockingStartupDelay                 = 4000                                {product}
     bool TraceBiasedLocking                        = false                               {product}
     bool UseBiasedLocking                          = true                                {product}
  • BiasedLockingBulkRebiasThreshold:批量重新偏向临界值,默认值20
  • BiasedLockingBulkRevokeThreshold:批量撤销偏向临界值,默认值40
  • BiasedLockingDecayTime:重置计数的时间,默认值为25000毫秒
  • BiasedLockingStartupDelay:偏向锁延迟时间,默认值4000毫秒
  • TraceBiasedLocking:是否追踪偏向锁,默认值false
  • UseBiasedLocking:是否使用偏向锁,默认值true

BiasedLockingStartupDelay单位是毫秒

偏向锁实现方式

  1. 根据对象的Mark Word确定为偏向模式
  2. 通过CAS操作,将获取对象锁的线程信息记录在对象的Mark Word中
  3. 偏向锁没有释放锁的操作,一直记录着持有偏向锁的线程信息,这样可以减少同步操作(例如:Locking、UnLocking以及修改Mark Word操作)。

偏向锁终止条件

当存在另一个线程去尝试获取这个对象锁,偏向模式才结束。

测试代码

/**
 * 验证偏向锁
 * <pre>
 *      默认偏向锁有延迟,为了测试方便,直接设置0延迟
 *      VM参数:-XX:BiasedLockingStartupDelay=0
 * </pre>
 *
 * @throws InterruptedException
 */
public static void testBiasedLocking() throws InterruptedException {
	// 该类无任何属性和函数
    Obj obj = new Obj();

    System.out.println("初始内容----------------------");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    synchronized (obj) {
        System.out.println("偏向内容----------------------");
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

日志说明

初始内容----------------------
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

偏向内容----------------------
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 ca 02 (00000101 00111000 11001010 00000010) (46807045)
      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

根据以上日志可以看出,刚开始时为101,偏向线程为空,表示匿名偏向锁,当第一个线程获取到锁之后,将锁的偏向线程改成当前线程。

重偏向

A similar mechanism, called bulk rebiasing, optimizes situations in which objects of a class are locked and unlocked by different threads but never concurrently. It invalidates the bias of all instances of a class without disabling biased locking. An epoch value in the class acts as a timestamp that indicates the validity of the bias. This value is copied into the header word upon object allocation. Bulk rebiasing can then efficiently be implemented as an increment of the epoch in the appropriate class. The next time an instance of this class is going to be locked, the code detects a different value in the header word and rebiases the object towards the current thread.
一个类似的机制称为批量重偏,它优化了由不同线程锁定和解锁类对象但从不并发的情况。它使类的所有实例的偏向失效,而不禁用偏向锁。类中的epoch值充当时间戳,指示偏差的有效性。此值在对象分配时复制到头字中。批量再偏可以有效地实现为适当类中的历元的增量。下一次锁定这个类的实例时,代码在头字中检测到一个不同的值,并将对象重新定位到当前线程。来自:OpenJDK

触发条件

在没有并发情况下,一个类对象下类实例获取锁的线程发生变化,当达到BiasedLockingBulkRebiasThreshold值时,会发生重偏向。

测试代码

/**
 * 测试重偏向
 * <pre>
 *      默认偏向锁有延迟,为了测试方便,直接设置0延迟: -XX:BiasedLockingStartupDelay=0
 *      跟踪偏向锁:-XX:+TraceBiasedLocking
 * </pre>
 *
 * @throws InterruptedException
 */
public static void testRebias() throws InterruptedException {

    List<Obj> objList = new ArrayList<>();

    new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            Obj obj = new Obj();
            synchronized (obj) {
                objList.add(obj);
            }
        }
        try {
            //为了防止JVM线程复用,在创建完对象后,保持线程存活状态
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    Thread.sleep(3000);
    System.out.println("objList中第20个对象的偏向内容----------------------");
    System.out.println((ClassLayout.parseInstance(objList.get(19)).toPrintable()));

    //创建另一个线程去竞争之前线程中已经退出同步块的锁
    new Thread(() -> {
        for (int i = 0; i < 20; i++) {
            Obj obj = objList.get(i);
            synchronized (obj) {
                if (i == 18 || i == 19) {
                    System.out.println("第" + (i + 1) + "次偏向结果");
                    System.out.println((ClassLayout.parseInstance(obj).toPrintable()));
                }
            }
        }
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

日志分析

objList中第20个对象的偏向内容----------------------
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 88 e8 1f (00000101 10001000 11101000 00011111) (535332869)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第19次偏向结果
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           30 f3 7c 20 (00110000 11110011 01111100 00100000) (545059632)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第20次偏向结果
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 69 dc 1e (00000101 01101001 11011100 00011110) (517761285)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

根据日志可以看出同一个实例,在第一次打印时持有偏向锁的线程和第二次发生偏向的线程不是同一个线程。

撤销偏向

当偏向发生了撤销偏向之后,就会禁用偏向。

// TODO 未研究透彻

static HeuristicsResult update_heuristics(oop o, bool allow_rebias) {
  markOop mark = o->mark();
  if (!mark->has_bias_pattern()) {
    return HR_NOT_BIASED;
  }

  Klass* k = o->klass();
  jlong cur_time = os::javaTimeMillis();
  jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time();
  int revocation_count = k->biased_lock_revocation_count();
  if ((revocation_count >= BiasedLockingBulkRebiasThreshold) &&
      (revocation_count <  BiasedLockingBulkRevokeThreshold) &&
      (last_bulk_revocation_time != 0) &&
      (cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) {
    k->set_biased_lock_revocation_count(0);
    revocation_count = 0;
  }

  // Make revocation count saturate just beyond BiasedLockingBulkRevokeThreshold
  if (revocation_count <= BiasedLockingBulkRevokeThreshold) {
    revocation_count = k->atomic_incr_biased_lock_revocation_count();
  }

  if (revocation_count == BiasedLockingBulkRevokeThreshold) {
    return HR_BULK_REVOKE;
  }

  if (revocation_count == BiasedLockingBulkRebiasThreshold) {
    return HR_BULK_REBIAS;
  }

  return HR_SINGLE_REVOKE;
}

轻量级锁

触发条件

当存在多个线程竞争同一把锁时,每个线程等待的时间都不长(通过自旋都可以获得锁)。

测试代码

/**
 * 轻量锁
 * <pre>
 *      默认偏向锁有延迟,为了测试方便,直接设置0延迟
 *      VM参数:-XX:BiasedLockingStartupDelay=0
 * </pre>
 *
 * @throws InterruptedException
 */
public static void testLightweight() throws InterruptedException {
    
    Obj obj = new Obj();

    new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    Thread.sleep(200);

    System.out.println("----------------------");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    
    new Thread(() -> {
        synchronized (obj) {
            System.out.println("T2----------------------");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }

    }).start();

    Thread.sleep(200);

    System.out.println("----------------------");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

}

日志分析

----------------------
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 30 a0 1e (00000101 00110000 10100000 00011110) (513814533)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

T2----------------------
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           40 f3 0a 20 (01000000 11110011 00001010 00100000) (537588544)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

----------------------
cn.hgy.jol.Obj 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)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第一个线程通过CAS成功修改对象的MarkWord,获取偏向锁,持有锁的时间为400毫秒;第二个线程在200毫秒后启动,通过CAS修改对象的MarkWord失败,JVM将该对象的锁升级为轻量级锁;此时第二个线程进行自旋,也就不断的执行CAS操作,最终在允许自旋的最大时间内获取锁。

重量级锁

触发条件

当存在多个线程竞争同一把锁时,存在线程通过自旋无法获得锁。

测试代码

同轻量锁的测试代码相似,修改第一个线程的持锁时间。

/**
 * 重量锁
 * <pre>
 *      默认偏向锁有延迟,为了测试方便,直接设置0延迟
 *      VM参数:-XX:BiasedLockingStartupDelay=0
 * </pre>
 *
 * @throws InterruptedException
 */
public static void testHeavyweight() throws InterruptedException {
    Obj obj = new Obj();

    new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();

    Thread.sleep(200);

    System.out.println("----------------------");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());

    new Thread(() -> {
        synchronized (obj) {
            System.out.println("T2----------------------");
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }

    }).start();

    Thread.sleep(5000);

    System.out.println("----------------------");
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}

日志分析

----------------------
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 38 b1 1f (00000101 00111000 10110001 00011111) (531707909)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

T2----------------------
cn.hgy.jol.Obj object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           3a 0a b3 1c (00111010 00001010 10110011 00011100) (481495610)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

----------------------
cn.hgy.jol.Obj 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)                           92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

第一个线程通过CAS成功修改对象的MarkWord,获取偏向锁,持有锁的时间为5000毫秒;第二个线程在200毫秒后启动,通过CAS修改对象的MarkWord失败,JVM将该对象的锁升级为轻量锁;此时第二个线程进行自旋,在允许自旋的最大时间内无法获取锁,则将该对象的锁膨胀为重量级锁。

参考资料

  1. OpenJDK-Synchronization
  2. 周志明-深入理解Java虚拟机 第2版
  3. 简书-白龙6-JOL工具分析java对象大小
  4. 简书-biudefu-并发编程-(5)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

瑾析编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值