首先先介紹三個性質 可見性 可見性代表主記憶體中變數更新,線程中可以及時獲得最新的值。 下麵例子證明瞭線程中可見性的問題 由於發現多次執行都要到主記憶體中取變數,所以會將變數緩存到線程的工作記憶體,這樣當其他線程更新該變數的時候,該線程無法得知,導致該線程會無限的運行下去。 public class te ...
首先先介紹三個性質
可見性
可見性代表主記憶體中變數更新,線程中可以及時獲得最新的值。
下麵例子證明瞭線程中可見性的問題
由於發現多次執行都要到主記憶體中取變數,所以會將變數緩存到線程的工作記憶體,這樣當其他線程更新該變數的時候,該線程無法得知,導致該線程會無限的運行下去。
public class test1 {
private static int flag = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (flag == 1){
}
},"t1");
t1.start();
Thread.sleep(1000);
flag = 2;
}
}
疑問
當我們在這個死迴圈中加入一個synchronized關鍵字的話就會將更新
猜測:synchronized會使更新當前線程的工作記憶體
public class test1 { private static int flag = 1; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ while (flag == 1){ synchronized ("1"){ } } },"t1"); t1.start(); Thread.sleep(1000); flag = 2; } }
原子性
即多線程中指令執行會出現交錯,導致數據讀取錯誤。
比如i++的操作就可以在位元組碼的層面可以被看成以下操作
9 getstatic #9 <com/zhf/test3/test2.i : I> 獲得i
12 iconst_1 將1壓入操作數棧
13 isub 將兩數相減
14 putstatic #9 <com/zhf/test3/test2.i : I> 將i變數存儲
然後在多線程的情況下,會出現以下程式出現非0的結果。
public class test2 {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int j = 0; j < 400; j++) {
i++;
}
});
Thread t2 = new Thread(()->{
for (int j = 0; j < 400; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
設計模式---兩階段停止使用volatile
@Slf4j
public class test3 {
private Thread monitor;
private volatile boolean flag = false;
public static void main(String[] args) {
test3 test3 = new test3();
test3.monitor = new Thread(()->{
while (true){
Thread thread = Thread.currentThread();
if (test3.flag){
log.debug("正在執行後續");
break;
}
try {
log.debug("線程正在執行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
// 當進程在睡眠過程中被Interrupte()打斷此時isInterrupted()為false
// 從而當異常被抓住後會繼續執行
// 所以要調用下麵方法繼續將isInterrupted()置為true
// thread.interrupt();
}
}
});
test3.start();
try {
Thread.sleep(5500);
} catch (InterruptedException e) {
e.printStackTrace();
}
test3.stop();
}
public void stop(){
flag = true;
monitor.interrupt();
}
public void start(){
monitor.start();
}
}
設計模式---猶豫模式
具體實現,最常見的就是單例模式。
首先是餓漢模式,這裡的多線程安全是由JVM保證的,對象是在類載入的載入階段創建的。
class SingtonHungry{
private static Object object = new Object();
// 餓漢模式
public synchronized Object getObject() {
return object;
}
}
其次就是餓漢模式,最常見的不過就是下麵的進行多線程安全的方案。文章後面會對其進行優化。
class SingtonLazy{
private Object object;
// 懶漢模式
// 由於這樣的話不管有沒有創建出對象都要加鎖然後才能取對象,性能太差
public synchronized Object getObject() {
if (object == null){
object = new Object();
return object;
}
return object;
}
}
有序性
JVM會對指令進行重排序,其和CPU的流水線操作類似,當需要流水線操作的時候,需要進行優化的時候,就會對CPU指令進行重排序優化。
當操作的順序變了之後,就會出現問題。可能會導致條件的提前觸發等等。
Volatile使用
使用域: Volatile只能在類的靜態成員變數或者成員變數上。
volatile標識符能夠讓線程強制去讀主存的該變數的值,保證了線程變數的可見性。
volatile標識符能夠讓線程去順序執行該變數的操作,保證了執行變數的語句的有序性
-
在讀取該變數時,會為其添加讀屏障。在該讀屏障之後的代碼不會放在讀屏障之前執行。
-
在寫該變數時,會為其添加寫屏障。在該寫屏障之前的代碼不會在屏障之後執行。
所以在volatile的修飾下,能夠保證變數的可見性和有序性,但並不能保證其的原子性。
class SingtonLazy{
// 加上volatile的主要目的就是防止在synchronized內的代碼指令重排,正常是先構造好對象然後賦對象地址
// 導致object會被首先賦予了地址,導致其不為null,然而構造方法還沒有開始構造
// 被其他的線程拿走會出現使用出錯。
private static volatile Object object;
// 懶漢模式
public static Object getObject() {
if (object != null){
return object;
}else{
// 這裡可能出現這裡的線程還沒有為其進行聲明對象,但已經由線程進入了等待鎖
// 所以需要在這裡來一個為空判斷。
synchronized (SingtonLazy.class){
// 這裡可能會出現指令重排,所以要加上volatile
if(object == null){
object = new Object();
}
return object;
}
}
}
}
實現單例的另外一個方式
public class Singleton {
// 當使用到ObjectHolder才會進行到這個靜態內部類的載入,同時才會創建該類
// 也是屬於懶漢式
private static class ObjectHolder{
static final Singleton singleton = new Singleton();
}
}
synchronized補充
首先在synchronized代碼塊中,它會保證代碼塊中的可見性,原子性和有序性。
有序性僅僅是表現在synchronized的執行後最後的結果都是一樣的,並不會阻止JVM在其內部進行代碼的重排序。就比如上個例子來說,在synchronized代碼塊中最後代碼的執行結果都是一樣的,但可能由於其優化,導致其他線程出錯。