Java中如何保證執行緒安全性
一、執行緒安全在三個方面體現
1.原子性:提供互斥訪問,同一時刻只能有一個執行緒對資料進行操作,(atomic,synchronized);
2.可見性:一個執行緒對主記憶體的修改可以及時地被其他執行緒看到,(synchronized,volatile);
3.有序性:一個執行緒觀察其他執行緒中的指令執行順序,由於指令重排序,該觀察結果一般雜亂無序,(happens-before原則)。
接下來,依次分析。
二、原子性---atomic
JDK裡面提供了很多atomic類,AtomicInteger,AtomicLong,AtomicBoolean等等。
它們是通過CAS完成原子性。
我們一次來看AtomicInteger,
(1)AtomicInteger
先來看一個AtomicInteger例子:
public class AtomicIntegerExample1 { // 請求總數 public static int clientTotal = 5000; // 同時併發執行的執行緒數 public static int threadTotal = 200; public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool();//獲取執行緒池 final Semaphore semaphore = new Semaphore(threadTotal);//定義訊號量 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); log.info("count:{}", count.get()); } private static void add() { count.incrementAndGet(); } }
我們可以執行看到最後結果是5000是執行緒安全的。
那麼看AtomicInteger的incrementAndGet()方法:
再看getAndAddInt()方法:
這裡面呼叫了compareAndSwapInt()方法:
它是native修飾的,代表是java底層的方法,不是通過java實現的 。
再重新看getAndAddInt(),傳來第一個值是當前的一個物件,比如是count.incrementAndGet(),那麼在getAndAddInt()中,var1就是count,而var2第二個值是當前的值,比如想執行的是2+1=3操作,那麼第二個引數是2,第三個引數是1 。
變數5(var5)是我們呼叫底層的方法而得到的底層當前的值,如果沒有別的執行緒過來處理我們
因此傳到compareAndSwapInt方法裡的引數是(count物件,當前值2,當前從底層傳過來的2,從底層取出來的值加上改變數var4)。
compareAndSwapInt()希望達到的目標是對於var1物件,如果當前的值var2和底層的值var5相等,那麼把它更新成後面的值(var5+var4).
compareAndSwapInt核心就是CAS核心。
關於count值為什麼和底層值不一樣:count裡面的值相當於存在於工作記憶體的值,底層就是主記憶體。
(2)AtomicStampedReference
接下來我們看一下AtomicStampedReference。
關於CAS有一個ABA問題:開始是A,後來改為B,現在又改為A。解決辦法就是:每次變數改變的時候,把變數的版本號加1。
這就用到了AtomicStampedReference。
我們來看AtomicStampedReference裡的compareAndSet()實現:
而在AtomicInteger裡compareAndSet()實現:
可以看到AtomicStampedReference裡的compareAndSet()中多了 一個stamp比較(也就是版本),這個值是由每次更新時來維護的。
(3)AtomicLongArray
這種維護陣列的atomic類,我們可以選擇性地更新其中某一個索引對應的值,也是進行原子性操作。這種對陣列的操作的各種方法,會多處一個索引。
比如,我們看一下compareAndSet():
(4)AtomicBoolean
看一段程式碼:
public class AtomicBooleanExample {
private static AtomicBoolean isHappened = new AtomicBoolean(false);
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的執行緒數
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("isHappened:{}", isHappened.get());
}
private static void test() {
if (isHappened.compareAndSet(false, true)) {
log.info("execute");
}
}
}
執行之後發現,log.info("execute");只執行了一次,且isHappend值為true。
原因就是當它第一次compareAndSet()之後,isHappend變為true,沒有別的執行緒干擾。
通過使用AtomicBoolean,我們可以使某段程式碼只執行一次。
三、原子性---synchronized
synchronized是一種同步鎖,通過鎖實現原子操作。
JDK提供鎖分兩種:一種是synchronized,依賴JVM實現鎖,因此在這個關鍵字作用物件的作用範圍內是同一時刻只能有一個執行緒進行操作;另一種是LOCK,是JDK提供的程式碼層面的鎖,依賴CPU指令,代表性的是ReentrantLock。
synchronized修飾的物件有四種:
(1)修飾程式碼塊,作用於呼叫的物件;
(2)修飾方法,作用於呼叫的物件;
(3)修飾靜態方法,作用於所有物件;
(4)修飾類,作用於所有物件。
修飾程式碼塊和方法:
@Slf4j
public class SynchronizedExample1 {
// 修飾一個程式碼塊
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修飾一個方法
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
//一
executorService.execute(() -> {
example1.test1(1);
});
executorService.execute(() -> {
example1.test1(2);
});
//二
executorService.execute(() -> {
example2.test2(1);
});
executorService.execute(() -> {
example2.test2(2);
});
//三
executorService.execute(() -> {
example1.test1(1);
});
executorService.execute(() -> {
example2.test1(2);
});
}
}
執行後可以看到對於情況一,test1內部方法塊作用於example1,先執行完一次0-9輸出,再執行下一次0-9輸出;情況二,同情況一類似,作用於example2;情況三,可以看到交叉執行,test1分別獨立作用於example1和example2,互不影響。
修飾靜態方法和類:
@Slf4j
public class SynchronizedExample2 {
// 修飾一個類
public static void test1(int j) {
synchronized (SynchronizedExample2.class) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
// 修飾一個靜態方法
public static synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
public static void main(String[] args) {
SynchronizedExample2 example1 = new SynchronizedExample2();
SynchronizedExample2 example2 = new SynchronizedExample2();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
example1.test1(1);
});
executorService.execute(() -> {
example2.test1(2);
});
}
}
test1和test2會鎖定呼叫它們的物件所屬的類,同一個時間只有一個物件在執行。
四、可見性---volatile
對於可見性,JVM提供了synchronized和volatile。這裡我們看volatile。
(1)volatile的可見性是通過記憶體屏障和禁止重排序實現的
volatile會在寫操作時,會在寫操作後加一條store屏障指令,將本地記憶體中的共享變數值重新整理到主記憶體:
volatile在進行讀操作時,會在讀操作前加一條load指令,從記憶體中讀取共享變數:
(2)但是volatile不是原子性的,進行++操作不是安全的
@Slf4j
public class VolatileExample {
// 請求總數
public static int clientTotal = 5000;
// 同時併發執行的執行緒數
public static int threadTotal = 200;
public static volatile int count = 0;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count++;
}
}
執行後發現執行緒不安全,原因是執行conut++時分成了三步,第一步是取出當前記憶體count值,這時count值時最新的,接下來執行了兩步操作,分別是+1和重新寫回主存。假設有兩個執行緒同時在執行count++,兩個記憶體都執行了第一步,比如當前count值為5,它們都讀到了,然後兩個執行緒分別執行了+1,並寫回主存,這樣就丟掉了一次加一的操作。(3)volatile適用的場景
既然volatile不適用於計數,那麼volatile適用於哪些場景呢:
1. 對變數的寫操作不依賴於當前值
2. 該變數沒有包含在具有其他變數不變的式子中
因此,volatile適用於狀態標記量:
執行緒1負責初始化,執行緒2不斷查詢inited值,當執行緒1初始化完成後,執行緒2就可以檢測到inited為true了。
五、有序性
有序性是指,在JMM中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。
可以通過volatile、synchronized、lock保證有序性。
另外,JMM具有先天的有序性,即不需要通過任何手段就可以得到保證的有序性。這稱為happens-before原則。
如果兩個操作的執行次序無法從happens-before原則推匯出來,那麼它們就不能保證它們的有序性。虛擬機器可以隨意地對它們進行重排序。
happens-before原則:
1.程式次序規則:在一個單獨的執行緒中,按照程式程式碼書寫的順序執行。
2.鎖定規則:一個unlock操作happen—before後面對同一個鎖的lock操作。
3.volatile變數規則:對一個volatile變數的寫操作happen—before後面對該變數的讀操作。
4.執行緒啟動規則:Thread物件的start()方法happen—before此執行緒的每一個動作。
5.執行緒終止規則:執行緒的所有操作都happen—before對此執行緒的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到執行緒已經終止執行。
6.執行緒中斷規則:對執行緒interrupt()方法的呼叫happen—before發生於被中斷執行緒的程式碼檢測到中斷時事件的發生。
7.物件終結規則:一個物件的初始化完成(建構函式執行結束)happen—before它的finalize()方法的開始。
8.傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那麼可以得出A happen—before操作C。