Synchronized笔记

在分析synchronized之前,我们先了解一些概念:用户态和内核态、CAS、java对象布局

用户态和内核态

平时我们所写的java程序是运行在用户空间的,因为我们的jvm对于操作系统来讲就是一个普通程序。用户空间的程序要执行读写硬盘、读写网络、读写内存等重要操作时必须经过操作系统内核来进行。

在JDK早期,Synchronized是重量级锁,每次申请锁都需要调用系统内核。需要从用户空间切换到内核空间,拿到锁后再将状态返回给用户空间。

CAS

cas(compare and swap)它指的是比较与交换,使用无锁的机制保证操作对象的原子性,说是无锁,其实它是一个自旋锁。

首先读取当前值,在计算预期的结果值,在把值修改回去的时候,要比较一下原来读出来的值和现在的值是否相等,如果相等,说明没有别的线程改动过,更新为新的值。如果不一样则说明已经别的线程改过了,这个时候,再次读取当前值,重新再来一遍。cas的底层是由汇编指令lock和cmpxchg(compare and exchange)组合在一起来支撑的。

ABA问题解决:使用version标记,每次操作version加1

java对象布局

openjdk提供了一个查看java对象布局的工具,在maven中引入如下依赖即可使用

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

当我们new 了一个java对象的时候,它在jvm里面一定是占据了一块内存的,那么这个java对象在内存中的布局是什么样子的?

一个普通对象在内存中可以分成4个部分

  • markword:对象头标记字,存储对象自身的运行时数据信息,如锁状态标志,偏向线程ID等。
  • class pointer:存储类元数据的指针,它指向一个对象到底属于哪个类。例如Object.class。
  • instance data:对象自身真正有效数据存储区域,存储了各个字段的内容
  • padding:如果一个对象前面的三部分不能被8字节整除,补齐到能整除

下面我们用jol这个工具来看一下一个对象在内存中的布局
测试demo:

public class MackWordTest {
    public static void main(String[] args) {
        Object obj = new Object();
        String print = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(print);
        System.out.println("===========================================================");
        synchronized (obj) {
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }
}

运行结果如下:

前8个字节为mark word,后面4个字节为class pointer,然后是补齐。
给对象加上synchronized同步锁之后,我们发现mark word发生了变化,说明锁信息存在了mark word里面,给对象加锁,其实就是修改mark word。

接下来我们就来看synchronized锁升级的过程

锁升级

我们要探究锁升级的过程,只需要看它mark word的变化过程就可以了

锁状态25位31位1位4bit1bit偏向锁位2bit锁标志位
无锁态(new)unusedhashCodeunused分代年龄001
锁状态54位2位1位4bit1bit偏向锁位2bit锁标志位
偏向锁当前线程指针JAVA ThreadEpochunused分代年龄101
锁状态62位2bit锁标志位
轻量级锁/自旋锁指向线程栈中Lock Record的指针00
重量级锁指向重量级锁的指针(互斥量)10
GC标记信息CMS过程用到的标记信息11

锁的状态分成4中:无锁、偏向锁、轻量级锁或自旋锁、重量级锁
怎么区分锁的状态呢?看锁的标志位和偏向锁标志位就好了
首先看锁的标志位:
00代表轻量级锁,10代表重量级锁,我们看到无锁态和偏向锁的锁标志位都是01,这个时候再看偏向锁标志位,也就是看后三位,001是无锁,101是偏向锁。
表中当前线程指针JAVA Thread指向的就是当前线程ID
Lock Record:
由于synchronized默认是可以重入的,每个线程上自旋锁的时候,在自己的线程栈中生成一个LockRecord对象,哪个线程能在markword里写入自己LockRecord对象的指针,就算是持有了锁。锁重入的时候再次生成一个LockRecord,这样就记录了到底锁了多少次。

锁升级过程:

一个java对象刚new出来的时候,它有可能是无锁态或者匿名偏向状态,这个时候我们再用synchronized(obj)给这个对象上锁,优先上偏向锁。

偏向锁就是说它偏向于某个线程,因为我们在日常使用锁的时候,大多数都是在一个线程,为了这一个线程要去调用系统内核kernel,太浪费资源了,所以JDK在这里进行了优化。凡是有一个线程第一次获得这把锁,就认为这把锁偏向于它,也就是不惊动操作系统内核,只需要将线程ID放入mark word里就可以了。

当我们有多个线程竞争同一个资源的时候,锁升级为轻量级锁/自旋锁

自旋锁就是多个线程去竞争同一把锁,通过CAS的方式,哪个线程能把自己的信息写入mark word,就算是持有了锁。自旋锁也不需要调用操作系统内核。

如果有特别多的线程同时去竞争一把锁,那这个时候自旋锁就会出现问题,大家想一下,我们的自旋锁是一直在做while循环,假设有10000个线程参与竞争,这个时候真正在干活的只有1个线程,剩下的9999个线程都在做while自旋,cpu资源全都浪费了。所以这种情况下需要升级成为重量级锁

在hotspot虚拟机中,重量级锁的底层实现就是ObjectMonitor(对象监视器),对于每个对象来说,在重量级锁的状态下,mark word中Lock Word指向ObjectMonitor(对象监视器)的起始地址,对象就是这样与monitor建立关联的。

每个对象实例都会有一个 monitor(对象监视器),ObjectMonitor里面有两个队列_WaitSet和_EntryList,分别记录了等待的线程和参与竞争的线程,还有一个_owner用来保存持有对象锁的线程的唯一标识,而ObjectMonitor本质上是依赖于操作系统内核的mutext lock(互斥锁)来实现的,这就需要从用户态切换到内核态。这里的mutex lock的最终实现也是lock和cmpxchg

重量级锁轻量级锁最大的区别在于,重量级锁经过操作系统内核的调度之后,系统内核提供一把锁的同时,还会为锁提供wait set队列,这些获取不到锁的线程都进入队列等待,什么时候获取到锁,线程才能继续执行。重量级锁需要阻塞和唤醒线程,这些操作都需要操作系统内核来帮忙,这就需要从用户态切换到内核态,所以效率较低

这里面重量级锁需要经过系统内核kernel,而偏向锁和轻量级锁在用户空间就可以完成。

下面我们来看一些细节,轻量级锁在什么情况下会升级成为重量级锁?

JDK1.6之前,轻量级锁升级成重量级锁有两个条件:

  • 轻量级锁自旋次数超过10次
  • 等待线程的数量超过cpu核数的2分之一

JDK1.6之后对此进行了优化,引入了自适应自旋,JDK会根据每个线程的运行情况来判断是不是要升级。

从上图中可以看到,偏向锁没有启动的时候,我们new了一个对象,它是一个普通对象,偏向锁已经启动的时候,new出来是一个匿名偏向对象。这里到底是什么意思?

首先java提供了偏向锁启动配置的参数(使用java -XX:+PrintFlagsFinal可以查看JVM可设置的参数):
偏向锁启动延迟时间: -XX:BiasedLockingStartupDelay=4000
偏向锁开关:-XX:UseBiasedLocking=true
偏向锁默认是打开的,它有一个启动延迟,默认是4秒钟。

为什么偏向锁要延迟4秒?

jvm在启动过程中是有大量的线程竞争资源的,这个时候启动偏向锁是没有意义的,所以延迟4秒,等待JVM启动。

在偏向锁已经启动的情况下,刚new的一个对象,还没有任何线程持有这把锁,这个时候没有偏向任何线程,所以是匿名偏向

在偏向锁未启动的情况下,new出来的对象就是普通对象,在偏向锁还没启动的时候,如果有竞争,这个时候就直接升级成轻量级锁。

我们先来看一下普通对象

    public static void main(String[] args) {
        Object obj = new Object();
        String print = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(print);

    }

运行结果:


我们看到运行结果是001,也就是无锁态(对照前面锁状态表中的结果),说明这个时候new 出来是一个普通对象。
我们前面说过JVM默认是开启了偏向锁的,只不过要延迟4秒钟,在上面的demo中,new object()操作自然是远远不到4秒钟,所以这个时候偏向锁还未启动,new 出来的object对象就是一个普通对象,处于无锁态。

我们让它睡眠个5秒钟,再来看一下匿名偏向

    public static void main(String[] args) throws Exception{
        // 睡眠5秒
        TimeUnit.SECONDS.sleep(5);
        Object obj = new Object();
        String print = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(print);

    }

运行结果:

这个时候结果是101,表示偏向锁,new 出来的object对象,这个时候还没有任何线程持有这个对象的锁,所以是匿名偏向。

接着看下普通对象在偏向锁未启动的情况下,给对象加上synchronized,它会是什么样一个状态

 public static void main(String[] args) throws Exception{
        Object obj = new Object();
        String print = ClassLayout.parseInstance(obj).toPrintable();
        System.out.println(print);
        System.out.println("===========================================================");
        synchronized (obj) {
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }

运行结果:

我们看到这个对象由刚开始的 001(无锁态)转变到 00(轻量级锁)

 

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值