复习内存模型

Volatile特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
保证可见性
public class VolatileTest {
public static void main(String[] args) {
test01();
}
/**
* volatile可见性测试
* todo 不加Thread.sleep(1000)会直接同步回主内存,待研究;
*/
private static void test01() {
Data data = new Data();
new Thread(() -> {
System.out.println("come in");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setI(100);
System.out.println("update complete");
}, "t1").start();
while (data.i == 0) {
}
System.out.println("data = " + data.i);
}
}
class Data {
int i = 0;
// volatile int i = 0;
void setI(int i) {
this.i = i;
}
void addOne() {
i++;
}
}
输出结果

由输出结果可见,main线程已经陷入了死循环,没有察觉到被data.i的值已经被线程t1修改了
将data的i值加上volatile后
class Data {
volatile int i = 0;
void setI(int i) {
this.i = i;
}
}

由输出结果可见,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被其他线程感知到,其他线程的当前值作废,重新从主内存中获取值,这就叫可见性。
不保证原子性
package com.hcq.test.juc;
/**
* @author : hcq
* @date : 2019/8/14
*/
public class VolatileTest {
public static void main(String[] args) {
test02();
}
/**
* volatile原子性测试
*/
private static void test02() {
Data data = new Data();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
data.addOne();
}
}).start();
}
// 默认有 main 线程和 gc 线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(data.i);
}
}
class Data {
volatile int i = 0;
void addOne() {
i++;
}
}
输出结果
19926
Process finished with exit code 0
经多次测试发现,我们的期望值是20000,但是都不会达到2W,这是因为i++他在系统底层其实是三个语句
public void addOne();
addOne:
0: getstatic #2 // Field count:I 获取值
3: iconst_1
4: iadd // 值增加
5: putstatic #2 // Field count:I 值保存
8: return
}
可以用synchronized跟atomic原子变量解决。
禁止指令重排
计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种
- 编译器优化的重排
- 指令并行的重排
- 内存系统的重排
单线程环境里面确保程序最终执行的结果和代码执行的结果一致
处理器在进行重排序时必须考虑指令之间的数据依赖性
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01() {
a = 1; // flag = true;
// ----线程切换----
flag = true; // a = 1;
}
public void method02() {
if (flag) {
a = a + 3;
System.out.println("a = " + a);
}
}
}
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果无法预测
由于method01的两条语句a = 1; flag = true; 没有依赖性,CPU有可能会重排(flag = true,a = 1;),调换了位置,此时如果另一个线程在执行method02,在flag = true执行完,a=1还没执行的时候插了进来,就会导致获取的值为0,输出3;
volatile应用单例实例
package com.hcq.test.juc;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Singleton01 {
private static Singleton01 instance = null;
private Singleton01() {
System.out.println(Thread.currentThread().getName() + " construction...");
}
public static Singleton01 getInstance() {
if (instance == null) {
instance = new Singleton01();
}
return instance;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.execute(Singleton01::getInstance);
}
executorService.shutdown();
}
}
输出结果
Connected to the target VM, address: '127.0.0.1:59395', transport: 'socket'
pool-1-thread-3 construction...
pool-1-thread-1 construction...
pool-1-thread-2 construction...
Disconnected from the target VM, address: '127.0.0.1:59395', transport: 'socket'
这个单例因为可见性问题会导致多次初始化
解决方法之一: 双重锁,
package com.hcq.test.juc;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Singleton02 {
private static volatile Singleton02 instance = null;
private Singleton02() {
System.out.println(Thread.currentThread().getName() + " construction...");
}
public static Singleton02 getInstance() {
if (instance == null) {
synchronized (Singleton01.class) {
if (instance == null) {
instance = new Singleton02();
}
}
}
return instance;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(Singleton02::getInstance);
}
executorService.shutdown();
}
}
memory = allocate(); // 1.分配对象空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null
为什么要用双层锁,单单用volatile不行吗。
网友评论