问:谈谈对volatile的理解?
当用volatile去申明一个变量时,就等于告诉虚拟机,这个变量极有可能会被某些程序或线程修改。为了确保这个变量修改后,应用范围内全部线程都能知道这个改动,虚拟机就要保证这个变量的可见性等特色。最简单的一种方法就是加入volatile关键字。java
volatile是JVM提供的轻量级的同步机制。缓存
volatile有三大特性:安全
要了解它的三大特性,要先了解JMM。多线程
上面提到的概念 主内存 和 工做内存:并发
用代码验证volatile的可见性:性能
class MyData { // 定义int变量 int number = 0; // 添加方法把变量 修改成 60 public void addTo60() { this.number = 60; } } public class Test { public static void main(String[] args) { // 资源类 MyData myData = new MyData(); // 用lambda表达式建立线程 new Thread(() -> { System.out.println("线程进来了"); // 线程睡眠三秒,假设在进行运算 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 修改number的值 myData.addTo60(); // 输出修改后的值 System.out.println("线程更新了number的值为" + myData.number); }).start(); // main线程就一直在这里等待循环,直到number的值不等于零 while (myData.number == 0) { } //最后输出这句话,看是否跳出了上一个循环 System.out.println("main方法结束了"); } }
最后线程没有中止,没有输出 main方法结束了 这句话,说明没有用volatile修饰的变量,是没有可见性的。优化
当咱们给变量 number 添加volatile关键字修饰时,发现能够成功输出结束语句。this
volatile 修饰的关键字,是为了增长 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能立刻感知,是具有JVM轻量级同步机制的。
总线嗅探技术有哪些缺点:spa
不可分割,完整性。也就是说某个线程正在作某个具体业务时,中间不能够被加塞或者被分割,须要具体完成,要么同时成功,要么同时失败。线程
class MyData { // 定义int变量 volatile int number = 0; public void addPlusPlus() { number++; } } public class Test { public static void main(String[] args) { MyData myData = new MyData(); // 建立20个线程,线程里面进行1000次循环(20*1000=20000) for (int i = 0; i < 20; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { myData.addPlusPlus(); } }).start(); } /* 须要等待上面20个线程都执行完毕后,再用main线程取得最终的结果 这里判断线程数是否大于2,为何是2?由于默认有两个线程的,一个main线程,一个gc线程 */ while (Thread.activeCount() > 2) { Thread.yield(); // yield表示不执行 } System.out.println("线程运行完后,number的值为:" + myData.number); } }
线程执行完毕后,number输出的值并无 20000,而是每次运行的结果都不一致,这说明了volatile修饰的变量不保证原子性。
当 线程A 和 线程B 同时修改各自工做空间里的内容,因为可见性,须要将修改的值写入主内存。这就致使多个线程出现同时写入的状况,线程A 写的时候,线程B 也在写入,致使其中的一个线程被挂起,其中一个线程覆盖了另外一个线程的值,形成了数据的丢失。
i++不是原子操做,其执行要分为三步:
举个例子:如今有A、B两个线程,i 初始为 2。A线程完成第二步的加一操做后,被切换到B线程,B线程中执行完这三步后,再切换回来。此时A寄存器中的 i=3 写回内存,最后 i 的值不是正常的4。
synchronized
public synchronized void addPlusPlus() { number ++; }
引入synchronized关键字后,保证了该方法每次只可以一个线程进行访问和操做,保证最后输出的结果。
咱们还可使用JUC下面的原子包装类,i++
可使用AtomicInteger
来代替
//建立一个原子Integer包装类,默认为0 AtomicInteger number = new AtomicInteger(); public void addAtomic(){ number.getAndIncrement(); //至关于number++ }
计算机在执行程序时,为了提升性能,编译器和处理器经常会对指令重排,通常分为如下三种:
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令。
多线程环境中线程交替执行,因为编译器优化重排的存在,两个线程中使用的变量可否保证一致性是没法确认的,结果没法预测。
public void mySort() { int x = 11; int y = 12; x = x + 5; y = x * x; }
按照正常单线程环境,执行顺序是1234。
可是在多线程环境中,可能出现如下的顺序:213四、1324。
可是指令排序也是有限制的,例如3不能出如今1面前,由于3须要依赖步骤1的声明,存在数据依赖。
Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象。
首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的做用有两个:
在Volatile的写和读的时候,加入屏障,防止出现指令重排,线程安全得到保障。
public class SingletonDemo { //用静态变量保存这个惟一的实例 private static SingletonDemo instance = null; //构造器私有化 private SingletonDemo() { } //提供一个静态方法,来获取实例对象 public static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; } }
单线程下建立出来的都是同一个对象。可是在多线程的环境下,咱们经过SingletonDemo.getInstance()
获取到的对象,并非同一个。
public synchronized static SingletonDemo getInstance() { if (instance == null) { instance = new SingletonDemo(); } return instance; }
可是synchronizaed属于重量级的同步机制,它只容许一个线程同时访问获取实例的方法,可是所以减低了并发性,所以采用的比较少。
就是在 进来、出去 的时候,进行检测。
public static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }
可是DCL机制不必定是线程安全的,缘由是由于有指令重排的存在,咱们加入Volatile能够禁止指令重排。
private static volatile SingletonDemo instance = null;
由于instance的获取能够分为三步进行完成:
instance != null
由于步骤二、3不存在数据依赖,便可能出现第三步先于第二步执行;此时由于已经给即将建立的instance分配了内存空间,因此instance!=null,但对象的初始化还未完成,形成了线程的安全问题。
-
去掉第一个判断为空:即懒汉式(线程安全),这会致使全部线程在调用getInstance()方法的时候,直接排队等待同步锁,而后等到排到本身的时候进入同步处理时,才去校验实例是否为空,这样子作会耗费不少时间(即线程安全,但效率低下)。
去掉第二个判断为空:即懒汉式(线程不安全),这会出现 线程A先执行了getInstance()方法,同时线程B在由于同步锁而在外面等待,等到A线程已经建立出来一个实例出来而且执行完同步处理后,B线程将得到锁并进入同步代码,若是这时B线程不去判断是否已经有一个实例了,而后直接再new一个。这时就会有两个实例对象,即破坏了设计的初衷。(即线程不安全,效率高)
双重校验的目的:除了第一次实例化须要进行加锁同步,以后的线程只要进行第一层的if判断不为空便可直接返回,而不用每一次获取单例都加锁同步,所以相比前面两种懒汉式,双重检验锁更佳。(双重校验锁结合了 两种懒汉式 的优势)