美文网首页
Java锁的原理

Java锁的原理

作者: 不知名的蛋挞 | 来源:发表于2020-02-14 18:51 被阅读0次

    引子

    public class L {
    }
    
    package com.vip.poc.backend.vpao.lock;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Test {
        static L l = new L();
        static ReentrantLock reentrantLock = new ReentrantLock();
    
        public static void main(String[] args){
            lockTest();
        }
    
        /**
         * 假设现在lockTest()存在线程安全问题,需要对它进行同步操作,应该怎么做呢?
         * (1)synchronized
         * (2)reentrantLock
         */
        public static void lockTest(){
            //juc并发包中的锁
            /**
             * reentrantLock.lock()的本质就是改变reentrantLock对象中的值,把它从0改成1
             * 如果改成功的话就是上锁成功
             */
            reentrantLock.lock();
            System.out.print("xxxx");
            reentrantLock.unlock();
    
            // java内置锁
            /**
             * 给l上锁,至少有个标识说明给l上锁成功了
             * 那么在哪里体现给l加了锁呢?现在这个L对象里面什么都没有,没有变量体现线程给它加了锁
             * 所以有个线程通过synchronized关键字给对象加锁了,我们到底改变了这个对象什么东西?
             */
            synchronized (l){
                System.out.print("xxxx");
            }
        }
    }
    
    

    上锁就是改变对象的对象头?
    什么是对象头?----Java对象的布局----Java对象由什么组成----对象在堆上----把对象new出来之后,对象在堆上到底要分配多大内存?----分配的内存肯定是不固定的,要是固定就不会有内存溢出了

    至少要考虑:

    1. Java对象的实例数据----不固定,定义了多少变量,就有多大。要是对象里面没有任何属性,那么就没有实例数据;如果定义了一个int对象,那么就有4 byte
    2. 对象头----固定
    3. 数据对齐----64bit jvm要求一个数据必须是8的整数位byte
    public class L {
        /**
         * 实际分配内存时并不只为这个对象分配1 byte
         * 因为64位的虚拟机规定对象大小一定要是8的整数倍
         * 所以即使对象只有1 byte,但是在堆上也会为此对象分配8 byte来存放数据
         * 其中的7 byte仅仅是为了让数据对齐
         */
        boolean flag = false; // 1 byte
    }
    

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

    1. JOL开分析Java的对象布局

    // 添加JOL的依赖
    org.openjdk.jol:jol-core:0.9
    

    L.java

    public class L {
        boolean flag = false;
    }
    

    JOLExample.java

    import org.openjdk.jol.info.ClassLayout;
    import org.openjdk.jol.vm.VM;
    
    public class JOLExample {
        static L l = new L();
    
        public static void main(String[] args){
            // 打印对象布局
            System.out.print(ClassLayout.parseInstance(l).toPrintable());
        }
    }
    

    运行结果:l对象占用的内存是16 bytes

    这个对象一共16B,其中对象头(Object header)12B,对象的实例数据为1B,还有3B是对齐的字节(因为在64位虚拟机上的对象大小必须是8的倍数)。

    其中实例数据和对齐填充数据有可能有,也有可能没有。但是对象头一定有。那么问题来了?对象头里面的12B到底存的是什么呢?

    2. 什么是JVM?什么是HotSpot?

    • JVM可以理解为一种规范或者标准,规定了将来你要实现JVM你要怎么做。
    • HotSpot可以理解为一个产品/JVM的一种实现。除了HotSpot之外,j9,taobaovm也是JVM的实现。
    • openjdk是一个c++的项目/代码,HotSpot就是基于openjdk开发出来的。HotSpot里面80%都是openjdk的代码。

    所以就由JVM就规定了对象头长什么样子,由什么组成。无论是HotSpot还是j9,对象头的组成都必须按照JVM的规范。

    在openjdk的官方文档(http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html)中,可以看到对象头的定义:

    也就是下面这12字节里面包含了很多的信息,比如对象的hashcode(相当于对象地址),同步状态(synchronized加锁)、GC状态(记录对象的年龄)、对象类型(对象属于哪个类,换言之,这12个字节里面保存了类的模板信息)。

    【由两个词组成】,词,在计算机原理中就是【字长】的意思。Java中没有词这个概念,我们可以把它看作是属性,也就是【有两部分/两个属性组成】,从文档可以发现这两个部分就是 1. Mark Word 2. klass pointer。

    3. Mark Word

    HotSpot源代码中有一个markOop.hpp文件,里面就定义了Mark Word的信息。Mark Word占用的内存大小在文件中的注释写得非常清楚。

    • 在32位JVM中,前25bit存储hashcode,跟着后面4bit存储GC年龄(age),然后1bit存储偏向锁(biased_lock)信息,再后面2bit存储同步状态(lock),总共占32bit(size:32)
    • 在64位JVM中,Mark Word占用内存不一定固定是64bit的,根据对象状态不同,存的东西不一样。


    Java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态:无锁状态(刚刚new出来) 、加锁状态、gc标记状态(方法已经执行完了,对象引用已经没有了,被gc标记 )

    那么在不同状态情况下,存储的信息是什么样子的呢?下图进行说明。

    其中lock存储的是锁的信息(锁的状态),用2 bit来存储。2 bit最多能存4种状态(00 01 10 11),那怎么表示上述5种状态呢?其实有锁和无锁状态是用biased_lock和lock联合表示的。比如无锁状态下,biased_lock表示为0,lock为10;偏向锁状态下,biased_lock就表示为1,lock还是为10。后面3种状态就直接用00 01 11 表示即可。

    public class JOLExample {
        static L l = new L();
    
        public static void main(String[] args){
            // 打印无状态对象布局
            System.out.print(ClassLayout.parseInstance(l).toPrintable());
        }
    }
    

    输出:

    看到这里会不会很疑惑,不是说无状态对象前25bit是unused的吗(也就是应该全部为0),为什么还会有一个1出现呢?而且unused后面不是应该存储hashcode吗,为啥全都是0呢(全都是0,表示没有存储东西)?

    hashcode是地址,需要计算。l.hashCode()就是用来计算hashcode的。hashCode()是一个native方法(底层用c++写的),对象存在哪里只有c++知道。我们把代码改成:

    public class JOLExample {
        static L l = new L();
    
        public static void main(String[] args){
            System.out.print(l.hashCode());
            System.out.print(ClassLayout.parseInstance(l).toPrintable());
        }
    }
    

    输出:此时就有了hashcode

    那么前面的8个bit明明是unused,为啥还会有数据呢?这是电脑原因导致的。mac电脑是大端存储(从内存的低地址开始,先存储数据的高序字节再存储数据的低序字节),而本台电脑是小端存储(从内存的低地址开始,先存储数据的低序字节再存高序字)。例如:

    十进制数9877(二进制为10011010010101),
    
    用小端存储表示则为: 
    高地址 <- - - - - - - - 低地址 
    10010101[高序字节] 00100110[低序字节] 
    用大端存储表示则为: 
    高地址 <- - - - - - - - 低地址 
    00100110[低序字节] 10010101[高序字节]
    

    所以图中的字节是是倒着存放的,00000001表示的是图里面identity_hashcode后面【unused+age+biased_lock+lock】这部分加起来的8bit。

    那么我可以理解Java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。

    但是Java当中的锁有分为很多种,从输出结果可以看出大体分为偏向锁(只有一个线程持有这个对象)、轻量锁、重量锁三种锁状态。这三种锁的效率完全不同,关于效率的分析会在下文分析,我们只有合理地设计代码,才能合理地利用锁,那么这三种锁的原理是什么呢?所以我们需要先研究这个对象头。

    相关文章

      网友评论

          本文标题:Java锁的原理

          本文链接:https://www.haomeiwen.com/subject/grhdfhtx.html