精华内容
下载资源
问答
  • MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。我们知道,数据库查询是数据库的最主要功能之一。我们都希望查询数据的速度能尽...
  • Vue底层实现原理总结

    2020-08-28 00:25:43
    小编给大家整理Vue底层实现原理的知识点总结,如果大家对此有需要,可以学习参考下,希望我们整理的内容能够帮助到你。
  • 今天小编就为大家分享一篇对ArrayList和LinkedList底层实现原理详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • 主要介绍了Java CAS底层实现原理实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 今天小编就为大家分享一篇Python字典底层实现原理详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • 主要为大家详细介绍了Vue数据双向绑定底层实现原理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • HashMap底层实现原理详解

    万次阅读 多人点赞 2021-02-13 22:41:16
    提示:以下是本篇文章对HashMap的实现原理内容,下面案例可供参考 提示:以下是本篇文章正文内容,下面案例可供参考 一、快速入门 示例:有一定基础的小伙伴们可以选择性的跳过该步骤 HashMap是Java程序员使用频率...


    一、快速入门

    示例:有一定基础的小伙伴们可以选择性的跳过该步骤

    HashMap是Java程序员使用频率最高的用于映射键值对(key和value)处理的数据类型。随着JDK版本的跟新,JDK1.8对HashMap底层的实现进行了优化,列入引入红黑树的数据结构和扩容的优化等。本文结合JDK1.7和JDK1.8的区别,深入探讨HashMap的数据结构实现和功能原理。
    Java为数据结构中的映射定义了一个接口java.uti.Map,此接口主要有四个常用的实现类,分别是HashMap,LinkedHashMap,Hashtable,TreeMap,IdentityHashMap。本篇文章主要讲解HashMap以及底层实现原理。

    1.HashMap的常用方法

    
    //        Hashmap存值:----------------------------------》 .put("key","value"); ----------》无返回值。
    //
    //        Hashmap取值:----------------------------------》 .get("key");-------------------》 返回Value的类型。
    //
    //        Hashmap判断map是否为空:-----------------------》 .isEmpty(); -------------------》返回boolean类型。
    //
    //        Hashmap判断map中是否存在这个key:--------------》.containsKey("key");------------》返回boolean类型。
    //
    //        Hashmap判断map中是否含有value:----------------》.containsValue("value");-------》返回boolean类型。
    //
    //        Hashmap删除这个key值下的value:----------------》.remove("key");-----------------》返回Value的类型。
    //
    //        Hashmap显示所有的value值:---------------------》.values(); --------------------》返回Value的类型。
    //
    //        Hashmap显示map里的值得数量:-------------------》.size(); ----------------------》返回int类型
    //
    //        HashMap显示当前已存的key:---------------------》 .keySet();-------------------》返回Key的类型数组。
    //
    //        Hashmap显示所有的key和value:-----------------》.entrySet());------------------》返回Key=Value类型数组。
    //
    //        Hashmap添加另一个同一类型的map:--------------》.putAll(map); -----------------》(参数为另一个同一类型的map)无返回值。
    //
    //        Hashmap删除这个key和value:------------------》.remove("key", "value");-------》(如果该key值下面对应的是该value值则删除)返回boolean类型。
    //
    //        Hashmap替换这个key对应的value值(JDK8新增):---》.replace("key","value");-------》返回被替换掉的Value值的类型。
    //
    //        克隆Hashmap:-------------------------------》.clone(); ---------------------》返回object类型。
    //
    //        清空Hashmap:-------------------------------》.clear(); ---------------------》无返回值。
    
    

    2.HashMap的几个重要知识点

    1. HashMap是无序且不安全的数据结构。

    2. HashMap 是以key–value对的形式存储的,key值是唯一的(可以为null),一个key只能对应着一个value,但是value是可以重复的。

    3. HashMap 如果再次添加相同的key值,它会覆盖key值所对应的内容,这也是与HashSet不同的一点,Set通过add添加相同的对象,不会再添加到Set中去。

    4. HashMap 提供了get方法,通过key值取对应的value值,但是HashSet只能通过迭代器Iterator来遍历数据,找对象。


    二、JDK7与JDK8的HashMap区别

    既然讲HashMap,那就不得不说一下JDK7与JDK8(及jdk8以后)的HashMap有什么区别:

    1. jdk8中添加了红黑树,当链表长度大于等于8的时候链表会变成红黑树
    2. 链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红 黑树所以采用插入尾节点)
    3. hash算法简化 ( jdk8 )
    4. resize的逻辑修改(jdk7会出现死循环,jdk8不会)

    三、HashMap的容量与扩容机制

    1.HashMap的默认负载因子

        /**
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        /**
         *默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用
         *无参构造方法创建的HashMap 对象,他初始长度默认是16  阈值 = 当前长度 * 0.75  就
         *能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容
         */
    
    

    面试的时候,面试官经常会问道一个问题:为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
    答案有两种:

    1. 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

    2. 理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的

    2.HashMap的扩容机制

    写数据之后会可能触发扩容,HashMap结构内,我记得有一个记录当前数据量的字段,这个数据量字段到达扩容阈值的话,它就会触发扩容的操作

    阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 
    当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
    
    扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
    假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
    容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2
    

    问题又来了,为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?
    这个问题就比较简单了,因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

    3.HashMap中散列表数组初始长度

        /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        /**
         * HashMap中散列表数组初始长度为 16 (1 << 4)
         * 创建HashMap的时候可以设置初始化容量和设置负载因子,
         * 但HashMap会自动优化设置的初始化容量参数,确保初始化
         * 容量始终为2的幂
         */
    
    

    老问题又来了,为啥HashMap中初始化大小为什么是16呢?

    首先我们看hashMap的源码可知当新put一个数据时会进行计算位于table数组(也称为桶)中的下标:

    int index =key.hashCode()&(length-1);

    hahmap每次扩容都是以 2的整数次幂进行扩容

    因为是将二进制进行按位于,(16-1) 是 1111,末位是1,这样也能保证计算后的index既可以是奇数也可以是偶数,并且只要传进来的key足够分散,均匀那么按位于的时候获得的index就会减少重复,这样也就减少了hash的碰撞以及hashMap的查询效率。

    那么到了这里你也许会问? 那么就然16可以,是不是只要是2的整数次幂就可以呢?

    答案是肯定的。那为什么不是8,4呢? 因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小。


    四、HashMap的结构

    JDK7与JDK8及以后的HashMap结构与存储原理有所不同:
    Jdk1.7:数组 + 链表 ( 当数组下标相同,则会在该下标下使用链表)
    Jdk1.8:数组 + 链表 + 红黑树 (预值为8 如果链表长度 >=8则会把链表变成红黑树 )
    Jdk1.7中链表新元素添加到链表的头结点,先加到链表的头节点,再移到数组下标位置
    Jdk1.8中链表新元素添加到链表的尾结点
    (数组通过下标索引查询,所以查询效率非常高,链表只能挨个遍历,效率非常低。jdk1.8及以
    上版本引入了红黑树,当链表的长度大于或等于8的时候则会把链表变成红黑树,以提高查询效率)


    五、HashMap存储原理与存储流程

    1.HashMap存储原理

    1. 获取到传过来的key,调用hash算法获取到hash值

    2. 获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算
      出数组的下标 (把哈希值和数组容量转换为二进,再在数组容量范围内与哈希值
      进行一次与运算,同为1则1,不然则为0,得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量)

    3. 把传过来的key和value存到该数组下标当中。

    4. 如该数组下标下以及有值了,则使用链表,jdk7是把新增元素添加到头部节点 jdk8则添加到尾部节点。

    2.HashMap存储流程

    前面寻址算法都是一样的,根据key的hashcode经过高低位异或之后的值,再按位与 &(table.lingth - 1),得到一个数组下标,然后根据这个数组下标内的状况,状况不同,然后情况也不同,大概分为了4种状态:

    ( 1.)第一种就是数组下标下内容为空:
    这种情况没什么好说的,为空据直接占有这个slot槽位就好了,然后把当前.put方法传进来的key和value包装成一个node对象,放到这个slot中就好了。

    ( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:
    这种情况下先要对比一下这个node对象的key与当前put对象的key是否完全.相等,如果完全相等的情况下,就行进行replace操作,把之前的槽位中node.下的value替换成新的value就可以了,否则的话这个put操作就是一个正儿.八经的hash冲突,这种情况在slot槽位后面追加一个node就可以了,用尾插法 ( 前面讲过,jdk7是把新增元素添加到头部节点,而jdk8则添加到尾部节点)。

    ( 3.)第三种就是该数组下标下内容已经被链化了:
    这种情况和第二种情况处理很相似,首先也是迭代查找node,看看链表上中元素的key,与当前传过来的key是否完全一致,如果完全一致的话还是repleace操作,用put过来的新value替换掉之前node中的value,否则的话就是一致迭代到链表尾节点也没有匹配到完全一致的node,就和之前的一样,把put进来数据包装成node追加到链表的尾部,再检查一下当前链表的长度,有没有达到树化阈值,如果达到了阈值就调用一个树化方法,树化操作都是在这个方法里完成的。

    ( 4.)第四种情况就是冲突很严重的情况下,这个链表已经转化成红黑树了:
    红黑树就比较复杂 要将清楚这个红黑树还得从TreeNode说起 TreeNode继承了Node结构,在Node基础上加了几个字段,分别是指向父节点parent字段,指向左子节点left字段,指向右子节点right字段,还有一个表示颜色的red字段,这就是TreeNode的基本结构,然后红黑树的插入操作,首先找到一个合适的插入点,就是找到插入节点的父节点,然后红黑树它又满足二叉树的所有特性,所以找这个父节点的操作和二叉树排序是完全一致的,然后说一下这个二叉树排序,其实就是二分查找算法映射出来的结构,就是一个倒立的二叉树,然后每个节点都可以有自己的子节点,本且左节点小于但前节点,右节点大于当前节点,然后每次向下查找一层就能那个排除掉一半的数据,查找效率非常的高效,当查找的过程中也是分情况的。

    1. 首先第一种情况就是一直向下探测,直到查询到左子树或者右子树位null,说明整个树中,并没有发现node链表中的key与当前put key一致的TreeNode,那此时探测节点就是插入父节点的所在了,然后就是判断插入节点的hash值和父节点的hash值大小决定插入到父节点的左子树还是右子树。当然插入会打破平衡,还需要一个红黑树的平衡算法保持平衡。

    2. 其次第二种情况就是根节点在向下探测过程中发现TreeNode中key与当前put的key完全一致,然后就也是一次repleace操作,替换value。


    六、jdk8中HashMap为什么要引入红黑树?

    其实主要就是为了解决jdk1.8以前hash冲突所导致的链化严重的问题,因为链表结构的查询效率是非常低的,他不像数组,能通过索引快速找到想要的值,链表只能挨个遍历,当hash冲突非常严重的时候,链表过长的情况下,就会严重影响查询性能,本身散列列表最理想的查询效率为O(1),当时链化后链化特别严重,他就会导致查询退化为O(n)为了解决这个问题所以jdk8中的HashMap添加了红黑树来解决这个问题,当链表长度>=8的时候链表就会变成红黑树,红黑树其实就是一颗特殊的二叉排序树嘛,这个时间复杂…反正就是要比列表强很多


    七、扩容后的新table数组,那老数组中的这个数据怎么迁移呢

    迁移其实就是挨个桶位推进迁移,就是一个桶位一个桶位的处理,主要还是看当前处理桶位的数据状态把,这里也是分了大概四种状态:
    这四种的迁移规则都不太一样

    (1.)第一种就是数组下标下内容为空:
    这种情况下就没什么可说的,不用做什么处理。

    ( 2.)第二种情况就是数组下标下内容不为空,但它引用的node还没有链化:
    当slot它不为空,但它引用的node还没有链化的时候,说明这个槽位它没有发生过hash冲突,直接迁移就好了,根据新表的tableSize计算出他在新表的位置,然后存放进去就好了。

    ( 3.)第三种就是slot内储存了一个链化的node:
    当node中next字段它不为空,说明槽位发生过hash冲突,这个时候需要把当前槽位中保存的这个链表拆分成两个链表,分别是高位链和低位链

    (4.)第四种就是该槽位储存了一个红黑树的根节点TreeNode对象:
    这个就很复杂了,本文章暂时不做过多的介绍(博主还没整明白 =_=! )




    展开全文
  • HashMap底层实现原理解析

    万次阅读 多人点赞 2020-12-18 10:37:27
    一:HashMap底层实现原理解析 我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点: 1、数组结构: 存储区间连续、内存占用严重、空间复杂度大 优点:...

    一:HashMap底层实现原理解析

    我们常见的有数据结构有三种结构:1、数组结构 2、链表结构 3、哈希表结构 下面我们来看看各自的数据结构的特点:
    1、数组结构: 存储区间连续、内存占用严重、空间复杂度大

    • 优点:随机读取和修改效率高,原因是数组是连续的(随机访问性强,查找速度快)
    • 缺点:插入和删除数据效率低,因插入数据,这个位置后面的数据在内存中都要往后移动,且大小固定不易动态扩展。

    2、链表结构:存储区间离散、占用内存宽松、空间复杂度小

    • 优点:插入删除速度快,内存利用率高,没有固定大小,扩展灵活
    • 缺点:不能随机查找,每次都是从第一个开始遍历(查询效率低)

    3、哈希表结构:结合数组结构和链表结构的优点,从而实现了查询和修改效率高,插入和删除效率也高的一种数据结构
    常见的HashMap就是这样的一种数据结构
    在这里插入图片描述

    HashMap中的put()和get()的实现原理

    • 1、map.put(k,v)实现原理
      (1)首先将k,v封装到Node对象当中(节点)。
      (2)然后它的底层会调用K的hashCode()方法得出hash值。
      (3)通过哈希表函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着k和链表上每个节点的k进行equal。如果所有的equals方法返回都是false,那么这个新的节点将被添加到链表的末尾。如其中有一个equals返回了true,那么这个节点的value将会被覆盖。
    • 2、map.get(k)实现原理
      (1)先调用k的hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。
      (2)通过上一步哈希算法转换成数组的下标之后,在通过数组下标快速定位到某个位置上。如果这个位置上什么都没有,则返回null。如果这个位置上有单向链表,那么它就会拿着K和单向链表上的每一个节点的K进行equals,如果所有equals方法都返回false,则get方法返回null。如果其中一个节点的K和参数K进行equals返回true,那么此时该节点的value就是我们要找的value了,get方法最终返回这个要找的value。

    为何随机增删、查询效率都很高的原因是?
    原因: 增删是在链表上完成的,而查询只需扫描部分,则效率高。

    HashMap集合的key,会先后调用两个方法,hashCode and equals方法,这这两个方法都需要重写。

    为什么放在hashMap集合key部分的元素需要重写equals方法?
    因为equals方法默认比较的是两个对象的内存地址

    二:HashMap红黑树原理分析

    相比 jdk1.7 的 HashMap 而言,jdk1.8最重要的就是引入了红黑树的设计,当hash表的单一链表长度超过 8 个的时候,链表结构就会转为红黑树结构。
    为什么要这样设计呢?好处就是避免在最极端的情况下链表变得很长很长,在查询的时候,效率会非常慢。
    在这里插入图片描述

    • 红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
    • 链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n);

    简单的说,红黑树是一种近似平衡的二叉查找树,其主要的优点就是“平衡“,即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度为 log(n)。
    在这里插入图片描述
    关于红黑树的内容,网上给出的内容非常多,主要有以下几个特性:

    • 1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;

    • 2、每个红色节点的两个子节点一定都是黑色;

    • 3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);

    • 4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

    • 5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);

    在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件 3 或条件 4,需要通过调整使得查找树重新满足红黑树的条件。

    展开全文
  • 手写RPC底层实现原理

    2018-09-19 16:52:21
    让你对RPC有一个更加深层次的了解,让你明白服务端之间的调用以及如何去通信的(Socket)
  • 主要介绍了Java synchronize底层实现原理及优化,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 彻底理解Synchronized底层实现原理

    千次阅读 多人点赞 2020-05-16 11:30:00
    这篇文章会记录Synchronized的常用使用场景与Synchronized的底层实现原理。虽然我们平时经常会在多线程中使用Synchronized关键字,但可能对于这个我们很熟悉的关键字的底层到底是怎样实现的没有过多关注。作为开发者...

    这篇文章会记录Synchronized的常用使用场景与Synchronized的底层实现原理。虽然我们平时经常会在多线程中使用Synchronized关键字,但可能对于这个我们很熟悉的关键字的底层到底是怎样实现的没有过多关注。作为开发者,既然使用到了,可以试着去一步一步揭开下它的底层面纱。

    为什么要使用Synchronized?

    首先我们来看下这段代码

     

    
    public class Demo {
        private static int count=0;
        public /*synchronized*/ static void inc(){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
        public static void main(String[] args) throws InterruptedException {
            for(int i=0;i<1000;i++){
                new Thread(()-> Demo.inc()).start();
            }
            Thread.sleep(3000);
            System.out.println("运行结果"+count);
        }
    }
    

    这段代码的运行结果:运行结果970
    在这段代码中,首先没有加synchronized关键字,我们使用了循环的方法用1000个线程去访问count这个变量,运行的结果告诉我们,这个共享变量的状态是线程不安全的(我们期望1000次的访问可以得到1000的结果)。要解决这个问题, Synchronized关键字就可以达到目的。

    synchronized简介

    在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。这块在后续介绍中会慢慢引入。

    synchronized的基本语法

    synchronized 有三种方式来加锁,分别是

    1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    我在网上找了张图,大致也对应上面所说的。

     

    Synchronized的使用场景

    synchronized原理分析

    Java对象头和monitor是实现synchronized的基础!下面就这两个概念来做详细介绍。

    关于monitor,再来看一个小demo

     

    package com.thread;
    
    public class Demo1{
    
        private static int count = 0;
    
        public static void main(String[] args) {
            synchronized (Demo1.class) {
                inc();
            }
    
        }
        private static void inc() {
            count++;
        }
    }
    

    上面的代码demo使用了synchroized关键字,锁住的是类对象。编译之后,切换到Demo1.class的同级目录之后,然后用javap -v Demo1.class查看字节码文件:

     

    public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1
             0: ldc           #2                  // class com/thread/SynchronizedDemo
             2: dup
             3: astore_1
             4: monitorenter       //注意这个
             5: invokestatic  #3                  // Method inc:()V
             8: aload_1
             9: monitorexit    //注意这个
            10: goto          18
            13: astore_2
            14: aload_1
            15: monitorexit   //注意这个
            16: aload_2
            17: athrow
            18: return
    
    

    线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
    线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。

    对象在内存中的布局

    在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

     

    image

    Java对象头

    对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
    Klass Point:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
    Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键.

    加锁时MarkWord可能储存的四种状态

     

    synchronized 锁的升级

    在分析 markword 时,提到了偏向锁、轻量级锁、重量级锁。在分析这几种锁的区别时,我们先来思考一个问题使用锁能够实现数据的安全性,但是会带来性能的下降。不使用锁能够基于线程并行提升程序性能,但是却不能保证线程安全性。这两者之间似乎是没有办法达到既能满足性能也能满足安全性的要求。

    hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁的概念。因此大家会发现在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态根据竞争激烈的程度从低到高不断升级。

    偏向锁的基本原理

    偏向锁的获取
    前面说过,大部分情况下,锁不仅仅不存在多线程竞争,而是总是由同一个线程多次获得,为了让线程获取锁的代价更低就引入了偏向锁的概念。怎么理解偏向锁呢?当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了
    偏向锁的撤销
    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。并且直接把被偏向的锁对象升级到被加了轻量级锁的状态。
    对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

    1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程
    2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

    这是网上一张很经典的偏向锁流程图

     

    偏向锁流程图

    轻量级锁的基本原理

    加锁

    锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:

    1. 线程在自己的栈桢中创建锁记录 LockRecord。
    2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
    3. 将锁记录中的 Owner 指针指向锁对象。
    4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

    自旋锁

    轻量级锁在加锁过程中,用到了自旋锁所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。但是自旋必要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    解锁

    轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

     

    轻量级锁流程图

    重量级锁

    当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

    各种锁的比较

    各种锁的比较

    总结

    JVM在运行过程会根据实际情况对添加了Synchronized关键字的部分进行锁自动升级来实现自我优化。以上就是Synchronized的实现原理和java1.6以后对其所做的优化以及在实际运行中可能遇到的锁升级原理。虽然大家都懂得使用synchronized这个关键字,但我觉得一步一步深入挖掘它的原理实现的过程也是一种乐趣。

    1 ReentrantLock和synchronized区别

      (1) synchronized 是Java的一个内置关键字,而ReentrantLock是Java的一个类。
      (2) synchronized只能是非公平锁。而ReentrantLock可以实现公平锁和非公平锁两种。
      (3) synchronized不能中断一个等待锁的线程,而Lock可以中断一个试图获取锁的线程。
      (4) synchronized不能设置超时,而Lock可以设置超时。
      (5) synchronized会自动释放锁,而ReentrantLock不会自动释放锁,必须手动释放,否则可能会导致死锁。

    2 公平锁和非公平锁的区别

      公平锁的获取锁的过程:

     

    static final class FairSync extends Sync {
        final void lock() { // 1 注意对比公平锁和非公平锁的这个方法
            acquire(1);
        }
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 2 和非公平锁相比,这里多了一个判断:是否有线程在等待
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
    

      非公平锁的获取锁的过程:

     

    static final class NonfairSync extends Sync {
        final void lock() {
          //  1 和公平锁相比,这里会直接先进行一次CAS,如果当前正好没有线程持有锁,
          // 如果成功获取锁就直接返回了,就不用像公平锁那样一定要进行后续判断
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 2 这里没有对阻塞队列进行判断
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    

      从源码上看,可以看到公平锁和非公平锁的两个区别:

    (1) 线程在获取锁调用lock()时,非公平锁首先会进行一次CAS尝试抢锁,如果此时没有线程持有锁或者正好此刻有线程执行完释放了锁(state == 0),那么如果CAS成功则直接占用锁返回。
    (2) 如果非公平锁在上一步获取锁失败了,那么就会进入nonfairTryAcquire(int acquires),在该方法里,如果state的值为0,表示当前没有线程占用锁或者刚好有线程释放了锁,那么就会CAS抢锁,如果抢成功了,就直接返回了,不管是不是有其他线程早就到了在阻塞队列中等待锁了。而公平锁在这里抢到锁了,会判断阻塞队列是不是空的,毕竟要公平就要讲先来后到,如果发现阻塞队列不为空,表示队列中早有其他线程在等待了,那么公平锁情况下线程会乖乖排到阻塞队列的末尾。
      如果非公平锁 (1)(2) 都失败了,那么剩下的过程就和非公平锁一样了。
    (3) 从(1)(2) 可以看出,非公平锁可能导致线程饥饿,但是非公平锁的效率要高。

    展开全文
  • synchronized 底层实现原理

    万次阅读 2019-05-27 10:31:08
    synchronized 底层实现原理? 一、作用 确保线程互斥的访问同步代码 保证共享变量的修改能够及时可见 有效解决重排序问题 二、用法 修饰普通方法 修饰静态方法 修饰代码块 三、原理 同步代码块是通过 ...

    synchronized 底层实现原理?

    一、作用

    • 确保线程互斥的访问同步代码
    • 保证共享变量的修改能够及时可见
    • 有效解决重排序问题

     

    二、用法

    • 修饰普通方法
    • 修饰静态方法
    • 修饰代码块

     

    三、原理

    • 同步代码块是通过 monitorenter 和 monitorexit 指令获取线程的执行权
    • 同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制

    测试代码:

    public class TestSynchronized {
    	
    	public void sync() {
    		synchronized (this) {
    			System.out.println("sync");
    		}
    	}
    	
    	public synchronized void syncdo() {
    		System.out.println("syncdo");
    	}
    	
    	public static synchronized void staticSyncdo() {
    		System.out.println("staticSyncdo");
    	}
    }

    通过JDK 反汇编指令 javap -c -v TestSynchronized

    javap -c -v TestSynchronized
    
      Last modified 2019-5-27; size 719 bytes
      MD5 checksum e5058a43e76fe1cff6748d4eb1565658
      Compiled from "TestSynchronized.java"
    public class constxiong.interview.TestSynchronized
      minor version: 0
      major version: 49
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Class              #2             // constxiong/interview/TestSynchronized
       #2 = Utf8               constxiong/interview/TestSynchronized
       #3 = Class              #4             // java/lang/Object
       #4 = Utf8               java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = Utf8               Code
       #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
       #9 = NameAndType        #5:#6          // "<init>":()V
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lconstxiong/interview/TestSynchronized;
      #14 = Utf8               sync
      #15 = Fieldref           #16.#18        // java/lang/System.out:Ljava/io/PrintStream;
      #16 = Class              #17            // java/lang/System
      #17 = Utf8               java/lang/System
      #18 = NameAndType        #19:#20        // out:Ljava/io/PrintStream;
      #19 = Utf8               out
      #20 = Utf8               Ljava/io/PrintStream;
      #21 = String             #14            // sync
      #22 = Methodref          #23.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
      #23 = Class              #24            // java/io/PrintStream
      #24 = Utf8               java/io/PrintStream
      #25 = NameAndType        #26:#27        // println:(Ljava/lang/String;)V
      #26 = Utf8               println
      #27 = Utf8               (Ljava/lang/String;)V
      #28 = Utf8               syncdo
      #29 = String             #28            // syncdo
      #30 = Utf8               staticSyncdo
      #31 = String             #30            // staticSyncdo
      #32 = Utf8               SourceFile
      #33 = Utf8               TestSynchronized.java
    {
      public constxiong.interview.TestSynchronized();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #8                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lconstxiong/interview/TestSynchronized;
    
      public void sync();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter
             4: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
             7: ldc           #21                 // String sync
             9: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            12: aload_1
            13: monitorexit
            14: goto          20
            17: aload_1
            18: monitorexit
            19: athrow
            20: return
          Exception table:
             from    to  target type
                 4    14    17   any
                17    19    17   any
          LineNumberTable:
            line 6: 0
            line 7: 4
            line 6: 12
            line 9: 20
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      21     0  this   Lconstxiong/interview/TestSynchronized;
    
      public synchronized void syncdo();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #29                 // String syncdo
             5: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 12: 0
            line 13: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  this   Lconstxiong/interview/TestSynchronized;
    
      public static synchronized void staticSyncdo();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=0, args_size=0
             0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #31                 // String staticSyncdo
             5: invokevirtual #22                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 16: 0
            line 17: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
    }
    SourceFile: "TestSynchronized.java"

    深入分析可参考:

    https://www.cnblogs.com/paddix/p/5367116.html 

    javap指令

     


    【Java面试题与答案】整理推荐

     

    展开全文
  • volatile底层实现原理

    千次阅读 2019-09-02 19:52:55
    在介绍volatile语义实现原理之前,我们先来看两个与CPU相关的专业术语: 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。 缓存行(cache line):CPU高速缓存中可以分配的最小...
  • 主要介绍了JAVA序列化和反序列化的底层实现原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • synchronized底层实现原理及锁优化

    万次阅读 多人点赞 2019-03-11 19:02:26
    可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现) 有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”) 2、synchronized的...
  • SpringBoot底层实现原理

    千次阅读 2019-04-10 16:42:49
    <groupId>org.springframework.boot <artifactId>spring-boot-starter-parent <version>2.0.0.RELEASE <!-- SpringBoot 整合...-- 为什么我们映入spring-boot-starter-web 能够帮我整合Spring环境 原理
  • IOS KVO底层实现原理 (一)

    千次阅读 2019-06-27 11:21:47
    IOS KVO底层实现原理 (一)一,KVO简述二,KVO探索三,KVO底层原理四,KVO底层实现代码 一,KVO简述 KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。 带着问题探索: ...
  • MySQL事务底层实现原理

    千次阅读 2019-10-23 16:37:36
    事务特性 事务特性分为: ...也是在事务并发时实现一致性的一个前提,可以设置4种隔离级别。级别越高一致性越强,但并发性越低; 1.读未提交 会读到其他事务未提交的数据,产生脏读 2.读已提交 解...
  • CAS底层实现原理

    千次阅读 2020-07-04 21:06:25
    CAS底层实现原理 1.首先什么是CAS? Compare And Swap =========>compareAndSwapInt,它是一条CPU并发原语。它的功能时判断内存的某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。 CAS并发...
  • HashMap底层实现原理

    万次阅读 多人点赞 2019-04-24 13:57:56
    HashMap底层实现原理 HashMap是Java语言中用的最频繁的一种数据结构。 1.hashmap的数据结构 要了解hashmap首先要弄清楚他的结构。在java编程语言中最基本的数据结构有两种,数组和链表。 数组:查询速度快,可以根据...
  • python list底层实现原理

    千次阅读 2020-06-03 22:23:50
    list底层实现原理0 前言1 List对象的C结构2 List的初始化3 Append4 Insert5 Pop6 Romve总结 0 前言 在Python中list特别有用。在使用的过程中,python中的list呈现给我们的是一个长度可变对的数组。但是list底层的...
  • Golang map底层实现原理解析

    千次阅读 2020-04-07 18:44:04
    在开发过程中,map是必不可少的数据结构,在Golang中,使用map或多或少会遇到与其他语言不一样的体验,...本文希望通过研究map的底层实现,以解答这些疑惑。基于Golang 1.8.3 1. 数据结构及内存管理 hashmap的定义...
  • 本片文章是学习Java并发底层实现原理的一篇知识心得,对大家学习这个方便的知识很有帮助,一起参考下。
  • redis中zset底层实现原理

    千次阅读 多人点赞 2020-07-14 12:00:27
    五.Redis中的skiplist实现 六.Redis为什么用skiplist而不用平衡树? 一.Zset编码的选择 1.有序集合对象的编码可以是ziplist或者skiplist。同时满足以下条件时使用ziplist编码: 元素数量小于128个 所有membe
  • Vue底层实现原理

    千次阅读 多人点赞 2018-09-30 21:31:40
    为了便于说明原理实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢迎大家指正。不过这些并不会影响大家的阅读和理解,相信看...
  • synchronized的底层实现原理及各种优化

    千次阅读 多人点赞 2019-05-12 15:16:11
    synchronized实现原理及优化 几个概念: 并发 并行 同步 线程安全 synchronized概述 synchronized,单词译为同步,是Java的内建锁,用来确保线程安全,是解决并发问题的一种重要手段。synchronized可以保证在...
  • HashMap底层实现原理及面试问题

    万次阅读 多人点赞 2018-08-29 11:00:56
    ①HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取...
  • MyBatis的底层实现原理

    万次阅读 多人点赞 2018-09-11 10:56:32
    动态代理的功能:通过拦截器方法回调,对...注:本文默认认为,读者对动态代理的原理是理解的,如果不明白target的含义,难以看懂本篇文章,建议先理解动态代理。 一、自定义JDK动态代理之投鞭断流实现自动映射器M...
  • vue-router底层实现原理

    千次阅读 2019-05-17 10:09:44
    包括 router-link、router-view 两个组件,其中 router-link 用于实现跳转,router-view 用于展示视图 vue-router的两种模式 hash模式 hash模式背后的原理是onhashchange事件 因为hash发生变化的url都会被...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 403,158
精华内容 161,263
关键字:

底层实现原理