一.volatile是什么
如果用一句话概括volatile的话,那volatile其实就是java虚拟机提供的轻量级的同步机制。它具有一下三个特点:
1.保证可见性
2.不保证原子性(因为不保证原子性,所以他是轻量级的)
3.禁止指令重排
二.保证可见性
首先,我们先看看下面的代码
import java.util.concurrent.TimeUnit;
class MyData{
//int testData = 0;
volatile int testData = 0;
public void addTo60(){
this.testData = 60;
}
}
public class volatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
//工作线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();
System.out.println(Thread.currentThread().getName() + "\t update number value: " + myData.testData);
}, "test").start();
while (myData.testData == 0)
{
//假如不使用volatile修饰变量
//这里main线程会一直在这里等待
//因为工作线程修改了变量,却不能对main线程可见,main线程会一直拿着初始值的副本,也就是0
}
System.out.println(Thread.currentThread().getName() + "\t mission is over");
}
}
关于volatile是怎么实现可见性的,可以总结为以下两点:
1.从主内存到工作内存<读>:每次使用变量前 先从主内存中刷新最新的值到工作内存,用于保证能看见其他现场对变量修改的最新值。
2.从工作内存到主内存<写>:每次修改变量后必须立刻同步到主内存中,用于保证其他线程可以看到自己对变量的修改。(因为cpu比内存的速度要快很多,因此这中间有一个高速缓存的概念,一般修改后的变量都是先存进高速缓存,然后再刷新到主内存中)
3.指令重排序:保证代码的执行顺序和程序的执行顺序一致。(并发环境下 代码的执行顺序与程序的执行顺序有时并不一致,会出现串行的现象固有指令重排序优化一说。JAVA1.5之后彻底修复了这个BUG在用volatile变量的时)
三.不保证原子性
volatile有个比较不好的地方就是它不保证原子性,按照惯例,下面还是先上一段代码
import java.util.concurrent.TimeUnit;
class MyData{
volatile int testData = 0;
public void add(){
this.testData ++;
}
}
public class volatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++){
new Thread(() -> {
for (int j = 0; j < 1000; j++){
myData.add();
}
},"test"+i).start();
}
//后台默认有main线程和gc线程,因此这里选择当线程数量大于2时候,main线程退出暂停。
while (Thread.activeCount() > 2){
Thread.yield();
}
//这里每次运行出来的结果都是不同的
System.out.println(Thread.currentThread().getName() + "\t final number is " + myData.testData);
}
}
上述代码运行出来的结果不是我们想要的结果,导致这种情况出现的原因是,在并发编程中,可能存到两个线程同时去主内存中拿到变量,然后进行计算操作,例如线程a、b,同时拿到主内存中的变量x1到自己的工作内存里面,然后计算后得出同样的x2,当要把x2写回主内存时,其中一个会将数据刷新到主内存中,而另一个线程处于挂起状态,当前一个线程写入操作完成后,后一个线程被唤醒,在还没获取变量最新值的时候,立即进行写入操作(写覆盖),这时候主内存中的变量被刷新了两次,但是它的数值只增加了1,因为两个线程算出来的结果时一样的,这时候就会导致变量最后自增的结果不是我们想要的结果。
那么我们如何去解决这个问题呢?其实很简单,juc包里面有一个atomic类的数据,我们使用它来作为我们操作的对象就可以了。
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
volatile int testData = 0;
public void add(){
this.testData ++;
}
volatile AtomicInteger atomicInteger = new AtomicInteger();
public void addAtom(){
atomicInteger.getAndIncrement();
}
}
public class volatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++){
new Thread(() -> {
for (int j = 0; j < 1000; j++){
myData.add();
}
},"test"+i).start();
}
for (int i = 0; i < 20; i++){
new Thread(() -> {
for (int j = 0; j < 1000; j++){
myData.addAtom();
}
},"test-atomic"+i).start();
}
//后台默认有main线程和gc线程,因此这里选择当线程数量大于2时候,main线程退出暂停。
while (Thread.activeCount() > 2){
Thread.yield();
}
//这里每次运行出来的结果都是不同的
System.out.println(Thread.currentThread().getName() + "\t final number is " + myData.testData);
System.out.println(Thread.currentThread().getName() + "\t final number is " + myData.atomicInteger);
}
}
可是为什么使用atomic类的对象就可以解决原子性的问题呢?其实atomic是通过CAS来保证他的原子性的。
那,CAS又是什么呢?简单来说,CAS是compareAndSwap的缩写,意思是对比和交换。
java中CAS操作依赖于Unsafe类,Unsafe类所有方法都是native的,直接调用操作系统底层资源执行相应任务,它可以像C一样操作内存指针,是非线程安全的。
//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
而atomicInteger在更新他的value的时候,就是调用了unsafe类中的compareAndSwapInt方法去更新的。
举个例子,在线程需要对一个变量进行写操作的时候,会先对比这个变量是否符合预期值,如果符合,则会进行写操作,如果不符合,代表有其他线程对这个变量进行了修改,则获取变量当前值作为最新值,返回重新进行计算操作,依次循环,知道在对比的时候符合预期值。例如当前变量x1=1,同时有线程a,b过来获取变量,并进行+1操作,此时线程a计算完毕,对比主内存中变量是否为1,假如是,就将结果2写进主内存中,此时线程b也计算完毕了,对比主内存中的变量值,发现2!=1,意思是有其他线程已经对这个变量进行修改了,就会把2拿回去,重新进行+1的操作,然后再次对比。
原文:https://www.cnblogs.com/QicongLiang/p/13627591.html