一、简介
volatile是java中提供的一种轻量级的同步机制,相比于synchronized更加轻量,因为volatile避免了线程频繁的上下文切换和调度,但是也有缺点,同步性差并且使用起来易出错。
二、并发编程的三个概念
- 原子性
原子性即一个操作或者多个操作要不全部执行完(执行过程不被干扰),要不都不执行。 - 可见性
可见性指的是,当一个线程操作一个变量的值时,在其他线程是可以及时看到这个变量修改后的值,并能获取来操作。对比与synchronized和lock,这两者也都能保证可见性,因为它们在每次释放锁之前都会将修改好的变量值更新到主内存中,保证了可见性。 - 有序性
即程序执行的顺序是按照代码的先后顺序来执行的。
java内存模型中所说的有序性总结为,在本线程中看都是有序执行的,在一个线程看另外一个线程都是无序的,也就是说前半句表示“线程内表现为串型语义”,后面句表示“指令重排序”现象和“主内存与工作内存同步延迟”现象。
三、锁的互斥和可见性
- 互斥:即一次只允许一个线程持有某个特定的锁,而且一次只允许一个线程访问共享数据
- 锁的可见性:在前一个线程释放锁之前对变量进行更改后,将更改后的变量由工作内存更新到主内存中,保证下一个获得该锁的线程对这个变量具有可见性,如果没有及时更新到主内存中后果很严重,会导致数据不一致问题。
要让volatile线程安全,必须保证两点
(1)对变量的写操作不依赖于当前值
(2)该变量没有包含在具有其他变量的变式中
事实上就是保证该变量操作的原子性。
四、volatile变量的特性
- 保证可见性,不保证原子性
(1)当写一个变量是,当前线程会将该变量从工作内存强制更新到主内存,保证其他线程实时获取的变量是最新的,保证了可见性。
(2)这个写操作会导致其他线程中这个变量缓存无效。 - 禁止指令重排序
重排序是程序为了更好地性能调度,对指令进行新的排序的一种手段,需要遵守以下规则
(1)重排序不会对存在数据依赖关系的程序重排序
(2)重排序是为了优化性能,但无论怎么排序,在单线程下,从排序后的运算结果都不会改变,但是在多线程下重排序则可能会影响结果
(3)使用volatile能够禁止指令重排序,但要遵守一些规则。
1)当执行到volatile修饰的变量进行读或者写时,该操作前面的指令都执行完毕,且该操作结果对后面指令可见,而且该操作后面的指令都未执行
2)当指令优化时,不能将指令放到volatile后面执行,也不能把把volatile后面的语句放到前面执行。
五、volatile不适合的场景
- volatile不适合复合操作,下面模拟十个线程对num自增,每个线程自增1000,
比如 num++,不是一个原子性操作,分为读取,加,赋值三个操作,所以结果达不到10000
public class VolatileTest1 { private static volatile int num = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { for (int j = 0; j < 1000; j++) { num++; } }); thread.start(); } System.out.println("num=" + num); } }
结果
num=5657
num=4414
num=7491
…(完全随机)
- 解决办法
(1)使用synchornized锁住变量所在的类
public class VolatileTest1 { public static volatile int num = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { synchronized (VolatileTest1.class) { num++; } } } }).start(); } // 保证线程执行完毕 while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println("num=" + num); } }
结果:
num=100000
(2)使用lock锁住该方法
public class VolatileTest3 { public static volatile int num = 0; public static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { lock.lock(); try { num++; }finally { lock.unlock(); } } } }).start(); } // 保证线程执行完毕 while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println("num=" + num); } }
结果:
num=100000
(3)使用并发原子操作类AtomicInteger ,其原理是通过CAS循环的方式来保证原子性
public class VolatileTest5 { public static volatile AtomicInteger num = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { num.getAndIncrement(); } } }).start(); } // 保证线程执行完毕 while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println("num=" + num); } }
结果:
num=100000
六、单例模式的双重检验锁为什么要加volatile
public class InstanceTest{ private volatile static InstanceTest instance; public static InstanceTest getInstance(){ //1 if(instance == null){ //2 synchronized(InstanceTest.class){ //3 if(instance == null){ //4 instance = new InstanceTest(); //5 } } } return instance; //6 } }
在并发情况下,如果没有volatile修饰的话,代码中5处会出现问题instance = new InstanceTest();
这个可以分为三步,
a. memory = allocate() //分配内存 b. ctorInstanc(memory) //初始化对象 c. instance = memory //设置instance指向刚分配的地址
在编译时,指令重排序,不一定按照a->b->c的顺序来执行,可能会是a->c->b,多线程下一个线程在执行完a,马上执行c,设置instance指向刚分配的地址,这个时候另外一个线程刚好到第2步判断,则会出现不为空,直接跳到第6步,而此时并没有初始化,会返回一个未初始化的对象。
七、volatile原理
volatile可以保证线程的可见性并提供一定的有序性,底层采用的是“内存屏障”来实现的,但是不能保证原子性,需要结合lock,synchornized,AtomicInteger 等来实现线程并发安全。
内存屏障会提供三个功能:
- 他不会把volatile修饰的变量执行的操作的前面的指令方放到后面,当然相反也不会将后面的指令放到前面。(禁止指令重排序)
- 他会强制将当前修改的变量立即写入主缓存
- 如果是写操作,他会将其他线程的缓存置为无效
借鉴学习-侵删