今天又是摸魚的一天,在群里閑聊的時候突然有位群友題了個問題: ![](https://img2023.cnblogs.com/blog/2696704/202305/2696704-20230522233309409-1620806525.png) 群友們反應很快,一下子就解決了沒有加關鍵字vola ...
今天又是摸魚的一天,在群里閑聊的時候突然有位群友題了個問題:
群友們反應很快,一下子就解決了沒有加關鍵字volatile,不能保證可見性。所以迴圈線程中的kaiguan變數一直讀取的是線程緩存中的值,而線程緩存的值一直無法讀取到主記憶體中的值。
當然這個問題難不倒這位提問的群友,所以他又提問了。
除了加volatile之外,還有沒有其他方式?
正好閑著無聊,我便開始試著解決這個問題。
public class test2 {
private static boolean kaiguan =true;
private static void go(){
new Thread(()->{
while (kaiguan){
}
System.out.println("事情做完了");
}).start();
new Thread(()->{
try {
Thread.sleep(1000*3);
kaiguan=false;
}catch (InterruptedException e){
e.printStackTrace();
}
}).start();
}
public static void main(String[] args) {
go();
}
}
首先我問了ChatGPT
如果不使用 volatile
關鍵字,也可以使用其他的多線程機制來保證可見性,例如使用 AtomicBoolean
類,這個類提供了 get()
和 set()
方法,可以保證操作的原子性和可見性。
修改後的代碼如下所示:
private static AtomicBoolean kaiguan = new AtomicBoolean(true);
private static void go() {
new Thread(() -> {
while (kaiguan.get()) {
Thread.yield(); // 或者使用Thread.sleep(),暫停線程一段時間
}
System.out.println("事情做完了");
}).start();
new Thread(() -> {
try {
Thread.sleep(1000 * 3);
kaiguan.set(false); // 設置kaiguan變數為false,退出迴圈
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
public static void main(String[] args) {
go();
}
在上述代碼中,我們使用了 AtomicBoolean
類來代替原來的 boolean
類型的 kaiguan
變數,併在 while
迴圈中使用了 kaiguan.get()
方法來獲取 kaiguan
變數的值,從而保證可見性。在另一個線程中,我們使用 kaiguan.set(false)
方法將 kaiguan
變數設置為 false
,從而退出迴圈。
可以看到 除了使用volatile
來聲明變數,我們還可以用AtomicBoolean
來聲明kaiguan。同樣能保證可見性。
那如果不用AtomicBoolean
聲明kaiguan呢?
我看到了下麵這篇文章:
在Doug Lea大神的Concurrent Programming in Java一書中有這樣一個片段來描述synchronized
這個關鍵字:
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.
簡單翻譯一下:從本質上來說,當線程釋放一個鎖時會強制性的將工作記憶體中之前所有的寫操作都刷新到主記憶體中去,而獲取一個鎖則會強制性的載入可訪問到的值到線程工作記憶體中來。雖然鎖操作只對同步方法和同步代碼塊這一塊起到作用,但是影響的卻是線程執行操作所使用的所有欄位。
也就是說我們可以用加鎖來解決線程刷新這個問題。
所以我們可以手動加上System.out.println();來退出該迴圈。
因為System.out.println();底層是加鎖的
public void println() {
newLine();
}
private void newLine() {
try {
synchronized (this) {
ensureOpen();
textOut.newLine();
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush)
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
我們在看看chatgpt的回答,既然裡面有提到yield()與sleep(),那麼我們就試試
Thread.yield();
與
Thread.sleep(0);
發現果然成功跳出迴圈,那麼yield()與sleep(0)到底發生了什麼導致緩存刷新呢?
沒錯就是上下文切換!
yield()與sleep(0)會導致上下文切換,從而導致緩存失效,從而拉去主記憶體中的新值。
當然我們也可以直接使用Unsafe方法中的loadFence()方法。
使用UnsafeFactory.getUnsafe().loadFence();也同樣可以跳出迴圈,因為loadFence: 可以保證在這個屏障之前的所有讀操作都已經完成。
Unsafe需要我們通過反射獲取,直接調用會報錯:
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
在尋找其他答案的過程中我還發現final關鍵字同樣也有刷新緩存的作用
首先自定義一個類,將其中的num設置為final
public class TestTempOne {
final int num; //設為final;
TestTempOne(int num){
this.num=num;
}
}
private static void go(){
new Thread(()->{
while (kaiguan){
// new TestTempOne(0); //跳出迴圈
// new String("a"); //跳出迴圈
// new Integer(0); //死迴圈
// new Integer(129); //死迴圈
new Integer(100000000); //死迴圈
}
可以看到最終會跳出迴圈,但是有個問題Integer中value的值同樣也是final。但卻不能刷新緩存。而String則是一個final char數組,也可以跳出迴圈。目前沒有找到答案,如果有大佬知道答案,請告知我一下!
總結:我目前知道的有六種:
1、使用volatile
2、使用synchronized或者Lock
3、使用AtomicBoolean
4、使用UnsafeFactory.getUnsafe().loadFence();
5、使用yield()與sleep()
6、final關鍵字