今天又是摸鱼的一天,在群里闲聊的时候突然有位群友题了个问题:
群友们反应很快,一下子就解决了没有加关键字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呢?
我看到了下面这篇文章:
线程的缓存何时刷新? - 简书 (jianshu.com)
在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关键字