數組拷貝之System.arraycopy學習

看 JDK 源碼的時候,Java 開發設計者在對數組的複製時,一般都會使用 System.arraycopy() 方法。java

其實對數組的複製,有四種方法:數組

  • for安全

  • clone多線程

  • System.arraycopyide

  • arrays.copyof函數

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

  • 深複製,仍是淺複製this

  • 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循環過程當中的尋址時間,從而提升了效能。

相關文章
相關標籤/搜索