synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除

系列文章目录

第一节 synchronized关键字详解


synchronized关键字详解

  • 系列文章目录
  • 前言
  • 一、synchronized的作用?
  • 二、如何使用synchronized关键字?
    • 加锁方式
      • 1、修饰普通方法/同步代码块传参this
        • 特点
        • 代码案例
      • 2、修饰静态方法/同步代码块传参Class对象
        • 特点
        • 代码案例
      • 3、同步代码块传参变量对象
        • 特点
        • 代码案例
  • 三、synchronized原理
    • 1、底层原理
      • synchronized在字节码中存在的形式
        • 1、同步代码块的字节码展现形式
        • 2、同步方法的字节码展现形式
    • 2、Monitor监视器锁
    • 3、Monitor对象
    • 4、对象的内存布局
      • 对象头
    • 5、锁的膨胀升级
      • 对象头分析工具
      • 偏向锁
        • 1、无锁代码案例
        • 2、可偏向状态代码案例
        • 3、偏向锁代码案例
      • 轻量级锁
        • 轻量级锁代码案例
        • HashCode导致锁的升级
          • 1、调用hashcode方法
          • 2、不调用hashcode
      • 适应性自旋
      • 重量级锁
      • 锁粗化
      • 锁消除
      • 小结
  • 四、总结


前言

本节介绍synchronized关键字相关内容,包含其的作用与使用及其底层原理等。


一、synchronized的作用?

在多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等

共享:资源可以由多个线程同时访问
可变:资源可以在其生命周期内被修改

导致的问题:
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问,synchronized则是其中一个同步器,本身也具备与Volatile相同的特性,即可见性,因此这种情况下就可省去volatile修饰变量

二、如何使用synchronized关键字?

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

加锁方式

1、修饰普通方法/同步代码块传参this

锁住的是当前实例对象

特点

  • 同一个实例调用会阻塞
  • 不同实例调用不会阻塞

代码案例

package com.xj;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class Synchronized_LockOnThisObject {
    static Logger log = LoggerFactory.getLogger(Synchronized_LockOnThisObject.class);

    public static CountDownLatch latch = new CountDownLatch(1);

    static class Product {
        //库存变量
        public int stock = 5;

        //减少库存:同步代码块传参this-锁住的是当前实例对象
        public void decrStock(){
            synchronized (this){
                if(stock > 0) {
                    --stock;
                    log.info("["+Thread.currentThread().getName()+"]剩余库存stock="+stock);
                }else{
                    log.error("["+Thread.currentThread().getName()+"]库存不足");
                }
            }
        }

        //减少库存:修饰普通方法-锁住的是当前实例对象
        public synchronized void decrStock2(){
            if(stock > 0) {
                --stock;
                log.info("["+Thread.currentThread().getName()+"]剩余库存stock="+stock);
            }else{
                log.error("["+Thread.currentThread().getName()+"]库存不足");
            }
        }
    }

    public static void main(String[] args) {
        Product product = new Product();

        //1、利用CountdownLatch等待10个线程全部创建完毕,一同执行商品减库存的操作,创造并发的条件
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //减库存
                    //product.decrStock();
					product.decrStock2();
                }
            },"Thread-"+i);
            t.start();
        }

        //休眠2s,主线程等待10个子线程准备完毕
        try {
            Thread.sleep(2000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //休眠2s结束,发送启动信号,10个子线程可开启执行减库存操作
        latch.countDown();
    }

}

结果打印:

15:45:22.848 [Thread-0] INFO com.xj.Synchronized_LockOnThisObject - [Thread-0]剩余库存stock=4
15:45:22.851 [Thread-9] INFO com.xj.Synchronized_LockOnThisObject - [Thread-9]剩余库存stock=3
15:45:22.851 [Thread-6] INFO com.xj.Synchronized_LockOnThisObject - [Thread-6]剩余库存stock=2
15:45:22.851 [Thread-7] INFO com.xj.Synchronized_LockOnThisObject - [Thread-7]剩余库存stock=1
15:45:22.851 [Thread-5] INFO com.xj.Synchronized_LockOnThisObject - [Thread-5]剩余库存stock=0
15:45:22.851 [Thread-8] ERROR com.xj.Synchronized_LockOnThisObject - [Thread-8]库存不足
15:45:22.851 [Thread-3] ERROR com.xj.Synchronized_LockOnThisObject - [Thread-3]库存不足
15:45:22.851 [Thread-4] ERROR com.xj.Synchronized_LockOnThisObject - [Thread-4]库存不足
15:45:22.851 [Thread-2] ERROR com.xj.Synchronized_LockOnThisObject - [Thread-2]库存不足
15:45:22.852 [Thread-1] ERROR com.xj.Synchronized_LockOnThisObject - [Thread-1]库存不足

Process finished with exit code 0

2、修饰静态方法/同步代码块传参Class对象

锁是当前类对象,全局锁

特点

所有调用该方法的线程都会实现同步

代码案例

package com.xj;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class Synchronized_LockOnStaticClass {
    static Logger log = LoggerFactory.getLogger(Synchronized_LockOnStaticClass.class);

    public static CountDownLatch latch = new CountDownLatch(1);

    static class Product {
        //库存变量
        public static int stock = 5;

        //减少库存:同步代码块传参Class对象-锁住的是当前类对象
        public void decrStock(){
            synchronized (Product.class){
                if(stock > 0) {
                    --stock;
                    log.info("["+Thread.currentThread().getName()+"]剩余库存stock="+stock);
                }else{
                    log.error("["+Thread.currentThread().getName()+"]库存不足");
                }
            }
        }

        //减少库存:修饰静态方法-锁住的是当前类对象
        public static synchronized void decrStock2(){
            if(stock > 0) {
                --stock;
                log.info("["+Thread.currentThread().getName()+"]剩余库存stock="+stock);
            }else{
                log.error("["+Thread.currentThread().getName()+"]库存不足");
            }
        }
    }

    public static void main(String[] args) {
        Product product = new Product();

        //1、利用CountdownLatch等待10个线程全部创建完毕,一同执行商品减库存的操作,创造并发的条件
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //减库存
                    product.decrStock();
                    //product.decrStock2();

                }
            },"Thread-"+i);
            t.start();
        }

        //休眠2s,主线程等待10个子线程准备完毕
        try {
            Thread.sleep(2000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //休眠2s结束,发送启动信号,10个子线程可开启执行减库存操作
        latch.countDown();
    }

}

结果打印

15:57:12.530 [Thread-0] INFO com.xj.Synchronized_LockOnStaticClass - [Thread-0]剩余库存stock=4
15:57:12.533 [Thread-9] INFO com.xj.Synchronized_LockOnStaticClass - [Thread-9]剩余库存stock=3
15:57:12.533 [Thread-8] INFO com.xj.Synchronized_LockOnStaticClass - [Thread-8]剩余库存stock=2
15:57:12.533 [Thread-7] INFO com.xj.Synchronized_LockOnStaticClass - [Thread-7]剩余库存stock=1
15:57:12.533 [Thread-6] INFO com.xj.Synchronized_LockOnStaticClass - [Thread-6]剩余库存stock=0
15:57:12.533 [Thread-2] ERROR com.xj.Synchronized_LockOnStaticClass - [Thread-2]库存不足
15:57:12.533 [Thread-4] ERROR com.xj.Synchronized_LockOnStaticClass - [Thread-4]库存不足
15:57:12.533 [Thread-3] ERROR com.xj.Synchronized_LockOnStaticClass - [Thread-3]库存不足
15:57:12.533 [Thread-5] ERROR com.xj.Synchronized_LockOnStaticClass - [Thread-5]库存不足
15:57:12.534 [Thread-1] ERROR com.xj.Synchronized_LockOnStaticClass - [Thread-1]库存不足

Process finished with exit code 0

3、同步代码块传参变量对象

锁住的是变量对象

特点

同一个属性对象才会实现同步

代码案例

package com.xj;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class Synchronized_LockOnObject {
    static Logger log = LoggerFactory.getLogger(Synchronized_LockOnObject.class);

    public static CountDownLatch latch = new CountDownLatch(1);

    public static Object obj = new Object();

    static class Product {
        //库存变量
        public int stock = 5;

        //减少库存:同步代码块传参Class对象-锁住的是当前类对象
        public void decrStock(){
            synchronized (obj){
                if(stock > 0) {
                    --stock;
                    log.info("["+Thread.currentThread().getName()+"]剩余库存stock="+stock);
                }else{
                    log.error("["+Thread.currentThread().getName()+"]库存不足");
                }
            }
        }
    }

    public static void main(String[] args) {
        Product product = new Product();

        //1、利用CountdownLatch等待10个线程全部创建完毕,一同执行商品减库存的操作,创造并发的条件
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //减库存
                    product.decrStock();

                }
            },"Thread-"+i);
            t.start();
        }

        //休眠2s,主线程等待10个子线程准备完毕
        try {
            Thread.sleep(2000l);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //休眠2s结束,发送启动信号,10个子线程可开启执行减库存操作
        latch.countDown();
    }

}

结果打印

16:08:36.573 [Thread-0] INFO com.xj.Synchronized_LockOnObject - [Thread-0]剩余库存stock=4
16:08:36.575 [Thread-9] INFO com.xj.Synchronized_LockOnObject - [Thread-9]剩余库存stock=3
16:08:36.575 [Thread-8] INFO com.xj.Synchronized_LockOnObject - [Thread-8]剩余库存stock=2
16:08:36.576 [Thread-7] INFO com.xj.Synchronized_LockOnObject - [Thread-7]剩余库存stock=1
16:08:36.576 [Thread-6] INFO com.xj.Synchronized_LockOnObject - [Thread-6]剩余库存stock=0
16:08:36.576 [Thread-5] ERROR com.xj.Synchronized_LockOnObject - [Thread-5]库存不足
16:08:36.576 [Thread-1] ERROR com.xj.Synchronized_LockOnObject - [Thread-1]库存不足
16:08:36.576 [Thread-3] ERROR com.xj.Synchronized_LockOnObject - [Thread-3]库存不足
16:08:36.576 [Thread-4] ERROR com.xj.Synchronized_LockOnObject - [Thread-4]库存不足
16:08:36.576 [Thread-2] ERROR com.xj.Synchronized_LockOnObject - [Thread-2]库存不足

Process finished with exit code 0

三、synchronized原理

1、底层原理

  1. synchronized是基于JVM内置锁实现,通过内部对象Monitor监视器锁实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的**Mutex lock(互斥锁)**实现,它是一个重量级锁性能较低。
  2. 但是,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与Lock持平。

synchronized在字节码中存在的形式

synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第1张图片

1、同步代码块的字节码展现形式

如下是通过javap命令查看字节码文件:

public class LockOnObject_javap {
    public static Object object = new Object();

    private Integer stock = 10;

    public void decrStock(){
        synchronized (object){
        }
    }
}
#查看字节码
javap  -v  LockOnObject_javap.class
public void decrStock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #4                  // Field object:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: aload_1
         7: monitorexit
         8: goto          16
        11: astore_2
        12: aload_1
        13: monitorexit
        14: aload_2
        15: athrow
        16: return

由上面可知monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常退出释放锁

2、同步方法的字节码展现形式

public synchronized void decrStock2(){

}
#查看字节码
javap  -v  LockOnObject_javap.class
public synchronized void decrStock2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 21: 0

  1. 由上可知,方法的同步并没有通过指令monitorenter和monitorexit来完成,不过其常量池中多了ACC_SYNCHRONIZED标示符,也就是JVM会根据该标示符来实现方法的同步的。
  2. 当方法调用时,调用指令将会检查方法的
    ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。
  3. 两种同步方式本质上没有区别,只是同步方法是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2、Monitor监视器锁

每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第2张图片

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

  1. monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常退出释放锁;

通过上面的描述,能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

以下展示IllegalMonitorStateException异常的案例:
synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第3张图片
synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第4张图片

3、Monitor对象

可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor结构中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒(这也就是说明了调用wait方法会释放对象锁);
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每个Java对象的对象头MarkWord中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问
那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局

4、对象的内存布局

从下图得知,在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

  1. 对象头:比如hash码,对象的分代年龄,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  2. 实例数据:存放类的属性数据信息,包括父类的属性信息;
  3. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第5张图片

对象头

  1. HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“MarkWord”。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,MarkWord被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
  2. 但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  3. 对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,MarkWord会随着程序的运行发生变化。

32位虚拟机变化状态如下:
在32位的HotSpot虚拟机中对象无锁的状态下,MarkWord的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0(是否偏向锁),在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。
synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第6张图片

5、锁的膨胀升级

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级为重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。
下面的流程图可供参考:
synchronized关键字详解-偏向锁、轻量级锁、偏向锁、重量级锁、自旋、锁粗化、锁消除_第7张图片

对象头分析工具

运行时对象头锁状态分析工具JOL,属于OpenJDK开源工具包


<dependency>
    <groupId>org.openjdk.jolgroupId>
    <artifactId>jol-coreartifactId>
    <version>0.10version>
dependency>

打印对象头信息:

#object为我们的锁对象
System.out.println(ClassLayout.parseInstance(object).toPrintable());

偏向锁

  1. 偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
  2. 偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。
  3. 但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。
默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

1、无锁代码案例

public class Biased_Light_Heavy_Lock {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

打印结果

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

MarkWord的信息在第一行

      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)

这里输出的是一个小端模式的二进制数据:
00000001得知,为无锁状态,需要留意的是,无锁状态下对象的hashcode为0,是因为hashcode为类似懒加载的方式,当调用hashcode后,重新打印即可看到,由下面两种打印结果对比可知。

public class Biased_Light_Heavy_Lock {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

打印结果

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)                           01 9b 43 3a (00000001 10011011 01000011 00111010) (977509121)
      4     4        (object header)                           12 00 00 00 (00010010 00000000 00000000 00000000) (18)
      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

2、可偏向状态代码案例

public class Biased_Light_Heavy_Lock {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);//
        Object o1 = new Object();
        System.out.println(ClassLayout.parseInstance(o1).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

从第一行"00000101"得知为偏向锁状态,但是没有线程ID的数据,所以是一种可偏向状态(匿名偏向)

3、偏向锁代码案例

public class Biased_Light_Heavy_Lock {
    public static void main(String[] args) throws InterruptedException {
    	// 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
        // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动偏向锁,会出现很多没有必要的锁撤销
        TimeUnit.SECONDS.sleep(5);
        Object o1 = new Object();
        System.out.println(ClassLayout.parseInstance(o1).toPrintable());
        synchronized (o1){
            System.out.println(ClassLayout.parseInstance(o1).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 50 fd 02 (00000101 01010000 11111101 00000010) (50155525)
      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

从第二段对象头信息"00000101 01010000 11111101 00000010"得知为偏向锁状态,并记录了线程ID(高23为非0)

轻量级锁

  1. 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word的结构也变为轻量级锁的结构。
  2. 轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

轻量级锁代码案例

package com.xj;

import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

public class Biased_Light_Heavy_Lock {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("-------------------------无锁状态,hashcode为0-------------------------");
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        System.out.println("-------------------------无锁状态,hashcode非0-------------------------");
        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        TimeUnit.SECONDS.sleep(5);
        Object o1 = new Object();

        System.out.println("-------------------------可偏向状态-------------------------");
        System.out.println(ClassLayout.parseInstance(o1).toPrintable());

        new Thread(()->{
            synchronized (o1){
                System.out.println("-------------------------偏向锁状态-------------------------");
                System.out.println(ClassLayout.parseInstance(o1).toPrintable());
            }
        }).start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("-------------------------保持偏向锁状态-------------------------");
        System.out.println(ClassLayout.parseInstance(o1).toPrintable());

        //两个线程交替执行,有机会可升级为轻量级锁
        new Thread(()->{
            synchronized (o1){
                System.out.println("-------------------------轻量级锁状态-------------------------");
                System.out.println(ClassLayout.parseInstance(o1).toPrintable());
            }
        }).start();
    }
}

打印结果

-------------------------无锁状态,hashcode为0-------------------------
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

-------------------------无锁状态,hashcode非0-------------------------
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 9b 43 3a (00000001 10011011 01000011 00111010) (977509121)
      4     4        (object header)                           12 00 00 00 (00010010 00000000 00000000 00000000) (18)
      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

-------------------------偏向锁状态-------------------------
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a0 0d 20 (00000101 10100000 00001101 00100000) (537763845)
      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 a0 0d 20 (00000101 10100000 00001101 00100000) (537763845)
      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)                           e0 f0 86 20 (11100000 11110000 10000110 00100000) (545714400)
      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

从最后一段"11100000"得知已升级为轻量级锁

HashCode导致锁的升级

1、调用hashcode方法
public class PrintMarkWord {

    public static void main(String[] args) throws InterruptedException {
        // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
        // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动偏向锁,会出现很多没有必要的锁撤销
        Thread.sleep(5000);
        Object t = new Object();
        //未出现任何获取锁的时候
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        synchronized (t){
            // 获取一次锁之后
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
        //打印hashcode
        System.out.println(t.hashCode());
        // 计算了hashcode之后,再次获取锁时将导致锁的升级
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        synchronized (t){
            // 再次获取锁,直接变成了轻量级锁
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(t).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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int T.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 50 62 03 (00000101 01010000 01100010 00000011) (56774661)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int T.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

1130478920
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 48 bd 61 (00000001 01001000 10111101 01100001) (1639794689)
      4     4        (object header)                           43 00 00 00 (01000011 00000000 00000000 00000000) (67)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int T.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 f7 43 03 (10010000 11110111 01000011 00000011) (54785936)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int T.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 48 bd 61 (00000001 01001000 10111101 01100001) (1639794689)
      4     4        (object header)                           43 00 00 00 (01000011 00000000 00000000 00000000) (67)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int T.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
2、不调用hashcode
public class PrintMarkWord {

    public static void main(String[] args) throws InterruptedException {
        // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。
        // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动偏向锁,会出现很多没有必要的锁撤销
        Thread.sleep(5000);
        Object t = new Object();
        //未出现任何获取锁的时候
        System.out.println(ClassLayout.parseInstance(t).toPrintable());
        synchronized (t){
            // 获取一次锁之后
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
        //打印hashcode
        //System.out.println(t.hashCode());
        // 计算了hashcode之后,再次获取锁时将导致锁的升级
        //System.out.println(ClassLayout.parseInstance(t).toPrintable());
        synchronized (t){
            //没打印hashcode,还是偏向锁
            System.out.println(ClassLayout.parseInstance(t).toPrintable());
        }
        System.out.println(ClassLayout.parseInstance(t).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 50 a9 02 (00000101 01010000 10101001 00000010) (44650501)
      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 50 a9 02 (00000101 01010000 10101001 00000010) (44650501)
      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 50 a9 02 (00000101 01010000 10101001 00000010) (44650501)
      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 50 a9 02 (00000101 01010000 10101001 00000010) (44650501)
      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

对比以上两种结果,可知当调用了hashcode,当再次获取锁会导致锁的升级,由偏向锁升级为轻量级锁,原因可能是因为偏向锁没地方记录hashcode

适应性自旋

  1. 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋的优化手段。
  2. 这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。

重量级锁

如果自旋之后还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的,最终没办法也就只能升级为重量级锁了。

public class heavyWeightMonitor {

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object a = new Object();

        Thread thread1 = new Thread(){
            @Override
            public void run() {
                synchronized (a){
                    System.out.println("thread1 locking");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    try {
                        //让线程睡眠,造成锁的竞争
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {
                synchronized (a){
                    System.out.println("thread2 locking");
                    System.out.println(ClassLayout.parseInstance(a).toPrintable());
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();
        thread2.start();
    }

}

打印结果:

thread1 locking
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           da f5 68 1c (11011010 11110101 01101000 00011100) (476640730)
      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

thread2 locking
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           da f5 68 1c (11011010 11110101 01101000 00011100) (476640730)
      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

由二进制数据可知"11011010",已升级为重量级锁

锁粗化

JIT编译时,发现一段代码中频繁的加锁释放锁,会将前后的锁合并为一个锁,避免频繁加锁释放锁。
如:

synchronized(this){
	method1();
}
synchronized(this){
	method2();
}
synchronized(this){
	method3();
}

优化后:

synchronized(this){
	method1();
	method2();
	method3();
}

锁消除

JIT编译器在编译的时候,进行逃逸分析。分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况。这时就可以消除该锁了,提升执行效率。
锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析

-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+EliminateLocks 表示开启锁消除。

如:

private void method1(){
        Object object = new Object();
        //object对象只能被一个线程访问到
        synchronized (object){
        }
}

小结

  • 偏向锁:无竞争的情况下,只有一个线程进入临界区,采用偏向锁
  • 轻量级锁:多个线程可以交替进入临界区,采用轻量级锁
  • 重量级锁:多线程同时进入临界区,交给操作系统互斥量来处理

四、总结

以上就是本次总结的内容,包含synchronized的作用、加锁方式、加锁原理、锁的膨胀升级等

你可能感兴趣的:(并发编程,java,开发语言)