volatile保证变量的可见性
可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。
volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
什么意思呢?意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,这就保证了可见性
volatile可见性测试
不加volatile
线程1start之后一直空转,陷入死循环,因为线程1读取的一直是自己内存的副本数据,线程1并不知道要去主内存拉取新数据
public class Main extends Thread {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("start");
while (flag) {
}
System.out.println("end");
}, "线程1").start();
TimeUnit.SECONDS.sleep(3);
flag = false;
}
}
加上volatile
volatile关键字的修饰 使得一旦flag被修改,将会通知其他副本内存同步新的flag,while即可以在一定时间内结束
public class Main extends Thread {
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("start");
while (flag) {
}
System.out.println("end");
}, "线程1").start();
TimeUnit.SECONDS.sleep(3);
flag = false;
}
}
奇怪的事情
不加volatile ,按理说会一直while循环,但是在while里面加上一些代码之后,程序也会在一定时间之后结束。我并不知道问什么!
猜想可能是while循环有代码时,内存数据切换比较频繁,导致数据同步频率升高,这样的话就能读到新数据了
public class Main extends Thread {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("start");
while (flag) {
System.out.println("beauty");
}
System.out.println("end");
}, "线程1").start();
TimeUnit.SECONDS.sleep(3);
flag = false;
}
}
why
println方法中, 使用到了synchronized
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
volatile屏蔽指令重排序
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序
双重锁的volatile作用
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { // 1
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // 2
}
}
}
return singleton;
}
}
实际上当程序执行到2处的时候,如果我们没有使用volatile关键字修饰变量singleton,就可能会造成错误。这是因为使用new关键字初始化一个对象的过程并不是一个原子的操作,它分成下面三个步骤进行:
a. 给 singleton 分配内存
b. 调用 Singleton 的构造函数来初始化成员变量
c. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)
如果虚拟机存在指令重排序优化,则步骤b和c的顺序是无法确定的。如果A线程率先进入同步代码块并先执行了c而没有执行b,此时因为singleton已经非null。这时候线程B到了1处,判断singleton非null并将其返回使用,因为此时Singleton实际上还未初始化,自然就会出错。synchronized可以解决内存可见性,但是不能解决重排序问题
指令重排程序测试
public class Main extends Thread {
static int a = 0;
static volatile int x = 0;
static int b = 0;
static int y = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
a = 0;
x = 0;
b = 0;
y = 0;
Thread first = new Thread(() -> {
a = 1;
x = b;
});
Thread second = new Thread(() -> {
b = 1;
y = a;
});
first.start();
second.start();
first.join();
second.join();
if (x == 0 && y == 0) {
System.out.println("x=" + x + " y=" + y);
break;
} else {
System.out.println("x=" + x + " y=" + y);
}
}
}
}
讲道理,上述代码可能的x,y值有:01,10,11,但应该不能是00
测试结果表明,未加上volatile会出现00,加上就不会出现。这就表明a=1,x=b的顺序被换了,即多线程中未加volatile时会进行指令重排,造成数据有误。
volatile为什么不具备原子性?
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
AtomicInteger : volatile与cas结合确保原子性
结合上文volatile的原子性阐述,如果线程A修改的时候采用CAS方式修改,即能判断到主内存的i=101值与预期值100不相等,则要么此次更新失败,要么将101写回到线程A的本地内存进行加法CAS操作。AtomicInteger即是采用了volatile和compare and swap的方式操作的。
Java 中能创建 volatile 数组吗?
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了
volatile 变量和 atomic 变量有什么不同?
Volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用volatile修饰count变量那么count++操作就不是原子性的。
而AtomicInteger类提供的atomic方法可以让这种操作具有原子性
如getAndIncrement( )方法会原子性的进行增量操作把当前值加- ,其它数据类型和引用变量也可以进行相似操作
synchronized 和 volatile 的区别是什么?
作用:
- synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
- volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别:
- synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
- synchronized 可以保证线程间的有序性(猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,无法保证原子性。
- synchronized 线程阻塞,volatile 线程不阻塞。