1.volatile是Java虚拟机提供的轻量级的同步机制
① 保证内存可见性
②不能保证原子性
③禁止指令重排
2.谈到volatile就得说说JMM(Java memory model)Java内存模型
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
2.1可见性
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.如下代码:
import java.util.concurrent.TimeUnit;
//简单的Volatile的可见性Demo
class MyData {
int t = 0;
public void addTo() {
t = 10;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData data = new MyData();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.addTo();
System.out.println(Thread.currentThread().getName() + "t的值为:" + data.t);
}).start();
//main线程拿到的t值并不是最新的值,此时将进入死循环
while (data.t == 0){
}
//并不会执行
System.out.println(Thread.currentThread().getName() + "t的值为:" + data.t);
}
}
运行程序并不会结束,如图所示:
在上面程序中的资源类MyData中的实例变量t用volatile修饰后可以解决内存可见性问题,如图:
2.2原子性
Java多线程环境下的原子操作是相对访问共享变量操作而言的,原子操作是一个整体不可分割的,原子性表现为:
①访问某个共享变量的操作从执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,其他线程不会“看到”该操作执行了部分的中间效果;
②访问同一组共享变量的原子操作是不能够被交错的。
举例:i++就是原子性操作,有字节码可知i++被拆分成3个指令操作,拿到原值,原值加1,再将完成累加后的值写回。在多线程下i++是非线程安全的在不加synchronized的情况下可以使用java.util.concurrent.atomic包提供的AtomicInteger 解决:
import java.util.concurrent.atomic.AtomicInteger;
class MyData {
volatile int t = 0;
public void addTo() {
t++;
}
//如果不指定构造器中的参数那么默认就是0
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
//默认加1
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData data = new MyData();
//创建20个线程
for (int i = 1; i <= 20; i++) {
new Thread(()->{
//每个线程调用data.addTo(),data.addAtomic()各一千次
for (int j = 1; j <= 1000; j++) {
data.addTo();
data.addAtomic();
}
}).start();
}
//判断当前活动的线程数量
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " t的值为: " + data.t);
System.out.println(Thread.currentThread().getName() + " atomicInteger的值为: " + data.atomicInteger);
}
}
以上代码解决了可见性问题,但MyData中的addTo()方法并没有解决i++的原子性问题,例如线程A拿到atomicInteger的值为1在工作内存中加了1刚要写入主内存中,此时线程B将资源抢了过来线程1被挂起线程2修改atomicInteger的值为2并写入到了主内存中,然后JVM线程调度器再次调度线程1运行,并将修改后的值2写入了主内存中,但要注意此时atomicInteger的值要比实际的值3要小,所以控制台输出的第一行i总是小于20000,而addAtomic()解决了原子性问题,线程A在操作资源atomicInteger时必须要将修改后的值写入到主内存中后,线程B才能修改atomicInteger的值并写入主内存中,所以控制台输出的第二行总为20000。
2.3指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3中:
源代码->编译器指令重排->指令并行的重排->内存系统的重排->最终执行的指令
单线程执行中程序的执行结果和代码顺序执行的结果总是一致的。处理器在进行重新排序是必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测。
volatile实现禁止指令重排优化,从而避免多线程情况下的程序乱序执行的现象