目录
1、volatile是java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
2、JMM内存模型之可见性
- 可见性
- 原子性
- 有序性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
3、可见性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
4、可见性代码验证
public class VisibilityTest {
//
volatile int number = 0;
//int number = 0;
public void addTo60() {
this.number = 60;
}
public static void main(String[] args) {
VisibilityTest myData = new VisibilityTest();
// AAA线程 实现了Runnable接口的,lambda表达式
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 线程睡眠3秒,假设在进行运算
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改number的值
myData.addTo60();
// 输出修改后的值
System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);
}, "AAA").start();
// main线程就一直在这里等待循环,直到number的值不等于零
while (myData.number == 0) {
}
// 按道理这个值是不可能打印出来的,因为主线程运行的时候,number的值为0,所以一直在循环
// 如果能输出这句话,说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
System.out.println(Thread.currentThread().getName() + "\t mission is over");
}
}
number属性不添加volatile字段运行效果如下图,项目一直处于运行昨天,说明main方法没有感知到AAA线程写到主线程,说明线程之前是不可见的。
number属性添加volatile字段之后运行效果如图,项目运行3秒之后介绍,main方法顺利感知到AAA线程对number的改动,说明变量在线程间是可见的。
5、volatile不保证原子性代码
public class VisibilityTest {
//number 添加了volatile
volatile int number1 = 0;
//number 没有添加volatile关键字
int number = 0;
public void addPlusPlus() {
number++;
}
public void addPlusPlus1() {
number1++;
}
public static void main(String[] args) {
VisibilityTest myData = new VisibilityTest();
// 创建10个线程,线程里面进行1000次循环
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面分别调用addPlusPlus和addPlusPlus1方法
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addPlusPlus1();
}
}, String.valueOf(i)).start();
}
// 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
// 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
while(Thread.activeCount() > 2) {
// yield表示不执行
Thread.yield();
}
// 查看最终的值
// 假设volatile保证原子性,那么输出的值应该为: 20 * 1000 = 20000
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number1);
}
}
如图,上面代码中变量number和number1一个被关键字修饰一个不被关键字修饰,如果遵从原子性的话,得到的结果应该是20000,但是运行代码发现两个都不是20000,说明两者都不具备原子性
当我们对对象myData添加synchronized锁的时候,是可以实现原子性的,这里只是演示一下
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 里面
synchronized (myData){
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addPlusPlus1();
}}
}, String.valueOf(i)).start();
}
但是要解决原子性的问题我们也可以使用JUC中的AtomicInteger,因为AtomicInteger本来就是JDK提供的具有原子性的Integer
AtomicInteger number1 = new AtomicInteger(0);
public void addPlusPlus() {
//也可以使用其他方法 比如 getAndAdd
number1.addAndGet(1);
}
结果:
6、指令重排序
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一版分为一下3种
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
处理器在处理重排序时必须考虑指令之前的数据依赖性
多线程环境中交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保持一致性是无法确定的,结果无法预测。