在引入volatile、原子類、synchronized前,我們先來(lái)說(shuō)說(shuō)Java內(nèi)存模型的三大特性:可見(jiàn)性、原子性和有序性。
可見(jiàn)性:在多線程中,任一線程的修改對(duì)其它線程都是可見(jiàn)的。(確保可見(jiàn)性的方法有:volatile、synchronized 和 final)
原子性:指的是一個(gè)操作無(wú)法再進(jìn)行分割。(確保原子性的方法有:synchronized、原子類和加lock)
有序性:指代碼按程序員編寫(xiě)的順序串行執(zhí)行,然而在多線程中會(huì)出現(xiàn)指令重排。(確保有序性的方法有: volatile 和 synchronized)
下面,我們舉個(gè)例子對(duì)這多種方案進(jìn)行一一驗(yàn)證:
private static final int THREADS_CONUT = 200;
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREADS_CONUT];
for (int i = 0; i < THREADS_CONUT; i++) {
threads[i] = new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
count++;
//為了讓結(jié)果更加明顯,此處進(jìn)行休眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("執(zhí)行結(jié)果:"+count);
}
這是個(gè)很常見(jiàn)的線程安全問(wèn)題的例子,總共有200個(gè)線程,然后計(jì)數(shù)100次,拋開(kāi)線程安全問(wèn)題的話,總數(shù)應(yīng)該是200*100=20000,然而某一次的運(yùn)行結(jié)果如下:
執(zhí)行結(jié)果:19687
注意到最后的循環(huán)判斷是:Thread.activeCount() > 2
這里為什么是大于2而不是1呢?按理來(lái)說(shuō)應(yīng)該最后只剩下一個(gè)主線程才對(duì)啊?如果我們把最后一部分進(jìn)行如下的更改:
while (Thread.activeCount() > 1) {
System.out.println("當(dāng)前線程數(shù):"+Thread.activeCount());
Thread.currentThread().getThreadGroup().list();
Thread.yield();
}
你會(huì)發(fā)現(xiàn)程序進(jìn)入了死循環(huán)了,不停地循環(huán)打印出如下結(jié)果:
當(dāng)前線程數(shù):2
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Monitor Ctrl-Break,5,main]
其實(shí),從上面的日志我們也可以看出來(lái)了,程序運(yùn)行到最后是包含兩個(gè)線程的:主線程和守護(hù)線程,所以程序才會(huì)陷入無(wú)限循環(huán)中。
再說(shuō)回線程安全的問(wèn)題,其實(shí)主要的原因在于:count++;這一步并不是原子操作,它可拆分為count=count+1,故在多線程中就會(huì)出現(xiàn)上面的線程安全問(wèn)題。
既然我們今天說(shuō)的是volatile、原子類、synchronized,那肯定要進(jìn)行逐一試驗(yàn)的咯:
1、volatile
首先將上述中的count變量用volatile關(guān)鍵字來(lái)進(jìn)行修飾,如下:
private static volatile int count=0;
某次的運(yùn)行結(jié)果如下:
執(zhí)行結(jié)果:19833
事實(shí)證明,volatile關(guān)鍵字并不能確保Java內(nèi)存的原子性,正如上面所說(shuō)的,volatile關(guān)鍵字能確保的是可見(jiàn)性和有序性,下面來(lái)說(shuō)說(shuō)volatile關(guān)鍵字是如何確保可見(jiàn)性和有序性的:
我們先來(lái)看看多線程中Java內(nèi)存模型是怎么樣的?借用網(wǎng)上的一張圖(侵刪):

如上圖,我們知道每個(gè)線程都有自己獨(dú)立的工作內(nèi)存,對(duì)于普通變量,線程是不直接操作主內(nèi)存中的變量,操作的是工作內(nèi)存從主內(nèi)存copy過(guò)來(lái)的副本變量,而使用如果使用了volatile關(guān)鍵字標(biāo)記的變量,則是直接操作主內(nèi)存的變量,因此確保了可見(jiàn)性。
另一方面,使用volatile關(guān)鍵字修飾的變量,會(huì)禁止指令重排序優(yōu)化,禁止將后面的指令排到該變量前,相當(dāng)于建立了一個(gè)“內(nèi)存屏障”,以此來(lái)確保有序性。
性能方面,volatile 的讀性能消耗與普通變量幾乎相同,但是寫(xiě)操作稍慢,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來(lái)保證處理器不發(fā)生亂序執(zhí)行。
2、原子類
Java 5.0 提供了 java.util.concurrent(簡(jiǎn)稱JUC)包,根據(jù)修改的數(shù)據(jù)類型,可以將JUC包中的原子操作類可以分為4類。
- 基本類型: AtomicInteger, AtomicLong, AtomicBoolean ;
- 數(shù)組類型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
- 引用類型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
- 對(duì)象的屬性修改類型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
還是回到上面的例子,我們來(lái)看看原子類能否解決上面出現(xiàn)的線程安全問(wèn)題,將代碼更改為如下:
private static final int THREADS_CONUT = 200;
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREADS_CONUT];
for (int i = 0; i < THREADS_CONUT; i++) {
threads[i] = new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
count.incrementAndGet();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("執(zhí)行結(jié)果:"+count);
}
執(zhí)行結(jié)果:20000
事實(shí)證明,“原子類”這個(gè)名字可不是白叫的,果然能保證其原子性。
那么,原子類實(shí)現(xiàn)的原理又是什么呢?其實(shí),原子類是基于CAS實(shí)現(xiàn),而CAS是通過(guò)硬件命令保證了原子性,所以在性能方面有一定的優(yōu)勢(shì)。
CAS是一種“樂(lè)觀鎖”技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能更新變量的值,而其它線程都失敗,失敗的線程并不會(huì)被掛起,而是被告知這次競(jìng)爭(zhēng)中失敗,并可以再次嘗試。
CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則返回V。
CAS存在著ABA問(wèn)題:在更新前的值是A,但在操作過(guò)程中被其他線程更新為B,又更新為 A。此時(shí),按照上面的規(guī)則,當(dāng)前線程判斷可執(zhí)行,但其實(shí)發(fā)生了不一致現(xiàn)象,需評(píng)估是否對(duì)程序存在影響(極少極少)。
3、synchronized
與“樂(lè)觀鎖”相對(duì)應(yīng)的是“悲觀鎖”,而synchronized就是一種悲觀鎖技術(shù)。
樂(lè)觀鎖相信的是在它修改之前,沒(méi)有其它線程去修改它。
悲觀鎖則認(rèn)為在它修改之前,一定會(huì)有其它線程去修改它,悲觀鎖效率很低。
同樣的,我們使用synchronized再次更改上面的例子:
private static final int THREADS_CONUT = 200;
private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREADS_CONUT];
for (int i = 0; i < THREADS_CONUT; i++) {
threads[i] = new Thread(() -> {
for (int i1 = 0; i1 < 100; i1++) {
synchronized (threads){
count++;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(count);
}
執(zhí)行結(jié)果:20000
正如最開(kāi)始說(shuō)的,使用synchronized可以保證Java內(nèi)存的三大特性,它是一種悲觀鎖,它只有在確保其它線程不會(huì)造成干擾的情況下執(zhí)行,會(huì)導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
所以,如果如上面的例子的話,使用原子類會(huì)比synchronized在性能上有更大的優(yōu)勢(shì)!