volatile 可以理解为乞丐版的synchronized,唯一差的一点就是原子性!
volatile三大特性:
1.可见性
顺带一提
数据一般存储在内存中,如nosql的redis,而像mysql这样的数据则存在硬盘,CPU则只是负责运算。
所以数据的读取熟读为:
硬盘<内存<CPU
简单说一下线程工作原理,先去内存读取数据,然后将数据获取复制到线程的工作空间,将数据处理完毕后,才会重新写入内存,内存收到新写入的数据后,会通知其他的线程,数据变了!你们要拷贝数据,记得拷贝新数据哦!这就是volatile的可见性。
例:
首先一个普通的数据类,其中有一个int number=0,一个addint()方法,将自身的数据改为60。
class MyData{
int number=0;
public void addInt(){
this.number=60;
}
}
然后写个mian方法,这里一个线程中先睡他个3秒,等待3秒后,启用addInt()方法,将number赋值为60。
下面有个while循环,一开始number的值为0会一直循环,当3秒后,线程AAA将值改变后,number不等于0,此时理当跳出循环。
public class VolatileVisibility {
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.addInt();
System.out.println(Thread.currentThread().getName()+"\t update a:"+myData.number);
},"AAA").start();
while (myData.number==0){}
System.out.println("main come in");
}
}
但结果却并不然,因为默认是没有可见性的,所以mian线程并不知道数据已经被修改,就会在陷入死循环。
此时只需要在一开始MyData中的number属性,用volatile修饰,就能保证他的可见性,这个时候AAA线程修改值以后,内存就会通知mian线程,值改了,那么最后就能输出mian come in,同学们自己去打打代码,练练手!
这就是volatile的神奇之处
2.不保证原子性
前面说了,volatile有可见性,但是不保证原子性就是它乞丐版的根本。
要知道JMM模型的特性有以下几点:
1.可见性(Visibility)
2.原子性(Atomicity)
3.有序性(Uniformity)
什么是原子性,所以原子就是最小的单位,不可再拆分,要保持其完整性,也就是这个线程不可被其他线程加塞或者分割,整个线程的任务要么全部成功,要么全部失败。
但是前面也说了volatile不保证原子性,为什么这么说呢?上代码
class MyData{
volatile int number=0;
public void addPlusPlus(){
this.number++;
}
}
还是一样的MyData,不过这次我们改为addPlusPlus每次都自增1。
实验方法是:使用20个线程,每个线程修改1000次,如果正常走的话,应该是有20000个!来看看结果如何?
public class StudyVolatile {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <=20; i++) {
new Thread(()->{
for (int j = 1; j <=1000 ; j++) {
myData.addPlusPlus();
}
}).start();
}
while (Thread.activeCount()>2){//
Thread.yield();
}
System.out.println(myData.number);
}
}
可以看到无论怎么输出,基本都比20000小
18832
Process finished with exit code 0
---
19234
Process finished with exit code 0
---
19110
Process finished with exit code 0
这就是因为volatile不能保持原子性,导致重复读,例如有3个线程同时读到一个数字0,A、B、C三个线程都拿到自己的工作空间,写好了,A放到内存去了,B线程刚才被加塞了一下,也继续写下去了,内存还没来得及通知新数据,所以就会造成这样的结果,重复读!
如何解决原子性问题
在不用synchronized情况下,我们怎么解决呢?
既然volatile不保证原子性,我们可以使用JUC中的原子性数据类型,如数字的AtomicInteger()类。
class MyData{
volatile int number=0;
AtomicInteger atomicInteger = new AtomicInteger();
public void addInt(){
this.number=60;
}
public void addPlusPlus(){
this.number++;
}
public void addAtomicPlusPlus(){
this.atomicInteger.getAndIncrement();
}
}
public class StudyVolatile {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <=20; i++) {
new Thread(()->{
for (int j = 1; j <=1000 ; j++) {
myData.addPlusPlus();
myData.addAtomicPlusPlus();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(myData.number);
System.out.println(myData.atomicInteger);
}
}
这个时候,就可以看到原子性的int怎么操作都始终为20000
18100
20000
Process finished with exit code 0
那既然这样为什么还要用volatile呢?最后再说,先来看看volatile的最后一个特性
3.禁止指令重排
当计算机在执行程序时,为了提高性能,有可能对指令进行重排。
源代码->编译器优化重排->指令并行的重排->内存系统的重排->最终执行的指令
在单线程的情况下,无论如何重排都不影响整体的语义,保证执行结果一致。
指令重排需要尊崇 数据之间的依赖性。
但是在多线程的情况下,就有可能因为指令重排而导致结果无法确定。
如下:
x=1;//语句1
y=10;//语句2
x=x+5;//语句3
y=x+10;//语句4
指令可以重排为2134,这样并不影响输出结果。
也可以改为1324,这里同样不影响。
但是并不能将4提到前面去,因为这样就会印象最终的结果。
但是在多线程的情况下,我们就无法确定了
public class Demo2 {
int a=0;
boolean flag=false;
public void method1(){
a=1;
flag=true;
}
public void method2(){
if (flag){
a+=5;
}
}
}
当多线程的情况下,指令重排就也可能导致method1的flag=true先执行,再执行a=1,这样method2中的a就只会等于5。这时就可以将目标变量用volatile修饰,让其前后都加上一层屏障,导致无法被重排!
什么情况下用volatile
单例模式
我们先写个普通的单例模式,如下:
public class SingleDemo {
private static SingleDemo instance=null;
private SingleDemo(){
System.out.println(Thread.currentThread().getName()+" \t new SingleDemo");
}
public static SingleDemo getInstance(){
if (instance==null){
instance = new SingleDemo();
}
}
return instance;
}
public static void main(String[] args) {
System.out.println(SingleDemo.getInstance()==SingleDemo.getInstance());
System.out.println(SingleDemo.getInstance()==SingleDemo.getInstance());
System.out.println(SingleDemo.getInstance()==SingleDemo.getInstance());
}
}
但是在多线程并行的情况下,就有可能会导致这种问题
public class SingleDemo {
private static SingleDemo instance=null;
private SingleDemo(){
System.out.println(Thread.currentThread().getName()+" \t new SingleDemo");
}
public static SingleDemo getInstance(){
if (instance==null){
instance = new SingleDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
SingleDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
------------
1 new SingleDemo
6 new SingleDemo
5 new SingleDemo
4 new SingleDemo
2 new SingleDemo
3 new SingleDemo
多个线程抢着去新建对象,这样就违背了我们的单例模式原则,怎么解决了?可以用DCL控制
//DCL(Double Check Lock 双端检锁机制)
public static SingleDemo getInstance(){
if (instance==null){//在锁之前判断一次
synchronized (SingleDemo.class){
if (instance==null)//锁之后再判断一次
instance = new SingleDemo();
}
}
return instance;
}
两次判断就能保证这个对象不为空,这样每次在创建时,就只有一个线程能进行创建。
但是讲到这里,就无需用到volatile了?再往深了讲,创建对象其实也是要分成几个对象的。
1.分配地址
2.将对象放入地址
3.获取地址引用
但也有可能指令重排后,变成刚分配地址,线程就跑来获取地址引用了,这个时候有地址了,表面上是不为null了,但是对象还没住进去呢!可以说是有名无实了!也就是说你找到他家了,但是他只是签了合同,还没搬进来!你就要来这里找他,这显然不可能,所以我们需要保证他住进来了,我们再去找他!就可以用volatile修饰他,避免流程走错了!
private static volatile SingleDemo instance=null;
学单例模式,有6种形态,掌握这一个,再去融会贯通!就够了!