单例模式: 指的是 " 一个类有且只有一个实例 "
volatile关键字: 保证"有序性, 可见性"
synchronized关键字: 保证"原子性, 可见性"
规范版本:
问题1: Java单例模式双重校验锁的第一次是否为空的判断是什么目的?
已知: 在使用多线程时, 需要在多个线程访问共享资源之前加锁(即 synchronized块会进行一次是否为空的判断), 然后再进行操作. 因此, 一个类只创建一个实例这点得到了保障.
但是如果只在synchronized块里进行一次对象是否为空的判断, 会使得synchronized块总是会被执行到, 意味着每个线程执行getInstance方法时都必须获得一个内部对象锁, 于是就大大增加了锁的获得以及锁的释放的开销. 而如果在执行synchronized块之前先进行一次对象是否为空的判断, 那么就降低了synchronized块被多个线程执行到的几率(即避免了锁竞争), 因此降低了锁的开销,提高了性能
synchronized在第二次判断条件那里加锁, 原因是会引起 " 锁竞争 " , 锁竞争现象会影响效率, 有了锁竞争, 线程都会 " 同步互斥 ", 因此为了提高效率, 将锁放到第一层判断之外.
进行第一次变量是否为空的判断时, 要满足多线程下数据的读取操作, 会涉及到读取操作的安全性, 而将第一次判断放在锁之外, 只是获取到了变量的值, 然后进行判断了一下, 因此只涉及到了变量的读取操作, 涉及到了变量的可见性问题, 因此会使用volatile关键字来保证它的可见性
volatile的可见性: 一个线程在进行写操作的同时, 可以被其他正在进行读操作的线程立即看到.
问题2: 为什么synchronized要在第二次判断条件那里加锁?
因为在锁竞争之后, 为了保证单例模式是一个对象(单例,即就是指只有一个实例对象), 要加上变量是否为空的判断条件, 如果变量等于空, 才进行new操作创建一个对象, 否则不进行新对象的创建的. 如果不加上这个判断条件, 每次都是创建了一个新对象, 不满足单例模式的设计了
问题3: 对volatile禁止指令重排序的作用的理解.
多线程下的执行逻辑: 假如有10个线程在运行, 10个线程都可以在同一时间点调用getDataSource()这个方法.
这里要理解的前提知识有:
1. 在同一时间点, 可能有多个线程要执行某一代码行或者某一代码块.
2. synchronized对对象进行加锁操作时, 会造成线程执行代码同步互斥
没有加锁之前(即第一个判断对象是否为空的if语句), 所有线程都可以同时执行的, 加了锁之后(第二个if语句), 只有获取这个对象锁的线程才可以执行, 其他线程阻塞等待, 因此这段代码(第二个if判断语句里的代码)就只能在获取到对象锁的那个线程里执行
给某个线程加上对象锁, 而其他线程尝试获取同一个对象锁, 在执行到synchronized这个代码行,并尝试获取锁的时候, 因为获取不到,就阻塞等待(注意, 这里一定要强调是获取同一个对象锁, 因为如果线程不是获取同一个对象锁, 也是可以同时执行的)
n++操作和new操作是非原子性的, 就比如下图代码中的new操作, 会分为三个指令:(虽然在java源文件里看起来是一行代码,但java文件最终会编译成class文件,class文件在虚拟机/CPU里会分解为更细的指令)