JMM中主要是圍繞并發(fā)過程中如何處理原子性,可見性和有序性三個特性來建立的。最終可以保證線程安全性尊浓,volatile和synchronized兩個關鍵字又是我們最常碰到與最容易提到的關鍵字荔泳,這次放在一起來講。
線程安全性:當多個線程訪問某個類的時候占婉,不管運行環(huán)境采用何種調度方式
或這些線程如何交替執(zhí)行,并且在主調代碼中不需要額外的同步或協(xié)同
,這個類都能表現(xiàn)出正確的行為
淫半,那么就稱這個類是線程安全的。
原子性匣砖、可見性與有序性
首先來看一下這幾個特性代表的具體含義科吭。
-
原子性(Atomicity):原子性是指,一個操作是不可中斷的猴鲫。即使是多個線程一起執(zhí)行的時候对人,一個操作一旦開始,就不會被其他線程干擾拂共。
JDK的包中提供了專門的原子包
java.util.concurrent.atomic
牺弄,synchronized關鍵字還有Lock來讓程序在并發(fā)環(huán)境下具有原子性的特點。 -
可見性(Visibility):可見性是指當一個線程修改了共享變量的值宜狐,其它線程能立即得知這個修改势告。
volatile,synchronized和final關鍵字能實現(xiàn)可見性。使用final關鍵字需要注意
對象逃逸
-
有序性:如果再本線程內觀察抚恒,所有操作都是有序的咱台,如果再一個線程中觀察另外一個線程,那么所有操作都是無序的俭驮。前半句是指“線程內表現(xiàn)為串行”回溺,后半句是指“指令重排序”現(xiàn)象和“工作內存與主內存同步延遲現(xiàn)象”
volatile和synchronized關鍵字可以線程之間操作的有序性。
Volatile
一個變量定義為volatile之后,它將具有兩種特性:
- 保證次變了對所有線程的可見性馅而,一條線程修改了這個值祥诽,新值對其它線程是可以立即得知的。
- 禁止指令重排優(yōu)化瓮恭。
volatile變量在寫操作時候雄坪,會在寫操作后加上store屏障指令,將本地內存刷新到主內存屯蹦。
volatile變量讀操作的時候维哈,會在讀操作之前加入一條load屏障指令,從主內存中讀取共享變量登澜。
關于JMM的8大操作指令阔挠,可以查看我的上篇文章,java內存模型脑蠕。
volatile變量為什么在并發(fā)下不安全购撼?
volatile變量在各個線程的工作內存中也可以存在不一致的情況,但由于每次使用之前都要刷新谴仙,執(zhí)行引擎看不到不一致的情況迂求,因此可以認為不存在一致性問題,但是Java里面的運算并非原子操作晃跺。
假如說一個寫入值操作不需要依賴依賴這個值的原先值揩局,那么在進行寫入的時候我們就不需要進行讀取操作。
寫入操作對原本的值的時候沒有要求掀虎,那么所有線程都可以寫入新的值凌盯,雖然讀取到的值是相同的,每個線程的操作也是正確的烹玉,但是最終結果卻是錯誤的驰怎。
感興趣的可以運行如下代碼:
public class VolatileTest {
public static volatile int count = 0;
public static final int THREAD_COUNT = 20;
public static void add(){
count++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
});
threads[i].start();
}
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i].join();
}
System.out.println(count);
}
}
// 如果并發(fā)正確的話:應該是20000,但是每次運行結果都不到20000
Volatile適合做什么春霍?
適合做標量砸西,當一個線程對某個變量進行讀寫操作,而其它線程僅僅進行讀操作的時候址儒,是可以保證volatile的正確性的。如下:
volatile bool stopped;
public void stop(){
stopped = true
}
while(!stoppped){
// 執(zhí)行操作
}
Synchronized
Synchronized保證了原子性衅疙,可見性與有序性莲趣,它的工作時對同步的代碼塊加鎖,使得每次只有一個線程進入代碼塊饱溢,從而保證線程安全喧伞。synchronized反應到字節(jié)碼層面就是monitorenter與monitorexit.
注意*:雖然synchonized關鍵字看起來是萬能的,能保證線程安全性,但是越萬能的控制往往越伴隨著越大的性能影響潘鲫。
Synchonzied用法
- 實例方法上翁逞,被修飾的方法稱為同步方法,其作用的范圍是整個方法溉仑,作用的對象是調用這個方法的對象挖函;
- 靜態(tài)方法上,其作用的范圍是整個靜態(tài)方法浊竟,作用的對象是這個類的所有對象怨喘;
- 實例方法
代碼塊
. - 靜態(tài)方法代碼塊。
//實例方法
public synchronized void add(int value){
this.count += value;
}
//靜態(tài)方法
public static synchronized void add(int value){
count += value;
}
//實例方法代碼塊
public void add(int value){
synchronized(this){
this.count += value;
}
}
//靜態(tài)方法代碼塊
public class MyClass {
public static synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public static void log2(String msg1, String msg2){
synchronized(MyClass.class){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
Synchonzied案例
public class SynchronziedTest implements Runnable{
static int i = 0;
static int j = 0;
static SynchronziedTest instance= new SynchronziedTest();
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
}
public synchronized void increase(){
i++;
}
public static void main(String[] args) throws InterruptedException {
// 注意新建的線程指向的同一個實例振定,
// 如果指向不同的實例必怜,那么兩個線程關注的鎖就不是同一把鎖,就會導致線程不安全
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
//錯誤的用法
// Thread t3 = new Thread(new SynchronziedTest());
// Thread t4 = new Thread(new SynchronziedTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
//結果為:200000
注意創(chuàng)建線程的時候指向同一個實例后频,才會鎖住相同的對象梳庆。
最后
這次我們講了線程安全性的基本原則,然后解釋了volatile和synchronized關鍵字卑惜,多線程中不得不掌握的關鍵字靠益。
參考
- 《實戰(zhàn)Java高并發(fā)設計》
- 《深入理解JVM虛擬機》
- 《Java并發(fā)編程與高并發(fā)解決方案》