轉載:System:System.arraycopy方法詳解

chamwarren發表於2018-11-22

看 JDK 原始碼的時候,Java 開發設計者在對陣列的複製時,通常都會使用 System.arraycopy() 方法。

其實對陣列的複製,有四種方法:

  • for

  • clone

  • System.arraycopy

  • arrays.copyof

本文章主要分析 System.arraycopy() ,帶著幾個問題去看這個方法:

  • 深複製,還是淺複製

  • String 的一維陣列和二維陣列複製是否有區別

  • 執行緒安全,還是不安全

  • 高效還是低效

System.arraycopy() 的 API :

public static void arraycopy(
                             Object src,  //源陣列
                             int srcPos,  //源陣列的起始位置
                             Object dest, //目標陣列
                             int destPos, //目標陣列的起始位置
                             int length   //複製長度
                             )複製程式碼

深複製還是淺複製

程式碼:物件陣列的複製:

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迴圈過程中的定址時間,從而提高了效能。


轉載地址:https://segmentfault.com/a/1190000009922279


相關文章