volatile关键字的作用:保证变量的可见性
一定要谨记
volatile
关键字在 Java 代码中仅仅保证这个变量是可见的:它不保证原子性。在那些非原子且可由多个线程访问的易变操作中,一定不能够依赖于 volatile 的同步机制。相反,要使用java.util.concurrent
包的同步语句、锁类和原子类。它们在设计上能够保证程序是线程安全的。
volatile作用的具体描述:
Java如何保证可见性
Java提供了
volatile
关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。引自- wangzzu的博客文章
上面这段话能十分清楚的说明这个变量的用处了吧。下面配合代码更加准确的验证这个问题。
代码引自-- 阿里工程师oldratlee的github
Demo类com.oldratlee.fucking.concurrency.NoPublishDemo。
Demo说明
主线程中设置属性stop
为true
,以控制在main
启动的任务线程退出。
问题说明
在主线程属性stop
为true
后,但任务线程持续运行,即任务线程中一直没有读到新值。
出现上述问题说明的代码:
public class NoPublishDemo {
boolean stop = false;
public static void main(String[] args) {
// LoadMaker.makeLoad();
NoPublishDemo demo = new NoPublishDemo();
Thread thread = new Thread(demo.getConcurrencyCheckTask());
thread.start();
Utils.sleep(1000);
System.out.println("Set stop to true in main!");
demo.stop = true;
System.out.println("Exit main.");
}
ConcurrencyCheckTask getConcurrencyCheckTask() {
return new ConcurrencyCheckTask();
}
private class ConcurrencyCheckTask implements Runnable {
@Override
public void run() {
System.out.println("ConcurrencyCheckTask started!");
// 如果主线中stop的值可见,则循环会退出。
// 在我的开发机上,几乎必现循环不退出!(简单安全的解法:在running属性上加上volatile)
while (!stop) {
}
System.out.println("ConcurrencyCheckTask stopped!");
}
}
}
例子中主线程的stop变量没有做任何的显示定义volatile。
当我们执行后的打印结果为:
ConcurrencyCheckTask started!
Set stop to true in main!
Exit main.
并没有打印
ConcurrencyCheckTask stopped!
并且程序还没有结束。
接下来我们对stop变量添加volatile
volatile boolean stop = false;
程序完美结束。也印证了上文描述中的内容。
组合状态读到无效组合
--此时使用volatile则无法解决同时修改两个值,数据还是返回正确的问题。
意思是同时对两个值修改,但是实际返回确实错误的。
程序设计时,会需要多个状态记录(状态可以是个POJO
对象或是int
等等)。常看到多状态读写没有同步的代码,并且写的同学会很自然地就忽略了线程安全的问题。
无效组合 是指 从来没有设置过的组合。
Demo说明
主线程修改多个状态,为了方便检查,每次写入有个固定的关系:第2个状态是第1个状态值的2倍。在任务线程中读取多个状态。
Demo类com.oldratlee.fucking.concurrency.InvalidCombinationStateDemo。
问题说明
任务线程中读到了 第2个状态不是第1个状态值2倍的值,即是无效值。
快速运行
mvn compile exec:java -Dexec.mainClass=com.oldratlee.fucking.concurrency.InvalidCombinationStateDemo
实际测试代码
public class InvalidCombinationStateDemo {
public static void main(String[] args) {
CombinationStatTask task = new CombinationStatTask();
Thread thread = new Thread(task);
thread.start();
Random random = new Random();
while (true) {
int rand = random.nextInt(1000);
task.state1 = rand;
task.state2 = rand * 2;
}
}
private static class CombinationStatTask implements Runnable {
// 对于组合状态,加 volatile 不能解决问题
volatile int state1;
volatile int state2;
@Override
public void run() {
int c = 0;
for (long i = 0; ; i++) {
int i1 = state1;
int i2 = state2;
if (i1 * 2 != i2) {
c++;
System.err.printf("Fuck! Got invalid CombinationStat!! check time=%s, happen time=%s(%s%%), count value=%s|%s\n",
i + 1, c, (float) c / (i + 1) * 100, i1, i2);
} else {
// 如果去掉这个输出,则在我的开发机上,发生无效组合的概率由 ~5% 降到 ~0.1%
System.out.printf("Emm... %s|%s\n", i1, i2);
}
}
}
}
}
long
变量读到无效值
无效值 是指 从来没有设置过的值。
long
变量读写不是原子的,会分为2次4字节操作。
Demo类com.oldratlee.fucking.concurrency.InvalidLongDemo。
Demo说明
主线程修改long
变量,为了方便检查,每次写入的long
值的高4字节和低4字节是一样的。在任务线程中读取long
变量。
问题说明
任务线程中读到了高4字节和低4字节不一样的long
变量,即是无效值。
快速运行
mvn compile exec:java -Dexec.mainClass=com.oldratlee.fucking.concurrency.InvalidLongDemo
DEMO:
public class InvalidLongDemo {
long count = 0;
public static void main(String[] args) {
// LoadMaker.makeLoad();
InvalidLongDemo demo = new InvalidLongDemo();
Thread thread = new Thread(demo.getConcurrencyCheckTask());
thread.start();
for (int i = 0; ; i++) {
long l = i;
demo.count = l << 32 | l;
}
}
ConcurrencyCheckTask getConcurrencyCheckTask() {
return new ConcurrencyCheckTask();
}
private class ConcurrencyCheckTask implements Runnable {
@Override
public void run() {
int c = 0;
for (int i = 0; ; i++) {
long l = count;
long high = l >>> 32;
long low = l & 0xFFFFFFFFL;
if (high != low) {
c++;
System.err.printf("Fuck! Got invalid long!! check time=%s, happen time=%s(%s%%), count value=%s|%s\n",
i + 1, c, (float) c / (i + 1) * 100, high, low);
} else {
// 如果去掉这个输出,则在我的开发机上没有观察到invalid long
System.out.printf("Emm... %s|%s\n", high, low);
}
}
}
}
}
对于单个变量的值修改 给变量添加volatile也是可以解决此问题的。