单例模式:只产生一个实例对象,如何确保只有一个实例产生,私有化构造方法,提供一个public方法来获取实例对象
1 饿汉式
public class Singleton1 {
//饿汉式:在类加载的时候初始化实例对象,不管你需不需要用
//就像一个被饿怕了的人,不管饿没饿,都先把吃的准备好
private static Singleton1 instance = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return instance;
}
}
在类加载的时候就进行了实例化,不存在多线程同步的问题,是线程安全的。缺点是在类加载的时候就初始化,如果这个实例很占用内存就形成了浪费。如果这个实例很大且只在特定场景下才使用,就用懒汉式。
2 懒汉式
public class Singleton2 {
//就是一个懒惰的人,在饿了的时候才会去找吃的
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
懒汉式在需要实例时才进行创建,但是多个线程同时调用getInstance()方法时候可能会产生多个实例,就需要给这个获取实例的方法上锁,加上关键字synchronized后代码如下:
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {
}
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
加锁后的懒汉式解决了线程并发的问题,但是依然不够完美,锁住的东西太多了,导致程序运行速度变慢,需要进行优化:
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) { //标记步骤
synchronized(Singleton2.class){
instance = new Singleton2();
}
}
return instance;
}
}
此种方式依然不是线程安全的,如果线程1执行完if(instance == null)那一步然后中断了,此时是不存在实例的,线程2执行到这一步判断依然为空,向下执行创建实例,然后释放锁的钥匙,线程1获取线权后拿到钥匙继续执行并创建实例,程序就出错了,需要在同步代码块里面再判断一次,双重检查:
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
synchronized(Singleton2.class){
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
此时的代码没问题了吗?no no no!
因为处理器会进行指令重排序,比如说有如下代码:
int i = 1; //语句1
int n = 2; //语句2
i = i + 1; //语句3
n = i + n; //语句4
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。也就说这一段代码的执行顺序可能是 1 -> 2 -> 3 -> 4 ,也有可能是 1 -> 3 -> 2 -> 4 , 还可能是 2 -> 1 ->3 -> 4 ,总之只要不影响最后结果的顺序都有可能,但是如果一条指令(指令2)用到了另一条(指令1)的结果,那么处理器会确保指令1先于指令2执行,也就说上面代码不可能出现语句4先于语句3的情况。在单线程中是不会有问题的,但是多线程中指令重排序就会让程序存在问题了。
那指令重排序与我们这个单例模式又有啥关系?问题就出在一面这一句:
instance = new Singleton2();
这一句在处理器中执行的时候是分为三个步骤去执行的:
① 给instance分配内存
② 执行new Singleton2()创建实例
③ 将instance对象指向分配的内存空间(执行了这一步后instance就不再是null了)
因为存在指令重排序,当jvm执行时候顺序就可能是1 -> 2 -> 3 ,也可能是 1 -> 3 -> 2,如果是后者,线程1执行玩第3步,还没执行第2步,突然被线程2抢占了,此时instance已经不是null了,线程2执行getInstance()就会出错。所以此处需要一个关键字:volatile,代码如下
public class Singleton2 {
private static volatile Singleton2 instance = null;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (instance == null) {
synchronized(Singleton2.class){
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
volatile作用禁止指令重排序,能够保证线程的有序性,就是volatile修饰的变量进行读写操作时,前面的语句已经执行完了,对后面的可见,并且后面的语句都还没有执行。这就保证了上述步骤执行顺序时1 -> 2 -> 3
3 静态内部类模式
public class Singleton3 {
private static class Singleton3Holder{
private static final Singleton3 instance = new Singleton3();
}
private Singleton3() {
}
public static Singleton3 getInstance() {
return Singleton3Holder.instance;
}
}
这种模式和饿汉式一样使用了类加载机制去创建实例,不同的是这种可以再需要的时候进行创建,也不会产生线程并发的问题。
主要常用的还是静态内部类和懒汉式的双重检查锁模式