您现在的位置是:主页 > news > 海南城乡建设厅网站/公关策划公司
海南城乡建设厅网站/公关策划公司
admin2025/6/4 15:19:10【news】
简介海南城乡建设厅网站,公关策划公司,网站外链软件,漯河百度做网站电话文章目录1.什么是volatile?2.可见性3.volatile可见性代码证明3.不保证原子性4.禁止指令重排5.volatile应用1.什么是volatile? volatile是java虚拟机提供的轻量级的同步机制,一共有三大特性,保证可见性,不保证原子性&a…
文章目录
- 1.什么是volatile?
- 2.可见性
- 3.volatile可见性代码证明
- 3.不保证原子性
- 4.禁止指令重排
- 5.volatile应用
1.什么是volatile?
volatile是java虚拟机提供的轻量级的同步机制,一共有三大特性,保证可见性,不保证原子性,禁止指令重排。
2.可见性
首先提一个JMM的概念,JMM是Java内存模型,它描述的是一组规范或规则,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。JMM规定所有的变量都在主内存,主内存是公共的所有线程都可以访问,线程对变量的操作必须是在自己的工作内存中。在这个过程中可能出现一个问题。
现在假设主物理内存中存在一个变量他的值为7,现在线程A和线程B要操作这个变量所以他们首先要将这个变量的值拷贝一份放到自己的工作内存中,如果A将这个值改为1,这时候线程B要使用这个变量但是B线程工作内存中的变量副本是7不是新修改的1这就会出现问题。所以JMM规定线程解锁前一定要将自己工作内存的变量写回主物理内存中,线程加锁前一定要读取主物理内存的值。也就是说一旦A修改了变量值,B马上能知道并且能更新自己工作内存的值。这就是可见性。
3.volatile可见性代码证明
volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改。我们用代码证明一下。
设计思路首先我们新建一个类里面有一个number,然后写一个方法可以让他的值变成60,这时候在主线程中开启一个新的线程让他3s后将number值改为60然后主线程写一个循环如果主线程能立刻监听到number值的改变则主线程输出改变后的值此时说明有可见性。如果一直死循环说明主线程没有监听到number值的更改说明不具有可见性。
class MyData{public int number = 0;public void change(){number = 60;}
}public class VolatileTest {public static void main(String[] args) {MyData myData = new MyData();new Thread(()->{System.out.println(Thread.currentThread().getName() + "number is :"+ myData.number);try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}myData.change();System.out.println(Thread.currentThread().getName() + "has changed" + myData.number);},"A").start();while(myData.number == 0){}System.out.println(Thread.currentThread().getName() + "number is:" + myData.number);}
}
看一下结果
结果是进入了死循环一直空转,说明不具有可见性。下面我们在number前面加上关键字volatile。
证明能监控到number值已经修改说明加上volatile具有可见性。
3.不保证原子性
原子性指的是不可分割,完整性,也即某个线程正在做某个业务时不能被分割,要么同时成功,要么同时失败。为了证明volatile能不能保证原子性我们可以通过一个案例来证明一下。首先我们在之前的mydata类中加入一个方法addplus()能让number加1,然后我们创建20个线程然后每个线程调用1000次addplus()。看看结果如果number是20000那么他就能保证原子性如果不是20000那么就不能保证原子性。
class TestData{public static volatile int number = 0;public void change(){number = 60;}public void addPlus(){number++;}
}
public class VolatileDemo {public static void main(String[] args) {myData data = new myData();for (int i = 0; i < 20; i++) {new Thread(()->{for (int j = 0; j < 1000; j++) {data.addPlus();}},String.valueOf(i)).start();}while(Thread.activeCount() > 2){Thread.yield();}System.out.println("number is :" + TestData.number);}
}
结果不是20000说明不能保证原子性。
不保证原子性的原因:number++这个操作一共有3步第一步从主物理内存中拿到number的值第二步number + 1,第三步写回主物理内存。
假设一开始主物理内存的值为0线程1,线程2分别读取主物理内存的值到自己的工作空间,然后执行加1操作。这时候按理说线程1将1写回主物理内存然后线程2读取主物理内存的值然后加1变成2,但是在1写回的过程中突然被打断线程1挂起,线程2将1写回主物理内存这时候线程1重新将1写回主物理内存最终主物理内存的值为1,两个线程加了两次最后值居然是1,出错了。
如何解决volatile的原子性问题呢?
我们需要使用原子类,原子类是保证原子性的。加入一个AtomicInteger类的数据然后调用他的getAndIncrement()方法(就是把这个书加1底层用CAS保证原子性)
class TestData{public static volatile int number = 0;public static AtomicInteger atomicInteger = new AtomicInteger();public void change(){number = 60;}public void addPlus(){number++;}public void AtomicAdd(){atomicInteger.getAndIncrement();}
}
public class VolatileDemo {public static void main(String[] args) {TestData data = new TestData();for (int i = 0; i < 20; i++) {new Thread(()->{for (int j = 0; j < 1000; j++) {data.addPlus();data.AtomicAdd();}},String.valueOf(i)).start();}while(Thread.activeCount() > 2){Thread.yield();}System.out.println("number is :" + TestData.number);System.out.println("atomicInteger is:" + TestData.atomicInteger);}
}
结果是这个
这就解决了不保证原子性的问题。
4.禁止指令重排
计算机编译器在执行代码的时候不一定非得按照你写代码的顺序执行。他会经历编译器优化的重排,指令并行的重排,内存系统的重排,最终才会执行指令,多线程环境更是如此,可能每个线程代码的执行顺序都不一样,这就是指令重排。
public class VolatileReSort {int a = 0;boolean flag = false;public void method1(){//线程1a = 1;flag = true;}public void method2(){//线程2if(flag){a = a + 5;System.out.println("**********retValue:" + a);}}
}
假设现在线程1,线程2分别执行上面两种方法由于指令的重排序,可能线程1中的两条语句发生了指令重排,flag先变为true然后这是后线程2突然进来判断flag为true然后执行下面的最后输出结果为a = 5,但是也有可能先执行a = 1那这样结果就是a = 6所以由于指令重排可能导致结果不一定。现在加上volatile关键字他会在指令间插入一条Memory Barrier,来保证指令按照顺序执行不被重排。
5.volatile应用
传统的单例模式,在单线程下其实是没有什么问题的,多线程条件下就不行了。
public class SingletonDemo {private static SingletonDemo instance = null;private SingletonDemo(){System.out.println(Thread.currentThread().getName() +" :构造方法被执行");}public static SingletonDemo getInstance(){if(instance == null){instance = new SingletonDemo();}return instance;}public static void main(String[] args) {for(int i = 0;i<10;i++){new Thread(()->{SingletonDemo.getInstance();},String.valueOf(i)).start();}}
}
可以看出多线程下单例模式将会失效。我们通过DCL双重检查锁可以解决上述问题。
public static SingletonDemo getInstance(){if(instance == null){synchronized (SingletonDemo.class){if(instance == null){instance = new SingletonDemo();}}}return instance;}
但是这种方式也有一定的风险。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。instance = new SingletonDemo();可以分为一下3步完成1.分配对象内存空间2.初始化对象3.设置instance指向刚分配的内存地址,此时instance!=null。但是由于编译器优化可能会对2,3两步进行指令重排也就是先设置instance指向刚分配的内存地址但是这时候对象还没有初始化,如果这时候新来的线程调用了这个方法就会发现instance!=null然后就返回instance实际上instance没被初始化,也就造成了线程安全的问题。为了避免这个问题我们可以使用volatile对其进行优化禁止他的之指令重排就不会发生上述问题了。
private static volatile SingletonDemo instance = null;