精华内容
下载资源
问答
  • 2.3 Java中创建数组的方式 案例 【补充知识点 引用数据类型 难点】 引用在生活中很常见,比如 文献中引用,文言文注解引用,广告角标上标引用,这些目的,引入其他的内容,其他的文献,其他的操作... 引用存在的...

    1ae5c3c3fabfce31dcafaef2bd30cfdb.png

    方法总结和数组初识

    1. 方法总结

    1.1 方法的三要素

    返回值类型   
        当前方法运行之后对外的数据产出
    方法名
        明确告知用户这里运行的方法是哪一个,执行的效果会怎么样。
        小驼峰命名法,见名知意,动宾结构
    形式参数列表
        巧妇难为无米之炊,方法运行所需的外来数据,方法运行的必要条件!!!

    1.1.1 方法名

    规矩
        1. 只能用英文字母(A ~ Z a ~ z), 数字(0 ~ 9),还有下划线 _
        2. 见名知意,动宾结构
            好的代码从方法名开始
        3. 小驼峰命名法
            行为规范
            setXXX
            getXXX
            remove
            add
            delete
            update
            attribute

    1.1.2 返回值类型

    返回值
        1. 没有什么必要和不必要,需求分析过程!!!
        2. 返回值的数据类型
        3. 返回值的数据含义
        4. 返回值每一次有且只能返回一个数据

    1.1.3 形式参数列表

    巧妇难为无米之炊!
        需求分析过程!!!
        
        用户注册
            用户名,手机号,密码....
        
        用户购物下单
            用户地址,money
            
        做菜
            酱牛肉
            牛肉,葱,姜,蒜,料酒,八角,花椒,麻椒,辣椒,小茴香,香叶,桂皮,
            山奈,老抽,冰糖,耗油,黄豆酱
        
        形式参数实际上是在对于方法分析过程中,了解方法运行需要必要参数,并且在参数使用必要性上做论证。

    1.2 方法的完成过程

    1

    2. 数组【重点】

    2.1 生活中的数组

    超市
        商品货物的存放,是不是分门别类???
        分门别类有什么好处??? 
            便于管理,便于查找
        理念 【归纳总结】
    ​
    图书馆:
        1. 社科类,文学类,管理类,小说类,历史类,语言类
        2. 相同的书籍会存放于一个书架上 【同一个位置】
        3. 每一本书都要有一个唯一的编号 【同样的称呼】 W-101
        4. 同一本有10本,都会有一个唯一的索引 
            W-101-01 ~ W-101-10 【唯一索引】
        
    通过图书馆我们可以概括
        1. 存储位置一致,并且连续
        2. 有统一的称呼,方便管理
        3. 存在唯一索引,具有一定的唯一性

    2.2 开发中的实际情况

    开发中一定存在对于大量相同数据处理的过程!!!
    ​
    如果按照单一变量的定义方式,会导致
        1. 代码冗余!!!
        2. 代码维护性极差!!!
        3. 代码可操作性极差!!!
        4. 代码阅读性极差!!!
    ​
        这里可以模仿生活中的案例,图书馆,超市,把这些同一个数据类型的数据,存放在一起,方便管理和使用
        引入数组使用的场景和概念!!!

    2.3 Java中创建数组的方式

    案例
        

    【补充知识点 引用数据类型 难点】

        引用在生活中很常见,比如 文献中引用,文言文注解引用,广告角标上标引用,这些目的,引入其他的内容,其他的文献,其他的操作... 
        引用存在的一定的【指向性!!!】
        
        取快递
        快递小哥如何知道你的地址在哪里???
        根据快递单上的地址,联系方式和姓名来找到你的位置。
        快递单这里也存在一定的【指向性!!!】
        
        快递单中存有一个非常重要的数据【地址】!!!
    ​
        开发中的【引用数据类型】,实际上是一个指向其他内存空间的一个数据类型。引用数据类型的变量中存储的内容是其他内存空间的【首地址】。
        当CPU访问到【引用数据类型】变量时,会得到其存储的地址,然后直接跳转到对应的内存空间中,执行代码,获取数据,操作内容...

    2.4 定义数组和使用

    定义数组:
        

    2.5 数组内存分析图【难点】

    f2561426668bde4545d1df66687f6f6a.png

    2.6 数组和循环不得不说的秘密

    数组的下标是不是一个等差数列?
        0 ~ 数组容量 - 1
    ​
    这里和循环存在一定的关系!!!
    数组关系极为密切是for循环!!!
    class Demo3 {
        public static void main(String[] args) {
            /* 定义一个int类型数组 容量为10 */
            int[] arr = new int[10];
            
            /*
            数组名.length 
                获取当前数组的【容量 Capacity】,获取数组的【属性】
            */
            for (int i = 0; i < arr.length; i++) {
                // 给数组中每一个元素赋值操作 
                arr[i] = i + 1;
            }
            
            // 利用循环展示数据
            for (int i = 0; i < arr.length; i++) {
                System.out.println(arr[i]);
            }
        }
    }

    3. 作业

    练习题
        a. 在一个int类型数组中,存储的内容是1 ~ 10
        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        使用代码找出,元素 == 5 所在下标位置
        
        b.  在一个int类型数组中,存储的内容是
        int[] arr = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10};
        使用代码找出下标为5的元素
        
        c.  在一个int类型数组中,存储的内容是
        int[] arr = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10};
        使用代码找出数组中最大元素所在下标位置
        
        d.  在一个int类型数组中,存储的内容是
        int[] arr = {1, 3, 5, 7, 9, 2, 4, 6, 8, 10};
        使用代码找出数组中最小元素所在下标位置
        
    展开全文
  • java -version java version "13.0.2" 2020-01-14 Java(TM) SE Runtime Environment (build 13.0.2+8) Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)Array数组能够做到快速随机访问元素...

    java -version java version "13.0.2" 2020-01-14 Java(TM) SE Runtime Environment (build 13.0.2+8) Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)

    Array

    数组能够做到快速随机访问元素,这是因为当我们创建一个数组时:

    var array = new Person[3];array[0] = new Person();

    java 首先把这个数组的引用存入栈中,然后到堆空间开辟一片连续的地址空间,并将数组引用指向堆地址空间。

    当我们访问指定的数组元素时,则只需要根据 array 的引用地址 + 下标地址, 就能快速定位元素了。

    9e652b5e083bd4b8de8601128e911008.png

    需要注意的是,数组需要连续空间的特性,让数组扩容难以实现,所以各种语言实现的数组,数组的大小都是固定的

    数组有 length 属性,这个属性记录的是数组的大小,而不是元素的个数。

    List

    java 中 list 常用的有 ArrayList 和 LinkedList。

    ArrayList

    底层基于数组 Object[] 实现,继承数组的优势,可快速随机访问元素,对于增删操作,则最坏需要 O(n) 。

    我们知道 array 不能扩容,但是 ArrayList 明显可以,所以我们去看看,ArrayList ,是怎样扩容的。

    public class ArrayList extends AbstractList        implements List, RandomAccess, Cloneable, java.io.Serializable {    private static final int DEFAULT_CAPACITY = 10;    transient Object[] elementData; // non-private to simplify nested class access    private int size;    protected transient int modCount = 0;}
    • DEFAULT_CAPACITYP : 默认的容量大小
    • elementData : 用来记录元素的数组,当我们初始化时,如果未指定容量,则用默认值初始化该数组,但是注意,此时 size 的值是零
    • size : 记录列表中的元素个数,与数组的容量大小无关
    • modCount : 该属性继承自 AbstractList 。所有会修改 list 大小的操作,该值都会增加。当我们通过 iterator 遍历时, 如果该值发生了改变(被另一个线程增加/删除了元素),就会抛出 ConcurrentModificationException

    ArrayList 实现了 List / RandomAccess / Cloneable / Serializable 四个标记接口,标记接口 RandomAccess 在排序时会用到,用来选择迭代方式(for or iterator) 。

    通过源码能看到, 当我们执行 add 操作时:

    public boolean add(E e) {    modCount++;    add(e, elementData, size);    return true;}​private void add(E e, Object[] elementData, int s) {    if (s == elementData.length)        elementData = grow();    elementData[s] = e;    size = s + 1;}

    ArrayList 首先会比较数组长度和元素大小,如果相等,则执行 grow() 方法,进行扩容,扩容完成后,再把元素加到 elementData 数组元素中。注意,此时的 elementData ,事实上,已经不是它了,它已经变成可扩容后的它。

    现在看看 grow 方法的实现:

    private Object[] grow() {    return grow(size + 1);}private Object[] grow(int minCapacity) {    int oldCapacity = elementData.length;    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {        int newCapacity = ArraysSupport.newLength(oldCapacity,                    minCapacity - oldCapacity, /* minimum growth */                    oldCapacity >> 1           /* preferred growth */);        return elementData = Arrays.copyOf(elementData, newCapacity);    } else {        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];    }}

    实现的也很清晰,把原始元素大小,需要的最小扩容量(左移一位),和期待的扩容量,做比较,最终得到一个新的容量大小。

    之后通过 Arrays.copy 方法,将老数组的元素拷贝到新元素。

    具体比较方式不贴了,简单描述下:

    1. 取最小扩容量和期待扩容量中的最大值,加上源数组大小,作为新的容量,我们设为 newLength。
    2. 如果这个值在允许的最大数组长度(Integer.MAX_VALUE - 8) 内,返回
    3. 否则,直接比较最大数组长度和 源数组长度 + 最小扩容量,满足则返回
    4. 否则,抛出 OutOfMemoryError

    正常情况下,一次扩容的容量,会增加源数组大小的二分之一(即上面左移一位的操作)

    LinkedList

    底层基于双向链表 实现,对于增删,效率极高。

    public class LinkedList    extends AbstractSequentialList    implements List, Deque, Cloneable, java.io.Serializable {​    transient int size = 0;    transient Node first;    transient Node last;    protected transient int modCount = 0;}​private static class Node {    E item;    Node next;    Node prev;    Node(Node prev, E element, Node next) {        this.item = element;        this.next = next;        this.prev = prev;    }}

    可以看到, LinkedList 实现了 List,Deque,Cloneable 和 Serializable 接口。Deque 即我们所说的双端队列。在 util 包下,还有 ArrayDeque 的实现。

    Node 的结构中,主要有 prev , next 和 item ,分别指向上一个节点,下一个节点,item 保存当前节点数据。

    因为每个节点都需要保存上下两个节点的信息,所以必然比 ArrayList 要消耗更多的空间

    Map

    这里以 HashMap 为主。

    public class HashMap extends AbstractMap    implements Map, Cloneable, Serializable {        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16    static final int MAXIMUM_CAPACITY = 1 << 30;    static final float DEFAULT_LOAD_FACTOR = 0.75f;    int threshold;    static final int TREEIFY_THRESHOLD = 8;    static final int UNTREEIFY_THRESHOLD = 6;    transient Node[] table;}

    HashMap 几个重要的参数:

    • TREEIFY_THRESHOLD : 树化阈值, 即当 map 中的链表长度大于该值时, 会将链表转为红黑树[1]
    • UNTREEIFY_THRESHOLD : 当红黑树 size 小于该值时,重新转为链表
    • DEFAULT_INITIAL_CAPACITY : hashmap 初始化的容量大小
    • table : 即 hash 表,hashmap 做 hash 时,最终散列到这张表上,长度适中为 2n
    • DEFAULT_LOAD_FACTOR :常说的装载因子 。hashmap 判断是否需要 resize 时,会根据 threshold 判断,而 threshold 则是根据 装载因子 * table.length 算出来的

    我们 new HashMap(9) 时, HashMap 就会去初始化 loadFactor 和 threshold [2]

    HashMap 大概就长这个样子:

    d4ddf609c0602ee49525ecf7a2a74cc1.png

    HashMap 的扩容

    当我们执行 put 操作时, HashMap 会先将元素添加到 Map 中,然后查看 size(即 Map 中的元素个数) 是否大于 threshold (阈值, 即上面根据 loadFactory * capacity 即 table.length 算出来的) ,如果大于,就进行扩容,即 resize() 方法。

    if (++size > threshold)    resize();

    resize [3] 时, 首先判断 table 是否初始化,没有则先初始化 table ,然后根据 old table 以及初始化时的 loadFactor ,threshold 参数,计算新的 threshold ,以及需要扩容的大小,一般为 old table length 的 两倍

    介绍数组时,我们已经说过,数组无法扩容,而 HashMap 使用数组来维护 hash 表的,所以需要新建一个数组,这个数组的长度就是之前数组的两倍

    之后会进行元素移动,为什么说创建 HashMap 时,要先规划好大小,因为扩容这个操作是很消耗性能的,在一个 double for 循环中(for + while) 。

    元素移动完毕后,扩容结束。

    cc8a3fa4c7f8f956cdd3424efff96994.png

    Queue

    队列,FIFO, first in first out, 先进先出。

    java 中 Queue 实现 Collection 接口。具体的实现有

    • LinkedBlockingQueue :基于 LinkedList 的阻塞队列
    • ArrayBlockingQueue :基于 Array 的阻塞队列
    • ArrayDeque : 基于 Array 的双端队列
    • LinkedList : 是的, LinkedList 实现了 Deque 接口,也是一个双端队列,Deque deque = new LinkedList();

    队列常用方法:

    • add :添加节点,加入队列尾部, tail
    • remove :删除头节点, head
    • peek :获取 head 节点,但是不删除 , 偷取元素
    • poll :获取 head 节点,并删除
    • offer :立即插入节点到 tail , 对于定长队列,有空间且插入成功则返回 true , 若插入失败,则返回 false 。add 会抛出异常。
    • push : Deque 接口所有,插入元素到 head 。
    • take : BlockingQueue 接口所有 ,获取并删除 head 节点。 如果当前 queue 没有元素,则会阻塞 。

    Stack

    栈, FILO , first in last out ,先进后出。

    • peek : 获取 head ,但不会删除,偷取元素
    • pop : 获取 head , 同时删除, 弹出元素
    • push : 压入栈

    注释

    [1]

    看 HashMap 源码的时候,发现在 resize 时,对 for i 循环的时候,都是用 for(;;++1) 的写法:

    for (int j = 0; j < oldCap; ++j) { ... }

    平时不都是 fro(;;i++) 的吗?然后想了下,i++ 会产生一个中间变量,用于暂时寄存自增前的变量,这一步会消耗一定的性能。

    当然,平时我们写代码,最后编译的时候,编译器会把这部分优化成 ++i

    18abc3d3aaa1ad638e18fb86c4a98a1e.png

    [2]

    HashMap 初始化时(带 capacity 参数),我们跟踪下代码,就会发现,最终调用:

    static final int tableSizeFor(int cap) {    int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}// Integer.numberOfLeadingZerospublic static int numberOfLeadingZeros(int i) {    if (i <= 0)        return i == 0 ? 32 : 0;    int n = 31;    if (i >= 1 << 16) { n -= 16; i >>>= 16; }    if (i >= 1 <<  8) { n -=  8; i >>>=  8; }    if (i >= 1 <<  4) { n -=  4; i >>>=  4; }    if (i >= 1 <<  2) { n -=  2; i >>>=  2; }    return n - (i >>> 1);}

    这个方法在干嘛呢?

    我们知道, java 是通过 补码 计数的。

    原码 : 原码很好理解,就是一个数的二进制表示, 对于一个四位数,则 2 的原码为 0010, -2 为 1010

    反码 : 对于正数而言,其原码 = 反码, 对于负数而言,则只需要将其原码除符号位以外的数取反即可。同样对于四位数,2 为 0010 , -2 为 1101

    补码 : 对于正数而言, 其原码 = 补码,对于负数而言,则是将其反码 + 1 。 还是对于四位数, 2 为 0010 , -2 则为 1110

    溢出 : java 中,我们偶尔会碰到溢出 。我们知道, java 的基本数据类型中, 如 byte ,长度是一个字节,也就是 8 位,范围是 -128 ~ 127 。1111 1111 = 255 啊! 为什么最大是 127 呢? 因为他们都是有符号数据类型(最高位 0 表示正数, 1 表示负数),最高位是符号位, 所以 1111 1111 事实上应该是 -1

    当我们计算两个 byte 类型的 如 127 + 127 时,输出了 -2 。因为 0111 1111 + 0111 1111 = 1111 1110 , 对于 byte 类型,最高位符号位变成了 1 。这个二进制补码还原成十进制,就是 -2 。

    关于补码的更多介绍,可参考 这里

    现在回到代码。

    numberOfLeadingZeros 这个方法,java init 是一个 32 位数,我们假设 i > 1<< 16 , 这表明 i 的高 16 位中,至少有一个 1 , 所以将 n (从左往右,连续的 0 的个数) - 16 ,即低 16 位可以不考虑了。

    然后将 i 无符号右移(感觉这里 >> 和 >>> 都一样,因为负数已经在第一步就 return 掉了) 16 位 , 继续判断,直到最后。假设 i = 0011 , 则最后变成 : 31 - (0011 >>> 1) = 31 - 0001 = 30 。

    取得这个数后,对 -1 做无符号右移, -1 是 ffff ffff , 右移 n 位后, 再加上 1,可保证得到一个 cap 向上的 2 的次幂。

    至于为什么 HashMap 的 capacity 一定要是 2 次幂, 这就要说到它的 hash 算法了:

    static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

    可以看到, 这里将 key 的 hashcode 低 32 位和高 32 位做亦或运算 。

    if ((p = tab[i = (n - 1) & hash]) == null) {    tab[i] = newNode(hash, key, value, null);}

    放 node 时, index 是根据 (n-1) & hash 来运算的。

    因为 n 是一个 2 次幂,所以 n-1 是一个全 f 的数, 对 hash 做与运算,即拿到 hash 的低位。

    如, HashMap capacity = 8 ,即 n = 8 ,则 n-1 = 0000 0111 假设 key 的 hash 为 0111 0010 , 则 (n-1) & hash = 0000 0010 = 2,会被放入 tab[2] 中。

    这样有什么问题呢? 即每次都只有 key 的低位参与了运算,会导致较大概率的 hash 碰撞。

    所以 HashMap 的 hash 算法,将其 hash 的低 16 位和高 16 位做了亦或,保证数据的更大的随机性,从而减小 hash 碰撞的可能性。

    同时,全程的位操作,也给计算带来了性能上的优势。

    [3]

    参见 树 -- 算法浅析

    展开全文
  • Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的。为此,Java为我们提供了另外一个HashTable类,它对于...

    哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别。Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的。为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁。这种方法虽然简单,但导致了一个问题,那就是在同一时间内只能由一个线程去操作哈希表。即使这些线程都只是进行读操作也必须要排队,这在竞争激烈的多线程环境中极为影响性能。本篇介绍的ConcurrentHashMap就是为了解决这个问题的,它的内部使用分段锁将锁进行细粒度化,从而使得多个线程能够同时操作哈希表,这样极大的提高了性能。下图是其内部结构的示意图。

    f6475dc3d3496c0b600727c4c1a4fa89.png

    1. ConcurrentHashMap有哪些成员变量?

    //默认初始化容量
    

    在此,只有个别变量是我们现在需要了解的,例如Segment数组代表分段锁集合,并发级别则代表分段锁的数量(也意味有多少线程可以同时操作),初始化容量代表整个容器的容量,加载因子代表容器元素可以达到多满的一种程度。

    2. 分段锁的内部结构是怎样的?

    //分段锁
    

    Segment是ConcurrentHashMap的静态内部类,可以看到它继承自ReentrantLock,因此它在本质上是一个锁。它在内部持有一个HashEntry数组(哈希表),并且保证所有对该数组的增删改查方法都是线程安全的,具体是怎样实现的后面会讲到。所有对ConcurrentHashMap的增删改查操作都可以委托Segment来进行,因此ConcurrentHashMap能够保证在多线程环境下是安全的。又因为不同的Segment是不同的锁,所以多线程可以同时操作不同的Segment,也就意味着多线程可以同时操作ConcurrentHashMap,这样就能避免HashTable的缺陷,从而极大的提高性能。

    3. ConcurrentHashMap初始化时做了些什么?

    //核心构造器
    


    ConcurrentHashMap有多个构造器,但是上面贴出的是它的核心构造器,其他构造器都通过调用它来完成初始化。核心构造器需要传入三个参数,分别是初始容量,加载因子和并发级别。在前面介绍成员变量时我们可以知道默认的初始容量为16,加载因子为0.75f,并发级别为16。现在我们看到核心构造器的代码,首先是通过传入的concurrencyLevel来计算出ssize,ssize是Segment数组的长度,它必须保证是2的幂,这样就可以通过hash&ssize-1来计算分段锁在数组中的下标。由于传入的concurrencyLevel不能保证是2的幂,所以不能直接用它来当作Segment数组的长度,因此我们要找到一个最接近concurrencyLevel的2的幂,用它来作为数组的长度。假如现在传入的concurrencyLevel=15,通过上面代码可以计算出ssize=16,sshift=4。接下来立马可以算出segmentShift=16,segmentMask=15。注意这里的segmentShift是分段锁的移位值,segmentMask是分段锁的掩码值,这两个值是用来计算分段锁在数组中的下标,在下面我们会讲到。在算出分段锁的个数ssize之后,就可以根据传入的总容量来计算每个分段锁的容量,它的值c = initialCapacity / ssize。分段锁的容量也就是HashEntry数组的长度,同样也必须保证是2的幂,而上面算出的c的值不能保证这一点,所以不能直接用c作为HashEntry数组的长度,需要另外找到一个最接近c的2的幂,将这个值赋给cap,然后用cap来作为HashEntry数组的长度。现在我们有了ssize和cap,就可以新建分段锁数组Segment[]和元素数组HashEntry[]了。注意,与JDK1.6不同是的,在JDK1.7中只新建了Segment数组,并没有对它初始化,初始化Segment的操作留到了插入操作时进行。

    4. 通过怎样的方式来定位锁和定位元素?

    //根据哈希码获取分段锁
    

    在JDK1.7中是通过UnSafe来获取数组元素的,因此这里比JDK1.6多了些计算数组元素偏移量的代码,这些代码我们暂时不关注,现在我们只需知道下面这两点:
    a. 通过哈希码计算分段锁在数组中的下标:(h >>> segmentShift) & segmentMask。
    b. 通过哈希码计算元素在数组中的下标:(tab.length - 1) & h。
    现在我们假设传给构造器的两个参数为initialCapacity=128, concurrencyLevel=16。根据计算可以得到ssize=16, sshift=4,segmentShift=28,segmentMask=15。同样,算得每个分段锁内的HashEntry数组的长度为8,所以tab.length-1=7。根据这些值,我们通过下图来解释如何根据同一个哈希码来定位分段锁和元素。

    0ef21a4994b1212d78cd7ad76a6e9b7e.png

    可以看到分段锁和元素的定位都是通过元素的哈希码来决定的。定位分段锁是取哈希码的高位值(从32位处取起),定位元素是取的哈希码的低位值。现在有个问题,它们一个从32位的左端取起,一个从32位的右端取起,那么会在某个时刻产生冲突吗?我们在成员变量里可以找到MAXIMUM_CAPACITY = 1 << 30,MAX_SEGMENTS = 1 << 16,这说明定位分段锁和定位元素使用的总的位数不超过30,并且定位分段锁使用的位数不超过16,所以至少还隔着2位的空余,因此是不会产生冲突的。

    5. 查找元素具体是怎样实现的?

    //根据key获取value
    

    在JDK1.6中分段锁的get方法是通过下标来访问数组元素的,而在JDK1.7中是通过UnSafe的getObjectVolatile方法来读取数组中的元素。为啥要这样做?我们知道虽然Segment对象持有的HashEntry数组引用是volatile类型的,但是数组内的元素引用不是volatile类型的,因此多线程对数组元素的修改是不安全的,可能会在数组中读取到尚未构造完成的对象。在JDK1.6中是通过第二次加锁读取来保证安全的,而JDK1.7中通过UnSafe的getObjectVolatile方法来读取同样也是为了保证这一点。使用getObjectVolatile方法读取数组元素需要先获得元素在数组中的偏移量,在这里根据哈希码计算得到分段锁在数组中的偏移量为u,然后通过偏移量u来尝试读取分段锁。由于分段锁数组在构造时没进行初始化,因此可能读出来一个空值,所以需要先进行判断。在确定分段锁和它内部的哈希表都不为空之后,再通过哈希码读取HashEntry数组的元素,根据上面的结构图可以看到,这时获得的是链表的头结点。之后再从头到尾的对链表进行遍历查找,如果找到对应的值就将其返回,否则就返回null。以上就是整个查找元素的过程。

    6. 插入元素具体是怎样实现的?

    //向集合添加键值对(若存在则替换)
    

    ConcurrentHashMap中有两个添加键值对的方法,通过put方法添加时如果存在则会进行覆盖,通过putIfAbsent方法添加时如果存在则不进行覆盖,这两个方法都是调用分段锁的put方法来完成操作,只是传入的最后一个参数不同而已。在上面代码中我们可以看到首先是根据key的哈希码来计算出分段锁在数组中的下标,然后根据下标使用UnSafe类getObject方法来读取分段锁。由于在构造ConcurrentHashMap时没有对Segment数组中的元素初始化,所以可能读到一个空值,这时会先通过ensureSegment方法新建一个分段锁。获取到分段锁之后再调用它的put方法完成添加操作,下面我们来看看具体是怎样操作的。

    //添加键值对
    

    为保证线程安全,分段锁中的put操作是需要进行加锁的,所以线程一开始就会去获取锁,如果获取成功就继续执行,若获取失败则调用scanAndLockForPut方法进行自旋,在自旋过程中会先去扫描哈希表去查找指定的key,如果key不存在就会新建一个HashEntry返回,这样在获取到锁之后就不必再去新建了,为的是在等待锁的过程中顺便做些事情,不至于白白浪费时间,可见作者的良苦用心。具体自旋方法我们后面再细讲,现在先把关注点拉回来,线程在成功获取到锁之后会根据计算到的下标,获取指定下标的元素。此时获取到的是链表的头结点,如果头结点不为空就对链表进行遍历查找,找到之后再根据onlyIfAbsent参数的值决定是否进行替换。如果遍历没找到就会新建一个HashEntry指向头结点,此时如果自旋时创建了HashEntry,则直接将它的next指向当前头结点,如果自旋时没有创建就在这里新建一个HashEntry并指向头结点。在向链表添加元素之后检查元素总数是否超过阀值,如果超过就调用rehash进行扩容,没超过的话就直接将数组对应下标的元素引用指向新添加的node。setEntryAt方法内部是通过调用UnSafe的putOrderedObject方法来更改数组元素引用的,这样就保证了其他线程在读取时可以读到最新的值。

    7. 删除元素具体是怎样实现的?

    //删除指定元素(找到对应元素后直接删除)
    

    ConcurrentHashMap提供了两种删除操作,一种是找到后直接删除,一种是找到后先比较再删除。这两种删除方法都是先根据key的哈希码找到对应的分段锁后,再通过调用分段锁的remove方法完成删除操作。下面我们来看看分段锁的remove方法。

    //删除指定元素
    

    在删除分段锁中的元素时需要先获取锁,如果获取失败就调用scanAndLock方法进行自旋,如果获取成功就执行下一步,首先计算数组下标然后通过下标获取HashEntry数组的元素,这里获得了链表的头结点,接下来就是对链表进行遍历查找,在此之前先用next指针记录当前结点的后继结点,然后对比key和hash看看是否是要找的结点,如果是的话就执行下一个if判断。满足value为空或者value的值等于结点当前值这两个条件就会进入到if语句中进行删除操作,否则直接跳过。在if语句中执行删除操作时会有两种情况,如果当前结点为头结点则直接将next结点设置为头结点,如果当前结点不是头结点则将pred结点的后继设置为next结点。这里的pred结点表示当前结点的前继结点,每次在要检查下一个结点之前就将pred指向当前结点,这就保证了pred结点总是当前结点的前继结点。注意,与JDK1.6不同,在JDK1.7中HashEntry对象的next变量不是final的,因此这里可以通过直接修改next引用的值来删除元素,由于next变量是volatile类型的,所以读线程可以马上读到最新的值。

    8. 替换元素具体是怎样实现的?

    //替换指定元素(CAS操作)
    

    ConcurrentHashMap同样提供了两种替换操作,一种是找到后直接替换,另一种是找到后先比较再替换(CAS操作)。这两种操作的实现大致是相同的,只是CAS操作在替换前多了一层比较操作,因此我们只需简单了解其中一种操作即可。这里拿CAS操作进行分析,还是老套路,首先根据key的哈希码找到对应的分段锁,然后调用它的replace方法。进入分段锁中的replace方法后需要先去获取锁,如果获取失败则进行自旋,如果获取成功则进行下一步。首先根据hash码获取链表头结点,然后根据key和hash进行遍历查找,找到了对应的元素之后,比较给定的oldValue是否是当前值,如果不是则放弃修改,如果是则用新值进行替换。由于HashEntry对象的value域是volatile类型的,因此可以直接替换。

    9. 自旋时具体做了些什么?

    //自旋等待获取锁(put操作)
    

    在前面我们讲到过,分段锁中的put,remove,replace这些操作都会要求先去获取锁,只有成功获得锁之后才能进行下一步操作,如果获取失败就会进行自旋。自旋操作也是在JDK1.7中添加的,为了避免线程频繁的挂起和唤醒,以此提高并发操作时的性能。在put方法中调用的是scanAndLockForPut,在remove和replace方法中调用的是scanAndLock。这两种自旋方法大致是相同的,这里我们只分析scanAndLockForPut方法。首先还是先根据hash码获得链表头结点,之后线程会进入while循环中执行,退出该循环的唯一方式是成功获取锁,而在这期间线程不会被挂起。刚进入循环时retries的值为-1,这时线程不会马上再去尝试获取锁,而是先去寻找到key对应的结点(没找到会新建一个),然后再将retries设为0,接下来就会一次次的尝试获取锁,对应retries的值也会每次加1,直到超过最大尝试次数如果还没获取到锁,就会调用lock方法进行阻塞获取。在尝试获取锁的期间,还会每隔一次(retries为偶数)去检查头结点是否被改变,如果被改变则将retries重置回-1,然后再重走一遍刚才的流程。这就是线程自旋时所做的操作,需注意的是如果在自旋时检测到头结点已被改变,则会延长线程的自旋时间。

    10. 哈希表扩容时都做了哪些操作?

    //再哈希
    

    rehash方法在put方法中被调用,我们知道在put方法时会新建元素并添加到哈希数组中,随着元素的增多发生哈希冲突的可能性越大,哈希表的性能也会随之下降。因此每次put操作时都会检查元素总数是否超过阀值,如果超过则调用rehash方法进行扩容。因为数组长度一旦确定则不能再被改变,因此需要新建一个数组来替换原先的数组。从代码中可以知道新创建的数组长度为原数组的2倍(oldCapacity << 1)。创建好新数组后需要将旧数组中的所有元素移到新数组中,因此需要计算每个元素在新数组中的下标。计算新下标的过程如下图所示。

    eec0bdd76b1d878a64e492e3ed4a33a3.png

    我们知道下标直接取的是哈希码的后几位,由于新数组的容量是直接用旧数组容量右移1位得来的,因此掩码位数向右增加1位,取到的哈希码位数也向右增加1位。如上图,若旧的掩码值为111,则元素下标为101,扩容后新的掩码值为1111,则计算出元素的新下标为0101。由于同一条链表上的元素下标是相同的,现在假设链表所有元素的下标为101,在扩容后该链表元素的新下标只有0101或1101这两种情况,因此数组扩容会打乱原先的链表并将链表元素分成两批。在计算出新下标后需要将元素移动到新数组中,在HashMap中通过直接修改next引用导致了多线程的死锁。虽然在ConcurrentHashMap中通过加锁避免了这种情况,但是我们知道next域是volatile类型的,它的改动能立马被读线程读取到,因此为保证线程安全采用复制元素来迁移数组。但是对链表中每个元素都进行复制有点影响性能,作者发现链表尾部有许多元素的next是不变的,它们在新数组中的下标是相同的,因此可以考虑整体移动这部分元素。具统计实际操作中只有1/6的元素是必须复制的,所以整体移动链表尾部元素(lastRun后面的元素)是可以提升一定性能的。

    注:本篇文章基于JDK1.7版本。

    网页链接mp.weixin.qq.com

    扫描下方二维码获取更多学习资料

    11984a2e07119b2580d0b1649ba13b00.png
    展开全文
  • java新建数组: String[] s;//定义的时候不需要设置大小 s= new String[5];//为数组分配空间时就要设置大小 对于ArrayList, ArrayList<String> result = new ArrayList<String>(); //这时候就...

    java中新建数组:

    String[] s;//定义的时候不需要设置大小
    s = new String[5];//为数组分配空间时就要设置大小
     
    对于ArrayList,
    ArrayList<String> result = new ArrayList<String>();
    //这时候就不需要设置大小了,result.add(string)添加一个元素,result.remove(string)删除一个元素,大小可以由result.size()得到
     
    再来看看
    java里允许Map[] mapArray = new Map[5];
    也允许Map<Integer,String>[] mapArray = new Map[5];
    但不允许Map<Integer,String>[] mapArray = new Map<Integer,String>[5];//即不允许new一个带泛型的数组
     
    在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。泛型就是显示的告诉编译器容器里的类型,如Map<Integer,String>就是个泛型。

    转载于:https://www.cnblogs.com/lezhou2014/p/4128380.html

    展开全文
  • Java新建数组

    千次阅读 2015-10-25 16:51:01
    一、新建一维数组1、基本类型数组 int[] a = new int[3]; 或者int a[] = new int[3]; 2、对象类型数组 String[] s = new String[3]; 或者String s[] = new String[3]; Person[] persons = new Persons[3]; ...
  • Java新建数组

    2017-06-27 11:19:00
    smallPrimes = new int[] {17, 19, 23, 29, 31, 37} 在不新建数组的情况下重新初始化一个数组变量 int[] anonymous = {17, 19, 23, 29, 31, 37}; Array Copying int[] luckyNumbers = smallPrimes; luckyNumbers[5]...
  • 使 SortList 实现重复键排序SortList默认对按Key来排序,且Key值不能重复,但有时可能需要用有重复值的Key来排序,以下是实现方式: 1.对强类型:以float为例 #region 使SortList能对重复键排序 ...arduino图形化编程——...
  • 动态数组java简单实现

    2021-03-13 11:20:13
    动态数组java简单实现 动态数组 数组结构: 动态数组类: 底层结构: 数组 属性:下标 元素个数 长度 初始化: 1、 根据构造方法参数在实例化初始化数组 2、 默认长度初始化 增: 添加数组 容量不够: 扩容 删...
  • 第五章 5.3-5.6,不包括反射和设计5.3 泛型数组列表 ArrayList —— 为了处理java数组长度不确定、可变的情况,一个使用类型参数的泛型类。//新建 ArrayList<Employee> staff = new ArrayList<>();//...
  • */ /* Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 10 at Demo2.main(Demo2.java:22) ArrayIndexOutOfBoundsException 数组下标越界异常 */ /* 给数组中下标为-1的元素赋值为20 ...
  • 构建乘积数组java实现

    2017-10-10 14:21:23
    给定一个数组A[0,1,…,n-1],请构建一个...思路:遍历数组新建一个数组记录从下一个下标开始到结束的元素乘积;然后从头遍历原数组,最后再乘上对应的后续乘积。import java.util.ArrayList; public class Solution {
  • Java创建JSON对象private void getJson(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {PrintWriter out=response.getWriter();//servlet的输出流,在页面可以...
  • 思路:新建一个以两个集合长度之和为长度的新数组,从两数组最左边开始比起,把小的放入新集合,并用变量标记后一位置,每次比较都是比较的最左边未比较过的元素(通过变量),循环比较,直至其中有一个集合遍历结束,...
  • 展开全部importjava.util.ArrayList;importjava.util.Scanner;/****@authoryoung**/classPeople{privateStringname;privateStringpassword;privatedoubleheight;privatedoubletz;privateStringblood;pub...
  • 缘由之前leetcode刷题的时候,某道题试图新建一个HashMap的数组,总是编译出错,然后突然反应过来,Java里是不能按照像通常新建基本类型的数组一样,直接新建泛型数组的。希望我能以比较简单明了的方式说明其中的...
  • 展开全部public class ListInsert {public static long[] insert(long[] arr,int i,long l){//新建数组,对原62616964757a686964616fe59b9ee7ad9431333365633939数组扩容long[] arr1 = new long[arr.length+1];...
  • 思路就是新建一个数组,把原数组的元素赋进去,再去除因此产生的0。import java.util.Arrays;public class Solution {public static void main(String[] args) {int[] array1 = {1,2,3,4,4};int[] array2 = {3,1,4,1...
  • 先看看下面的代码,大家...public class ArrayTest {public static void main(String[] args){//新建一个对象(OneNum)数组(赋值为5、3、4)OneNum[] ac = {new OneNum(5),new OneNum(3),new OneNum(4)};//新建一个与a...
  • 展开全部public class ListInsert {public static long[] insert(long[] arr,int i,long l){//新建数组,对原数组扩容long[] arr1 = new long[arr.length+1];//将原数组数32313133353236313431303231363533e4b893e5...
  • 前言集合在Java中是非常重要,不仅在Java项目开发中高频使用,在面试中也经常出现集合相关的问题。本文主要给大家介绍一下ArrayList集合类。一、ArrayList简介ArrayList是实现了基于动态数组的数据结构,一个...
  • 新建一个oracle 数组类型的typecreate or replace type str_list as varray(1000) of varchar2(40)新建一个简单的存储过程测试用:create or replace procedure getReservoirReportForms(stcds in str_list,kk out ...
  • Java数组

    2020-07-30 00:40:48
    假如统计全班30人的平均分,若没有数组,需要新建30个变量,每个变量存储一个学生的成绩。 int stu1 = 95; int stu2 = 89; int stu3 = 79; int stu4 = 64; int stu5 = 76; int stu6 = 88; …… avg = (stu1+stu2+stu...
  • java数组

    2021-01-11 17:25:31
    chart[] c1=new chart[]{“1”,“2”,“3”,“4”};...如果新增一个 e, 先新建一个长度为5的数组,再然后将abcd拷贝进去,将e放进去; 2,如果删除一个a,先新建一个长度为4的数组,然后将bcd拷贝进去。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,003
精华内容 401
关键字:

新建数组java

java 订阅