原子類、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
並不簡單,至少我現在還對它瞭解不夠,所以就臨時換頻道,分享各種多執行緒執行緒安全解決方案的效能測試。
今天我們主要測試三種解決方案的效能,分別是原子類、lock
和synchronized
。從上面的程式碼中,我們也可以看出了,以上程式碼是執行緒不安全的,所以接下來我們就要分別通過這三種解決方案來優化上面的程式碼,然後分別測試執行時間。
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
或者原子類執行緒安全問題依然存在。總之,凡事多實踐多總結,共勉!