1. 程式人生 > 其它 >原子類、lock、synchronized簡單效能對比

原子類、lock、synchronized簡單效能對比

前言

前幾天,我們分享了原子類相關的知識點,展示了原子類的一些用法,之前也分享了lock相關的應用,但是我一直有一個困惑,就是在多執行緒資料安全的幾個常用解決方案中,到底哪一個效能最好,我們在實際應用開發中應該如何選擇,今天我們就來簡單探討下這個問題。

效能對比

開始之前,我們先看這樣一段程式碼:

public class CountDownLatchTest {
    static Integer count = 0;
    private static final int SIZE = 100;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            new Thread(new TaskPortion(startTime)).start();
        }
    }

    static class TaskPortion implements Runnable {
        private long startTime;
        public TaskPortion() {
        }
        public TaskPortion(long startTime) {
            this.startTime = startTime;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(count++);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

從程式碼大家應該能看出了,我原本是要分享CountDownLatch相關知識的,但是在實際操作的時候,我發現countDownLatch並不簡單,至少我現在還對它瞭解不夠,所以就臨時換頻道,分享各種多執行緒執行緒安全解決方案的效能測試。

今天我們主要測試三種解決方案的效能,分別是原子類、locksynchronized。從上面的程式碼中,我們也可以看出了,以上程式碼是執行緒不安全的,所以接下來我們就要分別通過這三種解決方案來優化上面的程式碼,然後分別測試執行時間。

lock

我們先看第一種lock,這裡我們主要是應用可重入鎖,然後優化程式碼:

public class CountDownLatchTest {
    static Integer count = 0;
    private static final int SIZE = 100;
    // 可重入鎖
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            new Thread(new TaskPortion(startTime)).start();
        }
    }

    static class TaskPortion implements Runnable {
        private long startTime;
        public TaskPortion() {
        }
        public TaskPortion(long startTime) {
            this.startTime = startTime;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                Thread.sleep(1000);
                System.out.println(count++);
                if (count == 99) {
                    System.out.println("用時:" + (System.currentTimeMillis() - startTime));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

我們定義了一個可重入鎖,在run方法中加鎖,然後在finally程式碼塊中釋放鎖,執行以上程式碼,我們會得到如下執行結果:

整個執行過程特別慢,大概需要90秒,具體取決於電腦效能。

synchronized

然後我們再來看下synchronized加持下的執行效能,調整後的程式碼如下:

public class CountDownLatchTest {
    static Integer count = 0;
    private static final int SIZE = 100;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            new Thread(new TaskPortion(startTime)).start();
        }
    }

    static class TaskPortion implements Runnable {
        private long startTime;
        public TaskPortion() {
        }
        public TaskPortion(long startTime) {
            this.startTime = startTime;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                synchronized (count){
                    System.out.println(count++);
                    if (count == 99) {
                        System.out.println("用時:" + (System.currentTimeMillis() - startTime));
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

關於synchrronized這裡就不過多說明了,應該算特別基礎的知識,它能修飾變數、方法、程式碼塊,在應用層面也比較靈活。

執行結果很感人,時間直接比lock快了90倍,但是執行過程中出現了併發的情況(翻車了),因為i++操作不是原子的,所以單synchrioned並不能保證執行緒安全。繼續往後看,後面有最終解決方案。

原子類

接下來,我們來看最後一種——原子類,程式碼調整如下:

public class CountDownLatchTest {
    static AtomicInteger count = new AtomicInteger(0);
    private static final int SIZE = 100;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            new Thread(new TaskPortion(startTime)).start();
        }
    }

    static class TaskPortion implements Runnable {
        private long startTime;
        public TaskPortion() {
        }
        public TaskPortion(long startTime) {
            this.startTime = startTime;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(count.getAndAdd(1));
                if (count.get() == 99) {
                    System.out.println("用時:" + (System.currentTimeMillis() - startTime));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行結果如下,效能方面確實要比lock優秀的多,但是執行結果依然翻車,出現了執行緒安全問題。

然後再一次測試的機緣巧合之下,我發現把原子類和synchronized組合一下,就可以完美地解決這個問題,程式碼調整如下:

public class CountDownLatchTest {
    static AtomicInteger count = new AtomicInteger(0);
    private static final int SIZE = 100;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            new Thread(new TaskPortion(startTime)).start();
        }
    }

    static class TaskPortion implements Runnable {
        private long startTime;
        public TaskPortion() {
        }
        public TaskPortion(long startTime) {
            this.startTime = startTime;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                synchronized (count){
                    System.out.println(count.getAndAdd(1));
                    if (count.get() == 99) {
                        System.out.println("用時:" + (System.currentTimeMillis() - startTime));
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

然後執行結果就正常了,而且效能一點也不弱,可以吊打lock

總結

在以前的認知裡,我一直覺得synchronized比顯式的lock要重,但是通過今天的測試,我發現自己以前的認知是有問題的,synchronized要比顯式的lock效能好的多,同時也意識到單獨使用原子類或者synchronized都是存線上程安全問題的,所以在日常開發中,更多時候是需要把兩者完美組合的。

在此之前,我一直以為原子類是可以單獨使用的,但是踩了今天的坑才知道,就算你用了synchronized或者原子類執行緒安全問題依然存在。總之,凡事多實踐多總結,共勉!