1.什么是volatile?
volatile是JAVA虚拟机提供的轻量级的同步机制。
2.volatile的特性
## 2.1 可见性
volatile的可见性是多线程同步之间的一种通讯机制,在JMM中
规定共享变量(实例域、静态域、数组元素)放在主内存中,非共享
变量(局部变量、方法定义参数和异常处理器参数)放在每个线程
自己的工作内存,每个线程在使用共享变量时会将其拷贝到自己的
工作内存进行操作,但操作完成后不知道何时将其写回主内存,此
时主内存的值未发生改变,其他线程嗅探到主内存的值未发生改变,
导致其他线程操作了脏数据”。
class MyThread extends Thread{
int number =0;
@Override
public void run() {
System.out.println("我的线程开始执行");
while (true) {
/**
*当嗅探到number的值被改变时跳出循环
*未使用volatile修饰number,
*此时main线程修改了number的值,
*但是对于我的线程并不可见,所以会死循环
*/
if (number != 0) {
break;
}
}
System.out.println("我的线程执行结束");
}
}
/**
* volatile实例类
* number未使用volatile修饰,多线程之间不具有可见性
*/
public class VolatileDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();//创建我的线程
myThread.start();
/**
* 为防止主线程跑在 我的线程 之前,
* 即防止我的线程还未开始时主线程已经执行完毕,将number的值该为100后我的线程才跑
*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在程序执行1秒后这段话会被执行,给 我的线程 足够的启动时间
myThread.number=100;
System.out.println(Thread.currentThread().getName()+" number="+myThread.number);
}
}
但对于加了volatile关键字的变量进行操作,JVM会向处理器发
送一条Lock前缀的指令,将这个变量所在的缓存行的数据写回主
内存,其他线程会嗅探在总线上的数据来检查自己的数据是否过期
当发现自己的缓存行对应的内存地址被改变就会将当前线程中的
变量置为无效,当线程需要对该变量进行操作时,会重新从主内存
中读取变量最新的值,在进行操作,这样就实现了多线程之间的通
信。
class MyThread extends Thread{
volatile int number =0;
@Override
public void run() {
System.out.println("我的线程开始执行");
while (true) {
/**
* 当嗅探到number的值被改变时跳出循环
* number被volatile修饰,
* 所以当main线程修改了number的值时
* 会及时通知 我的线程,我的线程从主内存
* 中获取最新的值后跳出循环
*/
if (number != 0) {
break;
}
}
System.out.println("我的线程执行结束");
}
}
/**
* volatile实例类
* number使用volatile修饰,多线程之间具有可见性,实现多线程间的通信
*/
public class VolatileDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();//创建我的线程
myThread.start();
/**
* 为防止主线程跑在 我的线程 之前,
* 即防止我的线程还未开始时主线程已经执行完毕,将number的值该为100后我的线程才跑
*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在程序执行1秒后这段话会被执行,给 我的线程 足够的启动时间
myThread.number=100;
System.out.println(Thread.currentThread().getName()+" number="+myThread.number);
}
}
## 2.2 禁止指令重排序
指令重排序是指JAVA语言规定JVM线程内部维持顺序化语义(满足
As-If-Serial语义和Happens-Before原则),即只要程序执
行的最终结果与其顺序化执行的结果相同,JVM允许指令执行的顺
序与代码逻辑书写的顺序可以不一致,这样的过程叫做指令重排序。
指令重排序的意义是指令更加符合CPU执行的特性,提高执行效率。
但是在多线程中某些时候指令重排序会让程序出现预期之外的结
果(如DCL中会造成线程不安全),所以我们有时候需要禁止JVM做
这种指令重排序的优化。
volatile通过加入内存屏障防止这种指令重排,用volatile修
饰的变量在写操作时在volatile写的前后分别加入StoreStore
屏障和StoreLoad屏障防止写操作时发生指令重排,在volatile
读操作之后加入LoadLoad屏障和LoadStore屏障防止读操作时
发生指令重排序。
## 2.3 不保证原子性
原子性是指该操作是一个完整的整体,是不可被分割的,同一时
刻只能有一个线程对它进行操作。
被volatile修饰的对象操作不能保证原子性,即该操作会因为
线程调度器而被中断操作。
class MyData{
volatile int number=0;
//该方法会使number+1
public void numberPlusPlus(){
number++;
}
}
public class VolatileByAtomicDemo {
public static void main(String[] args) {
MyData myData = new MyData();
/**
* 循环创建10000个线程,都调用numberPlusPlus使number+1
*/
for (int i = 1; i <= 10; i++) {
new Thread(()->{
//双重for为了增加线程被抢占的概率
for (int j = 1; j <= 1000; j++) {
myData.numberPlusPlus();
}
},String.valueOf(i)).start();
}
/**
* java运行时会自动调起main线程和GC线程
* Thread.activeCount()>2说明自己创建的线程还有未执行结束的,
* 等待所有的自创建线程执行结束
*/
while(Thread.activeCount() > 2){
Thread.yield();
}
/**
* 预期在循环结束后numberPlusPlus被调用10000次,该语句打印10000
* 实际结果小于10000(有可能等于10000),原因时number++在执行时被拆分为3步
* 1.读取number的值 2.执行number+1 3.将操作结果写回
* 因为无法保证原子性,a线程在执行number++的第二步后,b线程抢占了资源
* 此时a线程已经进行了+1操作但是因为b此时加塞抢占,a线程挂起,并未将值写回
* b线程读取到的值与a线程相同,b线程执行完number++操作后释放资源,
* a线程从挂起状态变为执行,但因为挂起前a线程只剩下写回操作,执行后直接写回,
* 导致b线程的操作被覆盖,丢失操作数据
*/
System.out.println(myData.number);
}
}
如果想要保证原子性则需要在number++时加上synchronized
或者int类型的number换为能够保证原子性的AtomicInteger
类型变量。