• JDK源码解析——集合(一)数组 ArrayList


    JDK源码解析——ArrayList

    本文针对JDK1.8

    概述

    ArrayList ,基于 [] 数组实现的,支持自动扩容的动态数组。相比数组来说,因为其支持自动扩容的特性,成为我们日常开发中,最常用的集合类,没有之一。

    类图

    1584625237213

    属性

    	private static final long serialVersionUID = 8683452581122892189L;
    
        /**
         * Default initial capacity.默认容量是10
         */
        private static final int DEFAULT_CAPACITY = 10;
    
        /**
         * Shared empty array instance used for empty instances.
         */
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
        /**
         * Shared empty array instance used for default sized empty instances. We
         * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
         * first element is added.
         */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        /**
         * The array buffer into which the elements of the ArrayList are stored.
         * The capacity of the ArrayList is the length of this array buffer. Any
         * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
         * will be expanded to DEFAULT_CAPACITY when the first element is added.
         */
        transient Object[] elementData; // non-private to simplify nested class access
    
        /**
         * The size of the ArrayList (the number of elements it contains).
         *
         * @serial
         */
        private int size;
    

    1584625496439

    • elementData 属性:元素数组。其中,图中红色空格代表我们已经添加元素,白色空格代表我们并未使用。
    • size 属性:数组大小。注意,size 代表的是 ArrayList 已使用 elementData 的元素的数量,对于开发者看到的 #size() 也是该大小。并且,当我们添加新的元素时,恰好其就是元素添加到 elementData 的位置(下标)。当然,我们知道 ArrayList 真正的大小是 elementData 的大小。

    构造方法

    ArrayList 一共有三个构造方法,我们分别来看看。

    #ArrayList(int initialCapacity)

    #ArrayList(int initialCapacity) 构造方法,根据传入的初始化容量,创建 ArrayList 数组。如果我们在使用时,如果预先指到数组大小,一定要使用该构造方法,可以避免数组扩容提升性能,同时也是合理使用内存。代码如下:

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    
    • 如果初始化容量为 0 时,使用 EMPTY_ELEMENTDATA 空数组。在添加元素的时候,会进行扩容创建需要的数组。笔者会在下文中介绍扩容机制。

    #ArrayList(Collection<? extends E> c)

    #ArrayList(Collection<? extends E> c) 构造方法,使用传入的 c 集合,作为 ArrayList 的 elementData 。代码如下:

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //JDK8存在BUG,它在 JDK9 中被解决,。
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    

    #ArrayList()

    无参数构造方法 #ArrayList() 构造方法,也是我们使用最多的构造方法。代码如下:

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    Q: public ArrayList(0)public ArrayList()两个构造方法有什么区别?

    回答这个问题之前,先看一下ArrayList的两个成员变量:

    这是两个空的Object数组,但需注意这是两个数组对象

    private static final Object[] EMPTY_ELEMENTDATA = {};
        
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    

    可以看到,public ArrayList(0)会将elementData数组赋值为EMPTY_ELEMENTDATA

    public ArrayList()会将elementData数组赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,虽然都是空的数组,但这是两个对象!

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    在下文中,我们会看到 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 首次扩容为 10 ,而 EMPTY_ELEMENTDATA 按照 1.5 倍扩容从 0 开始而不是 10 。两者的起点不同。

    添加单个元素

    #add(E e) 方法,顺序添加单个元素到数组,该方法返回boolean变量。代码如下:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

    #add(int index, E element) 方法,顺序插入单个元素到数组。前提是index > size || index < 0代码如下:

    public void add(int index, E element) {
            rangeCheckForAdd(index);//先检查index是否合法
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
        	 // 将 index + 1 位置开始的元素,进行往后挪
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
        }
    
    • 值得注意的是这里的#ensureCapacityInternal(int minCapacity)方法。它的作用是保证 elementData 数组容量至少有 minCapacityminCapacity至少为1
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
      //如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA对象,则将minCapacity设为DEFAULT_CAPACITY(10)
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    
        ensureExplicitCapacity(minCapacity);
    }
    

    其中#ensureExplicitCapacity(int minCapacity)方法

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)//如果minCapacity超出elementData数组长度,则扩容
            grow(minCapacity);
    }
    

    数组扩容

    #grow() 方法,扩容数组,并返回它。整个的扩容过程,首先创建一个新的更大的数组,一般是 1.5 倍大小(为什么说是一般呢,稍后我们会看到,会有一些小细节),然后将原数组复制到新数组中,最后返回新数组。代码如下:

     private void grow(int minCapacity) {
         // overflow-conscious code
         int oldCapacity = elementData.length;
         int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍扩容
         if (newCapacity - minCapacity < 0)
             //此时有一种情况:如果elementData为空,newCapacity仍为0,newCapacity置为1
             newCapacity = minCapacity;
         if (newCapacity - MAX_ARRAY_SIZE > 0)
             newCapacity = hugeCapacity(minCapacity);
         // minCapacity is usually close to size, so this is a win:
         elementData = Arrays.copyOf(elementData, newCapacity);
     }
    

    其中,#hugeCapacity(int minCapacity)如下:

    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
    }
    

    Q:ArrayList最大容量问题?

    rivate static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    

    最大容量为Integer.MAX_VALUE-8,

    public ArrayList(int initialCapacity)
    

    通过以上构造函数指定初始容量,而int最大值就是Integer.MAX_VALUE,-8是为了避免oom,因为有些vm可能存储头信息在数组里。因此最大容量为Integer.MAX_VALUE-8。但是当MAX_ARRAY_SIZE仍不够时,容量会扩展至HugeCapacity,为Integer.MAX_VALUE。

    private void grow(int minCapacity) {//保证 elementData 数组容量至少有 minCapacity
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍扩容
            if (newCapacity - minCapacity < 0)//若扩容之后,新容量还不够,就将新容量设为minCapacity
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    数组缩容

    #trimToSize() ,将elementData数组长度裁剪到当前size大小。代码如下:

    public void trimToSize() {
            modCount++;
            if (size < elementData.length) {
                elementData = (size == 0)
                  ? EMPTY_ELEMENTDATA//如果elementData已空,则将EMPTY_ELEMENTDATA赋值过去
                  : Arrays.copyOf(elementData, size);
            }
        }
    
    
    • 这点和HashMap不同,HashMap未提供缩容方法。

    添加多个元素

    #addAll(Collection<? extends E> c) 方法,批量添加多个元素。在我们明确知道会添加多个元素时,推荐使用该该方法而不是添加单个元素,避免可能多次扩容。代码如下:

    //向elementData数组末尾添加元素
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount,保证 elementData 数组容量够用
        System.arraycopy(a, 0, elementData, size, numNew);//直接在elementData数组末尾添加元素
        size += numNew;
        return numNew != 0;//若添加的Collection为空,则返回false
    }
    

    #addAll(int index, Collection<? extends E> c) 方法,从指定位置开始插入多个元素,前提是index > size || index < 0。代码如下:

    //向elementData数组中间(index处)插入元素
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);//先检查index是否合法
    
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount,保证 elementData 数组容量够用
        int numMoved = size - index;
        if (numMoved > 0)
            //elementData数组中间(index处)插入元素
            //例如elementData数组原来为{"a","b","c","d"},需要在index=1处添加数据{"q","w"}
            //插入之后,elementData数组变为{"a","q","w","b","c","d"}
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);//elementData数组index及以后元素往后移位index + numNew
    
        System.arraycopy(a, 0, elementData, index, numNew);//将a数组填入
        size += numNew;
        return numNew != 0;
    }
    

    移除单个元素

    #remove(int index) 方法,移除指定位置的元素,并返回该位置的原元素。代码如下:

    public E remove(int index) {
        rangeCheck(index);//先检查index是否合法,index >= size则抛异常
    
        modCount++;
        E oldValue = elementData(index);
    
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }
    

    #remove(Object o)方法会移除数组中第一个出现的元素。代码如下:

    public boolean remove(Object o) {
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }
    
    • 其中,#fastRemove(int index)
     private void fastRemove(int index) {
         modCount++;
         int numMoved = size - index - 1;
         if (numMoved > 0)
             System.arraycopy(elementData, index+1, elementData, index,
                              numMoved);
         elementData[--size] = null; // clear to let GC do its work
     }
    

    移除多个元素

    #removeAll(Collection<?> c),批量移除elementData中指定的多个元素。

    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, false);
    }
    

    #retainAll(Collection<?> c),保留elementData中指定的多个元素,其余的全部删除。

    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
    
    • 上述两个方法都是用#batchRemove(Collection<?> c, boolean complement)来实现的,他们的区别就是boolean complement是否为true。
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        int r = 0, w = 0;
        boolean modified = false;
        try {
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)// 判断符合条件
                    // 移除的方式,通过将当前值 写入到 w 位置,然后 w 跳到下一个位置。
                    elementData[w++] = elementData[r];
        } finally {
            // Preserve behavioral compatibility with AbstractCollection,
            // even if c.contains() throws.
            if (r != size) {
                //如果 contains 方法发生异常,则将 elementData的元素从 r 位置的数据写入到 es 从 w 开始的位置
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // clear to let GC do its work
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }
    
    • 如果觉得绕,多调试,可以手绘点图,辅助理解下哈。

    查找单个元素

    #indexOf(Object o) 方法,查找首个为指定元素的位置,如果没有找到,则返回-1。代码如下:

    public int indexOf(Object o) {
        //分为o为null和不为null两种情况
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
    

    #contains(Object o) 方法,就是基于该方法实现。代码如下:

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    

    获得指定位置的元素

    #get(int index) 方法,获得指定位置的元素。代码如下:

    public E get(int index) {
            rangeCheck(index);//先检查index是否合法,index >= size则抛异常
    
            return elementData(index);
        }
    

    设置指定位置的元素

    #set(int index, E element) 方法,设置指定位置的元素,返回旧值。代码如下:

    public E set(int index, E element) {
        rangeCheck(index);//先检查index是否合法,index >= size则抛异常
    
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    

    转换成数组

    #toArray() 方法,将 ArrayList 转换成 [] 数组。代码如下:

    // ArrayList.java
    
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    
    // Arrays.java
    
    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    
    • 注意,返回的是 Object[] 类型。

    实际场景下,我们可能想要指定 T 泛型的数组,那么我们就需要使用到 #toArray(T[] a) 方法。代码如下:

     public <T> T[] toArray(T[] a) {
         if (a.length < size)//如果传入的数组小于 size 大小,则直接复制一个新数组返回
             // Make a new array of a's runtime type, but my contents:
             return (T[]) Arrays.copyOf(elementData, size, a.getClass());
         //将 elementData 复制到 a 中
         System.arraycopy(elementData, 0, a, 0, size);
         //如果传入的数组大于 size 大小,则将 size 赋值为 null
         if (a.length > size)
             a[size] = null;
         return a;
     }
    
    • 所以有两种方法获取指定 T 泛型的数组:(假设我们想要将list转为String类型的数组)
      • list.toArray(new Sring[0]); 返回一个新数组
      • list.toArray(new Sring[list.size()]); 返回的就是a数组,这个性能略高一点~

    创建子数组

    #subList(int fromIndex, int toIndex) 方法,创建 ArrayList 的子数组,注意不包含toIndex哦。代码如下:

    public List<E> subList(int fromIndex, int toIndex) {
        subListRangeCheck(fromIndex, toIndex, size);//检查参数是否合法
        return new SubList(this, 0, fromIndex, toIndex);
    }
    
    private static class SubList<E> extends AbstractList<E> implements RandomAccess {
    
        /**
         * 根 ArrayList
         */
        private final ArrayList<E> root;
        /**
         *  父 SubList
         */
        private final SubList<E> parent;
        /**
         * 起始位置
         */
        private final int offset;
        /**
         * 大小
         */
        private int size;
    
        // ... 省略代码
    }
    
    • 实际使用时,一定要注意,SubList 不是一个只读数组,而是和根数组 root 共享相同的 elementData 数组,只是说限制了 [fromIndex, toIndex) 的范围。

    System.arraycopy() :

    public static void arraycopy(
                                 Object src,  //源数组
                                 int srcPos,  //源数组的起始位置
                                 Object dest, //目标数组
                                 int destPos, //目标数组的起始位置
                                 int length   //复制长度
                                 )
    

    下文参考:https://segmentfault.com/a/1190000009922279

    深复制还是浅复制

    代码:对象数组的复制:

    public class SystemArrayCopyTestCase {
    
        public static void main(String[] args) {
            User[] users = new User[] { 
                    new User(1, "seven", "seven@qq.com"), 
                    new User(2, "six", "six@qq.com"),
                    new User(3, "ben", "ben@qq.com") };// 初始化对象数组
            
            User[] target = new User[users.length];// 新建一个目标对象数组
            
            System.arraycopy(users, 0, target, 0, users.length);// 实现复制
            
            System.out.println("源对象与目标对象的物理地址是否一样:" + (users[0] == target[0] ? "浅复制" : "深复制"));  //浅复制
            
            target[0].setEmail("admin@sina.com");
            
            System.out.println("修改目标对象的属性值后源对象users:");
            for (User user : users) {
                System.out.println(user);
            }
            //
            //
            //
        }
    }
    
    class User {
        private Integer id;
        private String username;
        private String email;
    
        // 无参构造函数
        public User() {
        }
    
        // 有参的构造函数
        public User(Integer id, String username, String email) {
            super();
            this.id = id;
            this.username = username;
            this.email = email;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        @Override
        public String toString() {
            return "User [id=" + id + ", username=" + username + ", email=" + email + "]";
        }
    }
    

    图示:对象复制的图示

    clipboard.png

    所以,得出的结论是,System.arraycopy() 在拷贝数组的时候,采用的使用浅复制,复制结果是一维的引用变量传递给副本的一维数组,修改副本时,会影响原来的数组。


    一维数组和多维数组的复制的区别

    代码:一维数组的复制

            String[] st  = {"A","B","C","D","E"};
            String[] dt  = new String[5];
            System.arraycopy(st, 0, dt, 0, 5);
            
            //改变dt的值
            dt[3] = "M";
            dt[4] = "V";
            
            System.out.println("两个数组地址是否相同:" + (st == dt)); //false
            
            for(String str : st){
                System.out.print(" " + str +" ");   // A  B  C  D  E 
                
            }
            System.out.println(); 
            for(String str : dt){
                System.out.print(" " + str +" ");   // A  B  C  M  V 
            }
    

    使用该方法对一维数组在进行复制之后,目标数组修改不会影响原数据,这种复制属性值传递,修改副本不会影响原来的值。

    但是,请重点看以下代码:

            String[] st  = {"A","B","C","D","E"};
            String[] dt  = new String[5];
            System.arraycopy(st, 0, dt, 0, 5);
    
           for(String str : st){
                System.out.print(" " + str +" ");   // A  B  C  D  E 
                
            }
            System.out.println(); 
            for(String str : dt){
                System.out.print(" " + str +" ");   // A  B  C  D  E 
            }
    
            System.out.println("数组内对应位置的String地址是否相同:" + st[0] == dt[0]); // true
    

    既然是属性值传递,为什么 st[0] == dt[0] 会相等呢? 我们再深入验证一下:

            String[] st  = {"A","B","C","D","E"};
            String[] dt  = new String[5];
            System.arraycopy(st, 0, dt, 0, 5);
            dt[0] = "F" ;
            
            for(String str : st){
                System.out.print(" " + str +" ");   // A  B  C  D  E 
                
            }
            System.out.println(); 
            for(String str : dt){
                System.out.print(" " + str +" ");   // F  B  C  D  E 
            }
    
    
            System.out.println("数组内对应位置的String地址是否相同:" + st[0] == dt[0]); // false
    

    为什么会出现以上的情况呢?

    通过以上两段代码可以推断,在System.arraycopy()进行复制的时候,首先检查了字符串常量池是否存在该字面量,一旦存在,则直接返回对应的内存地址,如不存在,则在内存中开辟空间保存对应的对象。

    代码:二维数组的复制

            String[][] s1 = {
                        {"A1","B1","C1","D1","E1"},
                        {"A2","B2","C2","D2","E2"},
                        {"A3","B3","C3","D3","E3"}
                            };
            String[][] s2 = new String[s1.length][s1[0].length];  
            
            System.arraycopy(s1, 0, s2, 0, s2.length);  
            
            for(int i = 0;i < s1.length ;i++){ 
             
               for(int j = 0; j< s1[0].length ;j++){  
                  System.out.print(" " + s1[i][j] + " ");
               }  
               System.out.println();  
            }  
            
            //  A1  B1  C1  D1  E1 
            //  A2  B2  C2  D2  E2 
            //  A3  B3  C3  D3  E3 
            
            
            s2[0][0] = "V";
            s2[0][1] = "X";
            s2[0][2] = "Y";
            s2[0][3] = "Z";
            s2[0][4] = "U";
            
            System.out.println("----修改值后----");  
            
            
            for(int i = 0;i < s1.length ;i++){  
                   for(int j = 0; j< s1[0].length ;j++){  
                      System.out.print(" " + s1[i][j] + " ");
                   }  
                   System.out.println();  
             }  
    
            //  Z   Y   X   Z   U 
            //  A2  B2  C2  D2  E2 
            //  A3  B3  C3  D3  E3 
    

    上述代码是对二维数组进行复制,数组的第一维装的是一个一维数组的引用,第二维里是元素数值。对二维数组进行复制后,第一维的引用被复制给新数组的第一维,也就是两个数组的第一维都指向相同的“那些数组”。而这时改变其中任何一个数组的元素的值,其实都修改了“那些数组”的元素的值,所以原数组和新数组的元素值都一样了。


    线程安全,还是不安全

    代码:多线程对数组进行复制 (java中System.arraycopy是线程安全的吗? )

    public class ArrayCopyThreadSafe {
        private static int[] arrayOriginal = new int[1024 * 1024 * 10];
        private static int[] arraySrc = new int[1024 * 1024 * 10];
        private static int[] arrayDist = new int[1024 * 1024 * 10];
        private static ReentrantLock lock = new ReentrantLock();
    
        private static void modify() {
            for (int i = 0; i < arraySrc.length; i++) {
                arraySrc[i] = i + 1;
            }
        }
    
        private static void copy() {
            System.arraycopy(arraySrc, 0, arrayDist, 0, arraySrc.length);
        }
    
        private static void init() {
            for (int i = 0; i < arraySrc.length; i++) {
                arrayOriginal[i] = i;
                arraySrc[i] = i;
                arrayDist[i] = 0;
            }
        }
    
        private static void doThreadSafeCheck() throws Exception {
            for (int i = 0; i < 100; i++) {
                System.out.println("run count: " + (i + 1));
                init();
                Condition condition = lock.newCondition();
    
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        lock.lock();
                        condition.signalAll();
                        lock.unlock();
                        copy();
                    }
                }).start();
    
    
                lock.lock();
                // 这里使用 Condition 来保证拷贝线程先已经运行了.
                condition.await();
                lock.unlock();
    
                Thread.sleep(2); // 休眠2毫秒, 确保拷贝操作已经执行了, 才执行修改操作.
                modify();
    
                if (!Arrays.equals(arrayOriginal, arrayDist)) {
                    throw new RuntimeException("System.arraycopy is not thread safe");
                }
            }
        }
    
        public static void main(String[] args) throws Exception {
            doThreadSafeCheck();
        }
    }
    

    这个例子的具体操作是:

    1. arrayOriginal 和 arraySrc 初始化时是相同的, 而 arrayDist 是全为零的.
    2. 启动一个线程运行 copy() 方法来拷贝 arraySrc 到 arrayDist 中.
    3. 在主线程执行 modify() 操作, 修改 arraySrc 的内容. 为了确保 copy() 操作先于 modify() 操作, 我使用 Condition, 并且延时了两毫秒, 以此来保证执行拷贝操作(即System.arraycopy) 先于修改操作.
    4. 根据第三点, 如果 System.arraycopy 是线程安全的, 那么先执行拷贝操作, 再执行修改操作时, 不会影响复制结果, 因此 arrayOriginal 必然等于 arrayDist; 而如果 System.arraycopy 是线程不安全的, 那么 arrayOriginal 不等于 arrayDist.

    根据上面的推理, 运行一下程序, 有如下输出:

    run count: 1
    run count: 2
    Exception in thread "main" java.lang.RuntimeException: System.arraycopy is not thread safe
        at com.test.ArrayCopyThreadSafe.doThreadSafeCheck(ArrayCopyThreadSafe.java:62)
        at com.test.ArrayCopyThreadSafe.main(ArrayCopyThreadSafe.java:68)
    

    所以,System.arraycopy是不安全的。


    高效还是低效

    代码:for vs System.arraycopy 复制数组

            String[] srcArray = new String[1000000];
            String[] forArray = new String[srcArray.length];
            String[] arrayCopyArray  = new String[srcArray.length];
            
            //初始化数组
            for(int index  = 0 ; index  < srcArray.length ; index ++){
                srcArray[index] = String.valueOf(index);
            }
            
            long forStartTime = System.currentTimeMillis();
            for(int index  = 0 ; index  < srcArray.length ; index ++){
                forArray[index] = srcArray[index];
            }
            long forEndTime = System.currentTimeMillis();
            System.out.println("for方式复制数组:"  + (forEndTime - forStartTime));
    
            long arrayCopyStartTime = System.currentTimeMillis();
            System.arraycopy(srcArray,0,arrayCopyArray,0,srcArray.length);
            long arrayCopyEndTime = System.currentTimeMillis();
            System.out.println("System.arraycopy复制数组:"  + (arrayCopyEndTime - arrayCopyStartTime));
    

    通过以上代码,当测试数组的范围比较小的时候,两者相差的时间无几,当测试数组的长度达到百万级别,System.arraycopy的速度优势就开始体现了,根据对底层的理解,System.arraycopy是对内存直接进行复制,减少了for循环过程中的寻址时间,从而提高了效能。

    总结

    • ArrayList 是基于 [] 数组实现的 List 实现类,支持在数组容量不够时,一般按照 1.5自动扩容。同时,它支持手动扩容、手动缩容。

    • 源码当中涉及到许多数组的复制迁移,System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

    这个方法属于浅复制,线程不安全,它减少了for循环过程中的寻址时间,相较于for循环来说,提高了性能。

    如果文中有描述不对的地方,敬请批评指正。

    以上。

  • 相关阅读:
    Vue组件以及组件之间的通信
    VueRouter和Vue生命周期(钩子函数)
    Vuex、axios以及跨域请求处理
    element-ui和npm、webpack、vue-cli搭建Vue项目
    2018PyCharm激活方法
    pycharm修改选中字体颜色
    为自己的博客园添加目录锚点和返回顶部
    python初识
    JAVA判断当前日期是节假日还是工作日
    springmvc使用freemarker
  • 原文地址:https://www.cnblogs.com/yifengGG/p/12531200.html
Copyright © 2020-2023  润新知