JAVA锁的原理

synchronized锁对象的升级(膨胀)过程

在这里插入图片描述

1.膨胀过程:无锁(锁对象初始化时)-> 偏向锁(有线程请求锁) -> 轻量级锁(多线程轻度竞争)-> 重量级锁(线程过多或长耗时操作,线程自旋过度消耗cpu);

2.jvm默认延时4s自动开启偏向锁(此时为匿名偏向锁,不指向任务线程),可通过-XX:BiasedLockingStartUpDelay=0取消延时;如果不要偏向锁,可通过-XX:-UseBiasedLocking = false来设置

3.锁只能升级,不能降级;偏向锁可以被重置为无锁状态

4.锁对象头记录占用锁的线程信息,但不能主动释放,线程栈同时记录锁的使用信息,当有其他线程(T1)申请已经被占用的锁时,先根据锁对向的信息,找对应线程栈,若线程已结束,则锁对象先被置为无锁状态,再被T1线程占有后置为偏向锁;若线程位结束,则锁状态由当前偏向锁升级为轻量级锁。

5.偏向锁和轻量级锁在用户态维护,重量级锁需要切换到内核态(os)进行维护;

java头的信息分析

这里截取一张hotspot的源码当中的注释
在这里插入图片描述
在这里插入图片描述

意思是java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、gc标记状 态。那么我可以理解java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代 码块。但是java当中的锁有分为很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。这三种锁的效率 完全不同、关于效率的分析会在下文分析,我们只有合理的设计代码,才能合理的利用锁、那么这三种锁的原理是什么? 所以我们需要先研究这个对象头。

java对象的布局以及对象头的布局

JOL来分析java的对象布局

首先添加JOL的依赖

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

示例1

public class A {

}
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

import static java.lang.System.out;

public class JOLExample1 {
    static  A a = new A();
    public static void main(String[] args) {
        //jvm的信息
        out.println(VM.current().details());
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

运行结果:

在这里插入图片描述

分析结果1

Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]对应:[Oop(Ordinary Object Pointer), boolean, byte, char, short, int, float, long, double]大小

整个对象一共16byte,其中对象头(Object header)12byte,还有4byte是对齐的字节(因为在64位虚拟机上对象的大小必 须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0byte?

两个问题 :

1、什么叫做对象的实例数据呢?

2、那么对象头里面的12byte到底存的是什么呢? 首先要明白什么对象的实例数据很简单,我们可以在A当中添加一个boolean的字段,大家都知道boolean字段占 1byte,然后再看结果

示例2

public class A {
    boolean flag =false;
}

运行结果

在这里插入图片描述

分析结果

整个对象的大小还是没有改变一共16byte,其中对象头(Object header)12byte,boolean字段flag(对象的实例数据)占 1byte、剩下的3byte就是对齐字节。由此我们可以认为一个对象的布局大体分为三个部分分别是对象头(Object header)、 对象的实例数据字节对齐 接下来讨论第二个问题,对象头为什么是12byte?这个12byte当中分别存储的是什么呢?(不同位数的VM对象头的长度不一 样,这里指的是64bit的vm)

关于java对象头的一些专业术语 http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.htm

首先引用openjdk文档当中对对象头的解释

object header Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format

上述引用中提到一个java对象头包含了2个word,并且好包含了堆对象的布局、类型、GC状态、同步状态和标识哈 希码,具体怎么包含的呢?又是哪两个word呢?

mark word The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits. mark word

为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,第二个word是什么 呢?

klass pointer The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”.

klass word为对象头的第二个word主要指向对象的元数据。

假设我们理解一个对象头主要mark word和klass word两部分组成(数组对象除外,数组对象的对象头还包含一个数组长度),那么 一个java的对象头多大呢?我们从JVM的源码注释中得知到一个mark word一个是64bit,那么klass的长度是多少呢? 所以我们需要想办法来获得java对象头的详细信息,验证一下他的大小,验证一下里面包含的信息是否正确。 根据上述利用JOL打印的对象头信息可以知道一个对象头是12byte,其中8B是mark word 那么剩下的4B就是klass word了,和锁相关的就是mark word了,那么接下来重点分析mark word里面信息 在无锁的情况下markword当中的前56bit存的是对象的hashcode,那么来验证一下

import org.openjdk.jol.info.ClassLayout;

import static java.lang.System.out;

public class JOLExample2 {
    public static void main(String[] args) throws Exception {
        A a = new A();
        out.println("befor hash");
        //没有计算HASHCODE之前的对象头
        out.println(ClassLayout.parseInstance(a).toPrintable());
        //JVM 计算的hashcode
        out.println("jvm------------0x" + Integer.toHexString(a.hashCode()));
        HashUtil.countHash(a);
        //当计算完hashcode之后,我们可以查看对象头的信息变化
        out.println("after hash");
        out.println(ClassLayout.parseInstance(a).toPrintable());

    }
}
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class HashUtil {
    public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
        // 手动计算HashCode
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        long hashCode = 0;
        for (long index = 7; index > 0; index--) {
            // 取Mark Word中的每一个Byte进行计算
            hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index - 1) * 8);
        }
        String code = Long.toHexString(hashCode);
        System.out.println("util-----------0x"+code);

    }
}

运行结果

在这里插入图片描述

before hash是没有进行hashcode之前的对象头信息,可以看到1-7byte的56bit没有值,打印完hashcode之后就有值了,为什 么是1-7byte,不是0-6byte呢?因为是小端存储。jvm—我们通过hashcode方法打印的结果,util—是我根据1-7B的信息计算出来的 hashcode,所以可以确定java对象头当中的mark work里面的后七个字节存储的是hashcode信息,那么第一个字节当中的八位分别存的 就是分带年龄、偏向锁信息,和对象状态,这个8bit分别表示的信息如下图(其实上图也有信息),这个图会随着对象状态改变而改变, 下图是无锁状态下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OUD2cWfe-1598499318755)(D:\workdocument\开发笔记\锁的原理后8bit标识.png)]

示例3

public class JOLExample3 {
   static A a;
    public static void main(String[] args) throws Exception {
//        Thread.sleep(5000);
        a= new A();
        out.println("befor lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            out.println("lock ing");
           // out.println(ClassLayout.parseInstance(a).toPrintable());
        }
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

结果输出

在这里插入图片描述

public class JOLExample3 {
   static A a;
    public static void main(String[] args) throws Exception {
        Thread.sleep(5000);
        a= new A();
        out.println("befor lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            out.println("lock ing");
           // out.println(ClassLayout.parseInstance(a).toPrintable());
        }
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

在这里插入图片描述

public class JOLExample3 {
   static A a;
    public static void main(String[] args) throws Exception {
//        Thread.sleep(5000);
        a= new A();
        out.println("befor lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            out.println("lock ing");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

在这里插入图片描述

分析结果

上面这个程序只有一个线程去调用sync方法,故而讲道理应该是偏向锁,但是你会发现输出的结果(第一个字节)依 然是00000001和无锁的时候一模一样,其实这是因为虚拟机在启动的时候对于偏向锁有延迟,比如把上述代码当中的 睡眠注释掉结果就会不一样了,结果会变成00000101当然为了方便测试我们可以直接通过JVM的参数来禁用延迟XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0,想想为什么偏向锁会延迟?(除了这8bit,其他bit存储 了线程信息和epoch,这里不截图了),需要注意的after lock,退出同步后依然保持了偏向信息,在偏向锁延迟时间内默认开启轻量级锁 00000000。

示例4

public class JOLExample7 {
    static A a;
    public static void main(String[] args) throws Exception {
        //Thread.sleep(5000);
        a = new A();
        out.println("befre lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());//无锁

        Thread t1= new Thread(){
            public void run() {
                synchronized (a){
                    try {
                        Thread.sleep(5000);
                        System.out.println("t1 release");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();
        Thread.sleep(1000);
        out.println("t1 lock ing");
        out.println(ClassLayout.parseInstance(a).toPrintable());//轻量锁
        sync();
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());//无锁

        System.gc();
        out.println("after gc()");
        out.println(ClassLayout.parseInstance(a).toPrintable());//无锁---gc
    }

    public  static  void sync() throws InterruptedException {
        synchronized (a){
            System.out.println("t1 main lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
        }
    }
}

在这里插入图片描述
在这里插入图片描述

结果分析

量级锁的对象头标识是01001010,在退出同步代码块后对象头不发生变化仍然是01001010,在GC标记后对象头才发生变化为00001001,分代年龄标记了0001 锁标记为001。

示例5

如果调用wait方法则立刻变成重量锁

public class JOLExample9 {
    static A a;

    public static void main(String[] args) throws Exception {
        //Thread.sleep(5000);
        a = new A();
        out.println("befre lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());

        Thread t1 = new Thread() {
            public void run() {
                synchronized (a) {
                    try {
                        synchronized (a) {
                            System.out.println("before wait");
                            out.println(ClassLayout.parseInstance(a).toPrintable());
                            a.wait();
                            System.out.println(" after wait");
                            out.println(ClassLayout.parseInstance(a).toPrintable());
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t1.start();
        Thread.sleep(5000);
        synchronized (a) {
            a.notifyAll();
        }
    }

    public static void sync() throws InterruptedException {
        synchronized (a) {
            System.out.println("t1 main lock");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
    }

}

在这里插入图片描述

需要注意的是如果对象已经计算了hashcode就不能偏向了

public class JOLExample8 {
   static A a;
    public static void main(String[] args) throws Exception {
        Thread.sleep(5000);
        a= new A();
        a.hashCode();
        out.println("befor lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
        synchronized (a){
            out.println("lock ing");
            out.println(ClassLayout.parseInstance(a).toPrintable());
        }
        out.println("after lock");
        out.println(ClassLayout.parseInstance(a).toPrintable());
    }
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的机制是线程同步的关键,它用于控制多个线程对共享资源的访问,以避免数据竞争和并发问题。Java主要由`synchronized`关键字、`Lock`接口(如`ReentrantLock`)以及`Condition`类来实现。 **底层原理概述:** 1. **监视器(Monitor)和定:**每个对象都关联着一个监视器,当一个线程进入对象的`synchronized`代码块或方法时,它会获取该对象的监视器。如果另一个线程尝试获取同一对象的,那么线程会被阻塞直到第一个线程释放。 2. **互斥(Mutual Exclusion):**同一时间只有一个线程可以持有对象的,这就是互斥原则。其他等待的线程必须等到当前线程执行完毕并释放。 3. **可见性(Visibility):**当一个线程修改了共享状后,必须调用`notify()`或`notifyAll()`方法通知其他等待的线程,使它们能够重新检查条件并获得。 4. **等待(Waiting)和唤醒(Signal):**`wait()`方法会让当前线程放弃,进入等待状,直到被其他线程调用`notify()`或`notifyAll()`唤醒。如果等待过程中线程被中断,它会抛出`InterruptedException`。 5. **重入(Recursion):`synchronized`关键字支持一个线程在同一对象上多次获取,但只有在未被其他线程占用时才允许。 **ReentrantLock扩展:**Java的`ReentrantLock`提供了更多的灵活性,比如可选择公平(按等待线程顺序获取)和非公平(最快的线程优先获取),以及显式的获取和释放。此外,它还提供`tryLock()`方法用于无操作,以及条件变量`Condition`用于更复杂的同步场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值