-
单例模式:一个只能创建一个对象的类。
-
饿汉式,直接在内部创建好对象,需要时return,优点是需要时直接就能获取对象,且并发安全,缺点是如果一直不需要的话,会造成资源浪费:
public class test { private static test aInstance = new test(); private test(){} public static test getInstance(){ return aInstance; } }
-
简单懒汉式(懒加载):需要时再创建:
public class test { private static test aInstance = null; private test(){} public static test getInstance(){ if(aInstance == null) aInstance = new test(); return aInstance; } }
-
简单懒汉式的缺点是并发不安全,可能有多个线程同时进入到 if 语句内,导致创建多个实例,解决方法很简单,加锁:
public class test { private static test aInstance = null; private test(){} public static synchronized test getInstance(){ if(aInstance == null) aInstance = new test(); return aInstance; } }
-
加锁后,解决了线程安全的问题,但是,当我们正确创建了唯一实例后,线程不安全的问题就不存在了,以后每次都只是 return aInstance,但由于加了锁,带来了不必要的时间开销,解决方法是缩小锁的范围:
public class test { private static test aInstance = null; private test(){} public static test getInstance(){ if(aInstance == null) synchronized (test.class){ if(aInstance == null) aInstance = new test(); } return aInstance; } }
这种写法要注意的是,在锁内部又对 aInstance 进行了一次判空,原因是:如果没有第二次判空,仍然是线程不安全的,因为可能有多个线程同时越过了第一次判空,卡在了锁前,虽然锁能保证每次只进去一个线程,但是仍会创建多个实例。因此,在锁内必须要再进行一次判空,这种写法叫双重检测锁(DCL,double check lock)。
-
但是,DCL仍然有问题,因为 new 不是原子操作,在指令重排的情况下,可能会有某线程获得一个未初始化的实例对象(原子性及有序性问题)。解决方法是用 volatile 禁止指令重排(适用于jdk1.5之后):
public class test { private static volatile test aInstance = null; private test(){} public static test getInstance(){ if(aInstance == null) synchronized (test.class){ if(aInstance == null) aInstance = new test(); } return aInstance; } }
-
那么,有没有其他方法实现类似的效果呢?
-
静态内部类,在外部类加载时,静态内部类不会加载,因此只有在运行到Holder.aInstance时加载内部类,初始化单例,且初始化时保证线程安全,不过缺点是无法传参:
public class test { private test(){} private static class Holder { static test aInstance = new test(); } public static test getInstance(){ return Holder.aInstance; } }
-
以上的写法都无法抵御反射攻击以及序列化攻击,而能抵御这两种攻击的简单高效方法是通过:枚举类型:
public enum test { INSTANCE; public void speak(){ System.out.println("hello!"); } public static void main(String[] args) { test.INSTANCE.speak(); } }
-
JDK中的单例模式:Runtime类(饿汉式)。
-
指令重排:编译器优化重排(不改变单线程语义)、指令并行重排(不存在数据依赖的前提下,在流水线基础上进一步优化)、内存系统重排序。后两者属于处理器重排。
数据依赖:对同一变量,写后写,写后读,读后写。
对于编译器重排,可以人为限定优化规则,来禁止某些类型的编译器重排;而对于处理器重排,则需要通过插入底层指令来进行避免,而对于不同的处理器架构,需要的指令也不同,但是,我们期望其呈现出的效果总是一致的,这也就引出了所谓内存屏障的概念。
内存屏障:store store, store load, load load, load store。
-
happens-before,JMM(Java Memory Model)对指令重排做出的限定,保证特定规则下重排的结果一定符合按某种先后顺序推演的结果(即多线程可见性)。JMM并非完全禁止了指令重排,而是在保证结果正确性的前提下,给编译器、CPU留了些优化余地。再次强调,happens-before是多线程的。
-
并发编程不安全问题根源:可见性、有序性、原子性。
可见性:多核心下,工作在不同CPU的线程间只能看到局部的工作内存(这里借用了JMM的说法,注意,工作内存包括缓存、寄存器、写缓冲区等),由此引发的不同步问题(单核多线程不存在可见性问题)。
缓存一致性协议:多核心为解决缓存(cache)不一致问题,在硬件层面实现了各核心的缓存(cache)的一致性。常见的有MESI协议。但是,各核心的工作内存间仍无法保证瞬时一致。
原子性:单核多线程间切换,导致一条高级编程语言(i++)对应的多条CPU指令被拆散,引发的不一致问题。
有序性:编译器、处理器重排序。
-
X86架构下volatile原理: volatile会增加一条lock前缀空指,在写的时候让store buffer的值立刻刷入缓存,由此触发MESI,使得其他缓存的缓存行数据失效,保证可见性(lock同时也能禁止指令重排序)。详情参考:知乎相关问题:volatile与MESI的关系,罗一鑫的回答以及对应评论,内存屏障今生之Store Buffer, Invalid Queue。
volatile一图流(原创),求了再别问我啥原理了orz:
-
为了便于以后回想本部分,列一些关键点。
Java中广义的volatile实现原理是插入内存屏障(参见第11条):volatile写前后各一个,volatile读后边两个。而一旦落实的具体实现,由于不同的CPU架构下,会出现的处理器重排序情况是不同的,故要插入的内存屏障,以及内存屏障的具体实现方式也是不同的。
一般而言,内存屏障的主要实现原理都是,写屏障:处理完store buffer回写cache;读屏障:处理完invalid queue。
x86支持MESI,采用TSO模型,没有invalid queue,故只需解决store load。
参考资料: