一篇长文---深入理解synchronized

提问

在开始之前,先声明,本文内容会很多,请耐心观看,看到目录应该就知道我没骗你

大概提一下会说什么,本次会整理synchronized的使用,对应的底层原理,monitor机制,对象内存布局,偏向锁,轻量级锁,重量级锁,以及这些锁之间的转换和关系,还有synchronized的优化

相信我,你不会吃亏的,这是一篇长达两万八千字的整理,我写了很久

在这里插入图片描述

我们思考一个问题

声明一个变量 int count = 0; 我们用两个线程,一个++,一个–,各自执行1000次,最后的结果一定会是0吗?

先来说答案,不是,结果可能是正数、零、负数,三种结果任一种

原因很简单,Java中的 ++ 和 – 操作,都不是原子操作,不信的可以多试几次,1000次不行就5000次,一定会出现不一样的结果的,不是预期的结果

临界区和竞态条件

我们来了解两个概念,临界区和竞态条件

临界区:一个程序本身的运行是没有问题的,问题在多个线程对资源的访问,多个线程对共享资源进行读共享,其实也没有问题,问题出在多个线程对资源的读写操作发生指令交错,一段代码内如果出现对共享资源的多线程读写操作,那么,就称这段代码为临界区,资源称为临界资源

竞态条件:多个线程在临界区内执行,由于代码执行顺序的不同,而导致结果无法预测,就称发生了竞态条件

为了避免临界区的竞态条件的发生,有不同的方法可以达到目的

阻塞式方案:synchronized、lock

非阻塞式方案:原子变量

在java中,无论同步还是互斥,我们都可以用synchronized来实现,但是它们也有一点区别,互斥是保证临界区竞态条件的发生,同一时刻只能有一个线程执行临界区代码;而同步,是因为线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点

synchronized的使用

synchronized是Java内置的一种原子锁,Java中每一个对象都可以把它当做一个同步锁来使用,这些Java内置的使用者看不到的锁称为内置锁,也会被称为监视器锁

synchronized可以用在方法上, 也可以用在代码块上,不同的位置有不同的区分及不同的锁对象

方法上使用synchronized

在方法上使用synchronized,不同的方法有不同的区别,方法分为普通的实例方法和静态方法
对于普通的实例方法来说,被锁的对象是类的实例对象

public synchronized void countMethod() {
	// todo
}

而对于静态方法来说,锁的是类对象

public static synchronized void countMethod() {
	// todo
}

代码块使用synchronized

在代码块使用synchronized,就像在方法上使用一样, 也有不同的区别
对实例对象来说,锁的就是类的实例对象

synchronized (this) {
	// todo
}

对class对象来说,锁的是类对象

synchronized (MyService.class) {
	// todo
}

对任意实例的Object来说,锁的就是实例的Object

String lock = "";

synchronized (lock) {
	// todo 
}

synchronized底层原理

synchronized是JVM内置锁,基于 Monitor 机制实现,依赖操作系统的互斥原语 Mutex,是一个重量级锁,性能比较低,不过,在JDK1.5之后,就进行了重大优化,比如锁粗化、锁消除、轻量级锁、偏向锁、自适应自选等技术,以此来减少锁的开销,现在内置锁的性能基本上能与Lock持平

​Java虚拟机通过一个同步结构来支持方法和方法中的指令序列的同步:Monitor

同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现的
同步代码块是通过 monitorenter 和 monitorexit 来实现的

两个指令的执行是JVM通过调用操作系统的互斥原语 Mutex 实现的,被阻塞的线程会挂起,等待重新被调度,会导致用户态和内核态的来回切换,对性能有比较大的影响

我们可以找个代码试试

在这里插入图片描述
当然,直接看这个可能看不明白,我们找一个方法访问标志对比解读 0x0021
在这里插入图片描述
看这两个,我们就可以知道是这样的没错

然后,我们来看同步代码块的字节码
在这里插入图片描述可以看到,这儿其实有两个退出,仔细看,第一个退出,会跳转到 14 行,正常退出,而第二个退出,其实是在抛异常的时候,为了解锁

也就是说,用 synchronized 的时候,不用担心解锁的问题,当然,相应的,我们也不能像lock那样,自己控制解锁条件

严格来说,其实也可以,因为 synchronized 是基于 Monitor 的,我们只要用它,也可以实现加锁解锁,但是,不推荐这么玩
在这里插入图片描述

Monitor

上面我们提到了 Monitor ,这是个很重要的东西,翻译为监视器,在操作系统,一般称为 管程。
管程指的是管理共享变量以及对共享变量操作的过程,让他们支持并发
synchronized关键字和 wait() notify() notifyAll() 这三个方法 是Java中实现管程技术的组成部分

在管程的发展史上,出现过三种管程模型,分别是 Hasen模型、Hoare模型、MESA模型,目前使用广泛的,是MESA模型
在这里插入图片描述
我们提到了 wait() notify() notifyAll() ,那我们就需要看看他们的使用条件

wait()

while (条件不满足时) {
	wait()
}

因为唤醒的时间和获取到锁继续执行的时间是不一样的,被唤醒的线程再次执行时,可能已经不满足条件了,所以要采用循环检验,并且,这也可以防止出现 notifyAll() 产生虚假唤醒,唤醒了不该唤醒的线程

为了防止进入等待队列永久阻塞,MESA模型的wait()方法还有一个超时参数设置

notify()和notifyAll()

这两个方法,从名字也可以看出来,唤醒一个和唤醒全部

既然是唤醒一个,那就不知道唤醒谁,也就是说,看运气,所以说,除了以下几种情况,尽量都用notifyAll(),天知道唤醒的是不是你想唤醒的

  1. 所有等待线程拥有相同的等待条件
  2. 所有阻塞的线程被唤醒后,执行相同的操作
  3. 只需要唤醒一个线程来执行操作就可以

Java内置的管程synchronized

我们大概提完了管程,让我们回到Java上,Java中,内置的管程就是synchronized,是基于JVM层面对管程的实现
它参考了MESA模型,对MESA模型进行了精简,在MESA模型中,条件变量有多个,而Java中,只有一个
在这里插入图片描述

Monitor机制在Java的实现

我们已经知道,Object提供了 wait() notify() notifyAll() ,所以,这些方法也有他们实现的方式

这些方法的具体实现,依赖于 ObjectMonitor,这是JVM内部基于C++实现的一套机制

我们先来了解它的数据结构,它在 hotspot源码ObjectMonitor.hpp

ObjectMonitor() {
    _header       = NULL; //对象头  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // 锁的重入次数 
    _object       = NULL;  //存储锁对象
    _owner        = NULL;  // 标识拥有该monitor的线程(当前获取锁的线程) 
    _WaitSet      = NULL;  // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;

看完数据结构,我们再来看看在Java中的执行流程
在这里插入图片描述在获取锁的时候,是将当前线程插入到_cxq的头部,而释放锁的时候,默认策略是:如果是EntrtyList为空,则将_cxq的元素按原有顺序插入到_EntrtyList,并唤醒第一个线程,也就是说,当_EntrtyList为空的时候,是后来的线程先获取锁,_EntrtyList不为空的时候,直接从_EntrtyList中唤醒线程

从这个默认策略这儿也能看出是不是一个公平锁,还是非公平锁,所谓公平锁,就是你等待的越久,你就越优先获取锁,非公平锁,那就是谁拿到就是谁,你等的再久我也不管你

到这儿,思考一个问题,synchronized 是加锁在对象上的,那,锁对象是怎么被记录锁状态的?
要解决这个问题,我们就得知道对象的内存布局

对象的内存布局

在虚拟机中,对象在内存中的存储布局大概可以分为三个部分:对象头-header、实例数据instance data、对齐填充padding

  • 对象头:比如hash码、对象所属的年代、对象锁、锁状态标志、偏向锁线程ID、偏向时间、数组长度(当然,这个数组对象才有)
  • 实例数据:存放类的属性数据信息,包括父类的属性信息
  • 对齐填充:这是因为虚拟机要求对象的起始地址必须是8字节的整数倍,填充数据为非必须存在,仅仅是为了字节上对齐而已

在这里插入图片描述

对象头详解

虚拟机的对象头包括 Mark Word、Klass Pointer、数组长度

  • Mark Word:用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,GC分代年龄占4bit
  • Klass Pointer:对象头的另一部分是klass类型指针,也就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,32位字节,64位开启指针压缩或最大堆内存<32G时4字节,否则就是8字节,JDK1.8默认开启指针压缩,压缩后为4字节,当在JVM关闭指针压缩后,长度为8字节
  • 数组长度:这一部分只有数组才有,如果对象是一个数据,那么在对象头中还有一块数据用于记录数组长度,占用4字节
    在这里插入图片描述所以,new Object() 占用的字节数为16,Mark Word占8个字节,压缩指针占4个字节,非数组,没有数组长度字节,无内部属性, 8+4=12,但是因为要是8的倍数,所以补充到16字节
    假设Object内部有一个属性是long,那就会占用24个字节,因为12+8=20,补充到8的倍数,24个字节

我们可以试一下查看对象的内存占用,有一个工具 JOL(JAVA OBJECT LAYOUT)

<!-- 查看Java 对象布局、大小工具 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

在这里插入图片描述
在这里插入图片描述可以看到,header占用了12个字节,对齐填充了4个字节,一共占用16

ok,拿我们现在来验证一下,如果有别的属性是什么样的

public class TestF {

    public static void main(String[] args) {
        SpaceTest spaceTest = new SpaceTest();
        System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
    }

    static class SpaceTest {
        private long time;
    }

}

在这里插入图片描述哦吼,看,是不是和我们上面说的一样

现在,让我们关闭了指针压缩试试 -XX:-UseCompressedOops

在这里插入图片描述
看,现在依旧是16字节,但是,没有填充,都是header

Mark Word

我们来回顾上面的问题,synchronized 是加锁在对象上的,那,锁对象是怎么被记录锁状态的?
我们说,要解决这个问题,得知道对象头的构造,我们已经知道了对象头的构造,那这个问题答案呢?

我们继续探究,既然对象头三个部分,最后的数组长度可能没有,那就只可能是Mark Word和Klass point,而Klass point是指针,那就只会是在Mark Word了

Hotspot通过markOop类型实现Mark Word,具体的实现位于markOop.hpp文件,由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,所以markOop被设计成了一个非固定的数据结构,以便在极小的空间存储更多的数据,根据对象的状态复用自己的存储空间,简单点来说就是,MarkWord结构搞成这么复杂,就是因为需要节省内存,让同一个内存区域在不同阶段有不同的用处

我们抓一些很重要的东西

  • hash:保存对象的哈希码,运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里
  • age:保存对象的分代年龄,表示对象被GC的次数,当该次数达到阈值的时候,对象就会被转移到老年代
  • biased_lock:偏向锁标识位,由于无锁和偏向锁的锁标识都是01,没办法区分,所以只能再引入一位偏向锁标识
  • lock:锁状态标识位,用于区分锁状态,比如11代表对象待回收的GC状态,只有最后两位锁标识位有效
  • JavaThread*:保存持有偏向锁的线程ID,偏向模式的时候,当某个线程持有对象的时候,对象这里就会被设置为该线程的ID,在后面的操作中,就无须再尝试进行获取锁的动作,这个线程ID并不是JVM分配的线程ID号,指的是操作系统的线程ID,和Java Thead中的线程ID是两个概念
  • epoch:保存偏向时间戳,偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向于哪个锁

ok,我们来看一下32位和64位的JVM对象结构
32位

这是32位的,我们再来看64位的
64位
注意,unused不是什么特殊值,是未使用的意思
线程ID指的是操作系统的线程ID

64位结构中ptr_to_lock_record和ptr_to_heavyweight_monitor

ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,当锁获取是无竞争时,JVM使用原子操作,而不是使用OS互斥,这种称为轻量级锁定,在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,如果两个不同多线程同时在一个对象上竞争,那么必须将轻量级锁升级为Monitor管理等待的线程,在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

从上面两幅图我们也可以看到,32位和64位,锁标志位在无锁状态和偏向锁状态都是01,所以也印证了我们上面说的,引入了一位是否偏向锁

锁状态其实就是我们上面看到的那几种,对应的枚举就是

enum { locked_value             = 0,    //00 轻量级锁 
         unlocked_value           = 1,   //001 无锁
         monitor_value            = 2,   //10 监视器锁,也叫膨胀锁,也叫重量级锁
         marked_value             = 3,   //11 GC标记
         biased_lock_pattern      = 5    //101 偏向锁
}         

如果说,要再明显直观一点看,也行
在这里插入图片描述

上面我们已经知道,synchronized依赖于Monitor机制,Monitor是管程,Java参考了管程模型-MESA,每个Object都可以使用synchronized,依赖于ObjectMonitor的实现,而ObjectMonitor的使用,依赖于对象头信息,对象头包含了不同锁的情况

so,我们现在来看看不同的锁以及他们之间是否有某种联系

偏向锁

首先,我们要知道偏向锁是什么

偏向锁是一种针对加锁操作的优化手段,在大多数情况下,锁对象不但不存在多线程竞争,而且总是由同一个线程持有,因此,为了消除数据在无竞争的情况下锁重入(CAS)的开销,从而引入偏向锁,对于没有竞争的场合,偏向锁有很好的优化效果

我们回过头来看一下上面查看过的对象占用
在这里插入图片描述可以看到,是001,无锁状态

注意,这儿的显示和处理器架构有关,X86架构一般是小端存储

所谓小端存储和大端存储做一个解释说明,小端存储,就是说低位字节存储在低地址端,高位字节存储在高地址端,而大端存储,就是说低位字节存储在高地址端,高位字节存储在低地址端(我试了一下,我的mac也是小端存储,我以为Mac的arm架构CPU会是大端存储)

我们现在尝试对这个对象加锁

    public static void main(String[] args) throws Exception {
        SpaceTest spaceTest = new SpaceTest();
        System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
        new Thread(()-> {
            synchronized (spaceTest) {
                System.out.println("--------------------------------------------------------------------------------");
                System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            }
        }).start();
    }

    static class SpaceTest {
        private long time;
    }

在这里插入图片描述可以看到,变成了轻量级锁,后面也有了锁记录指针指向

说到这里,有一个新的问题,为什么加锁之后不是偏向锁,而是轻量级锁?

这个是有原因的,JVM有一个偏向锁延迟机制,HotSpot虚拟机在启动之后有4S的延迟才会对每个新建的对象开启偏向锁模式,JVM启动时会进行一些列复杂的活动,比如装载配置,类的初始化等,这个过程会使用大量的synchronized对对象加锁,并且这些锁大部分都不是偏向锁,如果上来就偏向锁,再撤销,升级变成轻量级锁,这个过程也是会耗费时间的,所以为了减少初始化的时间,JVM默认延迟加载偏向锁

当然,默认开启偏向锁,就可以关闭,有延迟,我们也可以关闭

//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking 
//启用偏向锁

我们关闭延迟开启偏向锁来看看
在这里插入图片描述看,因为关闭了延迟偏向,所以上来之后也偏向锁了,加锁也偏向锁了

仔细看,上面创建完对象之后,和下面加锁,两个不大一样,上面的没有绑定线程id,这是因为JVM从JDK1.6开始默认开启了偏向锁模式,新建对象的Mark Word的Thread Id为0,说明可以偏向,但是未偏向任何线程,也叫作匿名偏向状态,真正有线程竞争的时候,才赋予了线程id

我们来印证另一个问题,延迟4S,那我们就休眠5S

    public static void main(String[] args) throws Exception {
        Thread.sleep(5000);
        SpaceTest spaceTest = new SpaceTest();
        System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
        new Thread(()-> {
            synchronized (spaceTest) {
                System.out.println("--------------------------------------------------------------------------------");
                System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            }
        }).start();
    }

    static class SpaceTest {
        private long time;
    }

在这里插入图片描述可以看到,都是偏向锁

那,休眠5S,如果我们对类对象加锁呢?不加锁创建的对象

        new Thread(()-> {
            synchronized (SpaceTest.class) {
                System.out.println("--------------------------------------------------------------------------------");
                System.out.println(ClassLayout.parseInstance(SpaceTest.class).toPrintable());
            }
        }).start();

在这里插入图片描述可以看到,直接是个轻量级锁

偏向锁撤销

我们提到了偏向锁撤销,那我们来看看哪些操作会让偏向锁撤销
先说结果:hashCode()、wait()、notify()

先回顾一下,休眠5S之后,竞争前和竞争后,都是偏向锁,这个时候我们调用一下hashCode()
在这里插入图片描述
在这里插入图片描述从偏向锁到重量级锁了…

我们来尝试wait()

        new Thread(()-> {
            synchronized (spaceTest) {
                System.out.println("--------------------------------------------------------------------------------");
                try {
                    spaceTest.wait(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            }
        }).start();

在这里插入图片描述重量级锁…

再来试试notify()

        new Thread(()-> {
            synchronized (spaceTest) {
                System.out.println("--------------------------------------------------------------------------------");
                try {
                    spaceTest.notify();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            }
        }).start();

在这里插入图片描述嗯哼,轻量级锁

先不说为什么是轻量级锁和重量级锁,总之,都不是偏向锁对不对?

现在来整理一下

  1. hashCode:回顾对象头结构,偏向锁没有保存hashCode的地方,只有无锁、轻量级锁、重量级锁,这三个会保存hashCode,轻量级锁保存在锁记录中,重量级锁会在Monitor中,所以,调用了hashCode之后,一定不会是偏向锁状态
  2. wait:我们上面提到过,wait是基于Monitor实现的,要wait,必须有Monitor对象,所以,一定是重量级锁
  3. notify:鄙人暂时也没想明白,为什么它持有Monitor,却是轻量级锁

总之有一个很重要,那就是偏向锁撤销之后,不一定升级为轻量级锁,还可能直接是重量级锁

当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量级锁,而当对象正处于偏向锁时,调用HashCode会使得偏向锁强制升级为重量级锁,再也无法偏向

轻量级锁

上面提到了轻量级锁,如果偏向锁失败,虚拟机不会立刻升级为重量级锁,而是尝试使用轻量级锁的优化手段,此时MarkWord的结构也变为轻量级锁的结构,轻量级锁适应的场景是线程交替执行同步块的场景,如果存在同一时间多个线程访问同一把锁的场合,那就会升级为重量级锁

轻量级锁就是为了优化性能,重量级锁需要使用到操作系统的park,直接到了内核态,而轻量级锁可以用CAS来实现,在用户态

在上面我们已经试过了,偏向锁升级为轻量级锁,那,轻量级锁撤销之后呢,会是偏向锁吗?我们改造一下代码,上面我们已经写过,调用notify()的时候会是轻量级锁

        public static void main(String[] args) throws Exception {
            Thread.sleep(5000);
            SpaceTest spaceTest = new SpaceTest();
            System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    spaceTest.notify();
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread1").start();

        }

    static class SpaceTest {
        private long time;
    }

在这里插入图片描述唔,轻量级锁撤销之后是无锁状态

轻量级锁的竞争过程:线程栈内可以记录锁状态,将锁对象MarkWord的内容复制到自己的栈内,而锁对象的MarkWord的ThreadId,也会指向对应的线程,当其他线程尝试获取锁,就需要改变锁对象的MarkWord的指向,但是ThreadId,已经有了指向,除非释放,回到了无锁状态,其他线程才可以通过CAS尝试修改指向,改成功了,那就获取到锁了

轻量级锁的重入:锁对象MarkWord指向了线程栈中的锁记录,线程栈也将MarkWord存了下来(这是因为之后要恢复到无锁状态,将MarkWord拷贝回去,所以要复制下来保存),而重入的时候,避免覆盖,所以不能操作栈中的锁记录,会生成一个新的锁记录,将新的锁记录MarkWord设置为null就行,释放锁的时候,栈结构的特性是后进先出,所以最后出去的,是最先复制的带有MarkWord的锁记录

画一个轻量级锁重入后的示意图
在这里插入图片描述
其中,栈最下面,displaced word就是拷贝的markword,markword与栈帧锁记录互相指向,00就是记录的锁标记

锁升级-轻量级锁

写一个,偏向锁升级轻量级锁

               public static void main(String[] args) throws Exception {
            Thread.sleep(5000);
            SpaceTest spaceTest = new SpaceTest();
            System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread1").start();

            Thread.sleep(1);

            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread2").start();

        }

    static class SpaceTest {
        private long time;
    }

在这里插入图片描述

锁升级-重量级锁

我们来模拟一下偏向锁升级为重量级锁的过程

    public static void main(String[] args) throws Exception {
        Thread.sleep(5000);
        SpaceTest spaceTest = new SpaceTest();
        System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
        new Thread(()-> {
            synchronized (spaceTest) {
                System.out.println(Thread.currentThread().getName() + " " + ClassLayout.parseInstance(spaceTest).toPrintable());
            }
        }, "thread1").start();
        new Thread(()-> {
            synchronized (spaceTest) {
                System.out.println(Thread.currentThread().getName() + " " + ClassLayout.parseInstance(spaceTest).toPrintable());
            }
        }, "thread2").start();
    }

    static class SpaceTest {
        private long time;
    }

在这里插入图片描述偏向锁在激烈竞争的情况下,变成了重量级锁

重量级锁

已知,重量级锁这种的会调用操作系统操作,从用户态切换到内核态,所以开销很大,轻量级锁撤销之后是无锁,那,重量级锁撤销之后呢?

为了显眼一点,我们在thread2释放锁之后,我们在等一等,看会不会有变化

        public static void main(String[] args) throws Exception {
            Thread.sleep(5000);
            SpaceTest spaceTest = new SpaceTest();
//            System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            new Thread(()-> {
//                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
//                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread1").start();

            new Thread(()-> {
//                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread2").start();

            Thread.sleep(5000);
            System.out.println("thread 2 释放锁 5s 后" + ClassLayout.parseInstance(spaceTest).toPrintable());

        }

执行结果如下

pri.study.auto.TestF$SpaceTest 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        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1加锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a e1 05 6e (01001010 11100001 00000101 01101110) (1845879114)
      4     4        (object header)                           e3 01 00 00 (11100011 00000001 00000000 00000000) (483)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2加锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a e1 05 6e (01001010 11100001 00000101 01101110) (1845879114)
      4     4        (object header)                           e3 01 00 00 (11100011 00000001 00000000 00000000) (483)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2释放锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           4a e1 05 6e (01001010 11100001 00000101 01101110) (1845879114)
      4     4        (object header)                           e3 01 00 00 (11100011 00000001 00000000 00000000) (483)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread 2 释放锁 5s 后pri.study.auto.TestF$SpaceTest 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total


Process finished with exit code 0

唔,看来重量级锁释放之后,是无锁状态,但是因为涉及到了操作系统,这种monitor操作比较耗时,其实清楚monitor是靠的GC

如果重量级锁释放后,再竞争锁,会是什么样呢?

我们在线程2释放了锁编程无锁之后,再开一个线程3
在这里插入图片描述也在我们意料之中,毕竟,无锁升级只能是轻量级锁,不可能是偏向锁,具体原因我们上面也说过了,偏向锁它没有地方存hashCode

线程复用

看一个神奇的现象,先上示例代码

        public static void main(String[] args) throws Exception {
            Thread.sleep(5000);
            SpaceTest spaceTest = new SpaceTest();
            System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread1").start();

            Thread.sleep(100);
            
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "线程开始" + ClassLayout.parseInstance(spaceTest).toPrintable());
                synchronized (spaceTest) {
                    System.out.println(Thread.currentThread().getName() + "加锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
                }
                System.out.println(Thread.currentThread().getName() + "释放锁" + ClassLayout.parseInstance(spaceTest).toPrintable());
            }, "thread2").start();

        }

看这个代码,猜测结果是什么?我们执行一下

pri.study.auto.TestF$SpaceTest 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        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1线程开始pri.study.auto.TestF$SpaceTest 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        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1加锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 a3 1e (00000101 00010000 10100011 00011110) (514002949)
      4     4        (object header)                           56 02 00 00 (01010110 00000010 00000000 00000000) (598)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1释放锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 a3 1e (00000101 00010000 10100011 00011110) (514002949)
      4     4        (object header)                           56 02 00 00 (01010110 00000010 00000000 00000000) (598)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2线程开始pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 a3 1e (00000101 00010000 10100011 00011110) (514002949)
      4     4        (object header)                           56 02 00 00 (01010110 00000010 00000000 00000000) (598)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2加锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 a3 1e (00000101 00010000 10100011 00011110) (514002949)
      4     4        (object header)                           56 02 00 00 (01010110 00000010 00000000 00000000) (598)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2释放锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 10 a3 1e (00000101 00010000 10100011 00011110) (514002949)
      4     4        (object header)                           56 02 00 00 (01010110 00000010 00000000 00000000) (598)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total


Process finished with exit code 0

仔细观察一下,线程偏向的一直是一个,数值一直是 514002949,从理论上说,thread1执行的时候,偏向thread1,而thread2执行的时候,应该偏向thread2,但是居然是同一个,这个就很神奇了,猜测可能是JVM做的优化,Thread1完事之后,底层的线程直接分配绑定给了thread2去用,并没有去创建新线程

我们再试一个也比较奇怪的地方,在 thead1里,释放锁之后,我们让thread1休眠一定时间,也就是说,在synchronized之后,休眠一定时间,占用这个线程,这样就不会分给thead2

pri.study.auto.TestF$SpaceTest 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        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1线程开始pri.study.auto.TestF$SpaceTest 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        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1加锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a0 45 03 (00000101 10100000 01000101 00000011) (54894597)
      4     4        (object header)                           ae 02 00 00 (10101110 00000010 00000000 00000000) (686)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread1释放锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a0 45 03 (00000101 10100000 01000101 00000011) (54894597)
      4     4        (object header)                           ae 02 00 00 (10101110 00000010 00000000 00000000) (686)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2线程开始pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 a0 45 03 (00000101 10100000 01000101 00000011) (54894597)
      4     4        (object header)                           ae 02 00 00 (10101110 00000010 00000000 00000000) (686)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2加锁pri.study.auto.TestF$SpaceTest object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           b0 f0 df fc (10110000 11110000 11011111 11111100) (-52432720)
      4     4        (object header)                           cc 00 00 00 (11001100 00000000 00000000 00000000) (204)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

thread2释放锁pri.study.auto.TestF$SpaceTest 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (alignment/padding gap)                  
     16     8   long SpaceTest.time                            0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看到,不是同一个线程了,but !,thread2是轻量级锁,这个也很奇怪,理论上说,thread1释放了锁,回归到了偏向锁,没人抢,thread2加锁之后应该也是偏向锁才对,但是,实际上却是一个轻量级锁…

synchronized锁优化

到这里,我们知道了synchronized的实现原理,也知道了它下面对应的锁有什么,这些锁升级,撤销的变化,也知道了该如何识别,我们来研究最后一个大块,synchronized锁优化

偏向锁的批量重偏向和批量撤销

从偏向锁的加锁和解锁过程,可以看到,当只有一个线程在反复进入和退出的时候,偏向锁带来的开销几乎可以忽略不计,但,当其他线程尝试进入的时候,就必须等待一个安全的切入点,再将偏向锁撤销为无锁状态或升级为轻量级锁,会消耗一定的性能,所以,在多线程竞争的情况下,偏向锁不但不会改善性能,反而会导致性能下降,于是,就出现了批量重偏向和批量撤销机制

我们来用通俗易懂的语言理解一下,假设n个线程,每个线程都创建了一个锁对象,进行了加锁

        public static void main(String[] args) throws Exception {
            Thread.sleep(5000);
            SpaceTest spaceTest = new SpaceTest();
            System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());

            List<Object> locks = new ArrayList<>(10);
                  new Thread(()->{
            for (int i = 0; i < 50; i++) {
                Object lock = new Object();
                synchronized (lock) {
                    locks.add(lock);
                }
            }

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

        }

此时,这个集合里的所有锁,都是偏向锁,假设这个时候,出现了同样多的线程去竞争这把锁

        public static void main(String[] args) throws Exception {
            Thread.sleep(5000);
            SpaceTest spaceTest = new SpaceTest();
            System.out.println(ClassLayout.parseInstance(spaceTest).toPrintable());

            List<Object> locks = new ArrayList<>(10);
               new Thread(()->{
            for (int i = 0; i < 50; i++) {
                Object lock = new Object();
                synchronized (lock) {
                    locks.add(lock);
                }
            }

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

		Thread.sleep(5000);
                    new Thread(
                () -> {
                    for (int i = 0; i < 50; i++) {
                        Object lock = locks.get(i);

                        synchronized (lock) {
                            	// TODO
                            }
                        }

                    }
                }
        ).start();

        }

那,这个集合里的所有偏向锁,都会撤销升级为轻量级锁
在这里插入图片描述

我们知道,偏向锁的撤销和升级是有开销的,这种多线程高并发的情况下,就会出现性能影响,因此,出现了批量重偏向操作

所谓批量重偏向操作,就是说,设置一个阈值,当发现这个class被多次撤销,那说明这个偏向可能存在问题,从阈值往后,也不用撤销了,直接重新偏向,放在上面10的例子,假设阈值是15,我们一共使用了20个线程,1-10线程,都是偏向锁,11-15都是轻量级锁,而15-20,线程都会重新被偏向,是偏向锁,而JVM提供的这个阈值,是20,也就是说,到了20个线程,就会被批量重偏向

原理:以class为单位,为每个class对象维护一个偏向锁撤销计数器,每次一该class的对象发生偏向锁撤销的时候,计数器就加1,当这个次数达到了一定阈值,JVM就认为该class的偏向锁有问题,因此,进行批量重偏向

每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Markword中也有该字段,其初始值为创建该对象时class中epoch的值,每次发生批量重偏向时,就将该值加1,并遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch改为新值,下次获取到锁时,发现当前对象的epoch值和class的epoch值不一样,那就算当前已经偏向了别的线程,也不会执行撤销操作,而是直接CAS,将其Markword的ThreadId改为当前线程ID

那,有批量重偏向,也不能一直偏向啊,对吧,竞争激烈了,该升级的还是会升级的,但是还是这个场景,如果竞争十分激烈,那么一直撤销也会有影响的,所以,就出现了批量撤销机制

所谓批量撤销,就是说,当重偏向达到阈值之后,假设其class的计数器还在增长,达到了批量撤销的阈值,JVM默认该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接开始走轻量级锁的逻辑

这个批量撤销的阈值,JVM默认40

    public static void main(String[] args) throws Exception {
        Thread.sleep(5000);
        List<Object> locks = new ArrayList<>();

        new Thread(()->{
            for (int i = 0; i < 50; i++) {
                Object lock = new Object();
                synchronized (lock) {
                    locks.add(lock);
                }
            }

            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"thread1").start();

        Thread.sleep(5000);
        System.out.println("锁创建完成,第20个锁状态 " + ClassLayout.parseInstance(locks.get(19)).toPrintable());

        new Thread(
                () -> {
                    for (int i = 0; i < 40; i++) {
                        Object lock = locks.get(i);

                        synchronized (lock) {
                            if (i == 17 || i == 18 || i == 19) {
                                System.out.println(Thread.currentThread().getName() + " " + (i+1) + " 加锁 " + ClassLayout.parseInstance(lock).toPrintable());
                            }
                        }

                    }

                    try {
                        Thread.sleep(100000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        ,"thread2").start();

        LockSupport.park();

    }

我们用上述代码进行验证,首先是批量重偏向,为了验证阈值,我们在线程1创建完锁对象之后,打印出第20个锁对象情况,并且,为了防止出现线程复用,我们对线程1进行保活
在这里插入图片描述接着,我们来验证一下批量撤销,这儿小心一个地方,线程2的1-18,在轻量级锁释放锁之后,为无锁状态,19-40,是偏向锁,因为19-40进行了批量重偏向,而线程第41开始到50个锁,都是线程1曾经占用释放,为偏向锁状态

我们加一个线程3,去竞争,看看线程3的1-18,和19-40,41-50分别是什么,以及线程3释放锁完毕之后,锁对象是什么
在这里插入图片描述到这里,你应该仔细想想,是能明白批量重偏向和批量撤销的

这两个机制的应用场景::

  • 批量重偏向为了解决:一个线程创建了大量的对象并执行了初始化的同步操作,后来另一个线程也将这些对象作为锁对象进行操作,导致了大量的锁偏向撤销行为
  • 批量撤销为了解决:在明显的多线程竞争激烈的情况下,使用偏向锁明显不合理

整理:

  1. 批量重偏向和批量撤销针对的是类,而非对象
  2. 偏向锁重偏向一次之后,不可以再重偏向
  3. 当某个类已经触发了批量撤销之后,JVM会认为当前类产生了严重问题,禁止该类再使用偏向锁

附:JVM也有相关参数可以调整

  1. -XX:BiasedLockingBulkRebiasThreshold=20,批量重偏向阈值
  2. -XX:BiasedLockingBulkRevokeThreshold=40,批量撤销阈值

还有一个地方,那就是这个阈值指的是规定时间内,我们可以通过相应参数调整 -XX:BiasedLockingDecayTime=25000ms,时间范围内没达到,那就清零,重新来计数

自旋

重量级锁的竞争,也可以用自旋来进行优化,如果当前线程自旋成功,就可以避免被阻塞,也就是说,这个时候持锁线程已经退出了同步块,释放了锁

自旋会占用CPU的时间资源,单核自旋没有任何用处,只有多核自旋才有用

Java6中,自旋是自适应的,比如某个线程刚自旋成功过,那就认为它这次自旋成功的可能性也高,就多试几次,相反的,就少自旋几次,从Java7开始,不能再选择是否开启自旋

自旋的目的,就是为了减少线程挂起的次数,减少开销,线程挂起需要从用户态切换到内核态,涉及到操作系统的调用,这是重量级锁最大的开销

锁粗化

如果一系列操作,都会对同一个对象反复加锁解锁,甚至,是在循环里加锁解锁,那么,即使没有多线程竞争,本身频繁地进行同步互斥操作,也会有性能问题

JVM如果检测到一系列零碎的操作都在对同一个对象加锁,它就会扩大锁同步的范围到整个序列的外部,这就是锁粗化

比如stringbuffer的append方法,进行连续使用,append本身就是同步方法,JVM就可能会对所有append方法加锁,也就是说,第一次append加锁,最后一次append完了释放锁

锁消除

锁消除,就是删除无意义的加锁操作,锁消除发生在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,节省无意义的请求锁时间

public class Test {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            append(i+"", i + 1 + "");
        }
    }


    public static void append(String s1, String s2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1).append(s2);
    }
    
}

如上述代码,append方法,每次都是新的stringbuffer,不可能有竞争的,加锁什么的,没有任何意义

JDK8默认开启锁消除,可以通过命令来关闭 -XX:-EliminateLocks 关闭锁消除,-XX:+EliminateLocks 开启锁消除,默认开启

逃逸分析

这儿有一个东西了解知道一下,逃逸分析,这是一种可以减少Java程序中同步负载和内存堆分配压力的全局数据流分析算法,通过逃逸分析,HotSpot编译器能够分析出一个新的对象被引用的使用范围,从而决定是否将这个对象分配在堆上,逃逸分析的基本行为,其实就是分析对象的作用域

分为两个逃逸类型,方法逃逸和线程逃逸

所谓方法逃逸,就是说,当一个对象在方法中创建之后,可能会被外部方法引用,比如当做方法的入参传递到其他地方

而所谓线程逃逸,就是说,这个对象可能被其他线程访问到

看我们上面的append方法
在这里插入图片描述
这个stringbuffer对象,就没有逃逸出当前方法

还有我们之前创建50个锁对象的时候
在这里插入图片描述
这个lock,也没有逃逸出当前线程

使用逃逸分析之后,编译器可以对代码做出如下优化:

  • 省略同步或者锁消除:如果一个对象只能从一个线程被访问到,那么对这个对象的操作可以不考虑同步
  • 可以将堆分配改为栈分配:如果一个对象只能从一个线程访问,那么可以直接在当前线程栈创建,不用在堆上创建,还可以避免GC,当线程执行完成,回收线程的时候也就直接清理掉了
  • 可以进行标量替换:有的对象可能并不需要一个连续的内存结构存在也可以被访问到,那么对象的部分,甚至全部属性,都可以不存储在内存,可以存储在寄存器中,比如一个对象,有两个long类型的属性,给它们分别赋值,然后打印它们,虚拟机可以认为这两个long类型就是常量,而不是对象的属性,进行替换,然后给我们结果

总结

锁对象的转换
在这里插入图片描述

偏向锁、轻量级锁、重量级锁的区别

偏向锁:不存在竞争,偏向某个线程,后续线程进入同步块的逻辑没有加锁解锁的开销

轻量级锁:线程间存在轻微竞争,CAS获取锁,失败膨胀,没有自旋

重量级锁:多线程竞争激烈,膨胀期间创建一个monitor对象,CAS自旋,多次尝试无法获取到锁,就调用park,进入阻塞

容易出现的误区

1、升级过程:无锁 ➡️ 偏向锁 ➡️ 轻量级锁 ➡️ 重量级锁
2、轻量级锁自旋获取锁失败,会膨胀升级为重量级锁
3、重量级锁不存在自旋

正解:
1、无锁无法到偏向锁
2、轻量级锁没有自旋操作
3、重量级锁为了防止park,会有很多次自旋

流程图

贴上一个流程图
在这里插入图片描述

至于synchronized的源码,有兴趣的可以研究一下,鄙人不大懂C++这种的,就不误导大家了,但是,可以确定的是,synchronized的实现,一定很精彩

好了,这次关于synchronized的分享到此为止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值