首先,什么是单例模式?
通俗来说就是,单例模式就是在程序运行过程中,一直只能存在一个实例
那么单例模式到底具备哪些特征呢?
1. 由这个类自己创建自己的实例,且最多只能创建一个
2. 构造方法私有化,不能在其它类里使用
3. 给外界提供一个获取实例的静态方法
单例模式的几种常见写法
1. 饿汉模式
public class P2 {
private static P2 p2 = new P2();
private P2() {}
public static P2 getP2() {
return p2;
}
}
这大概是最简单的一种实现方式,虽然线程安全,但是也有很明显的问题,实例在一开始就被创建,浪费内存空间
2. 懒汉模式
public class P2 {
private static P2 p2;
private P2() {}
public static P2 getP2() {
if(p2==null) {
p2 = new P2();
}
return p2;
}
}
这种方式虽然在需要使用的时候才创建实例,但是很明显线程不安全,因为可能会有多个线程同时进入if代码块里
3. 加了锁的懒汉模式
public class P2 {
private static P2 p2;
private P2() {}
public static P2 getP2() {
synchronized (P2.class) {
if(p2==null) {
p2 = new P2();
}
return p2;
}
}
}
这种方式虽然在需要的时候,才创建了实例,也保证了线程安全,但是对创建实例和获取实例都加锁,效率很低
4. 双重锁模式
public class P2 {
private volatile static P2 p2 = null;
private P2() {}
public static P2 getP2() {
if(p2==null) {
//1.
synchronized (P2.class) {
if(p2==null) {
p2 = new P2();
}
}
}
return p2;
}
}
在这种方式下,对p2进行了两次判断,第一次判断是防止在已经有p2实例的时候,线程还排队等锁,提高效率,第二次判断是防止创建多个p2实例,当并发量很高且一开始没有实例时,很多线程都会运行到<注释1>处,但是一开始只有一个线程能拿到锁,其它线程都在<注释1>处等着,当那个拿到锁的线程运行完创建好实例之后,其它线程陆续依次获得锁,但是接下来if(p2==null)就会防止它们继续创建实例,这样就表面来看确实实现了线程安全,而且接下来获取实例都没有加锁,提高了效率
但是大家细心就会发现,声明p2的时候加了volatile关键字,那么volatile在这里发挥了什么作用呢?
我们需要先分析一下jvm创建实例的过程:(也就是在执行new语句时它都干了什么
1. 在堆内存开辟内存空间
2. 在堆内存中实例化P2里面的各个参数
3. 把对象引用指向堆内存空间
而在多核CPU下,为了提高效率,JVM存在乱序执行优化现象,也就是说,在具体执行过程中,可能先执行步骤3再执行步骤2,当有一个线程在执行步骤2之前执行了步骤3,那么这个时候p2就不是null了,这个时候如果有其它线程要获取p2实例时,就直接获取走了(获取实例的时候不需要锁),但是这个实例是没有执行步骤2的实例,因此接下来在使用p2的时候可能会出错
但是加了volatile关键字之后,JVM的乱序执行优化就不会再执行了
5. 静态内部类单例模式
public class P2 {
private P2() {}
public static P2 getP2() {
return Inner.p2;
}
private static class Inner {
private static P2 p2 = new P2();
}
}
通过这种方式既也节约了空间,同时保证了线程安全
节约了空间是因为在加载P2类的时候,并不会同时加载内部类Inner,而是在调用getP2方法的时候才会对Inner类进行加载,同时实例化P2对象,也就是说要在使用的时候,p2实例才会被创建出来
由于内部类Inner只能被加载一次,而Inner加载的时候p2实例也被创建出来,因此它也可以保证线程安全
但是它也有一个很明显的缺陷,那就是对单例的传参并不方便
6. 枚举单例模式
public enum P2 {
p2;
public static P2 getInstance() {
return P2.p2;
}
}
枚举的方式不仅非常简洁,而且还由于枚举的特性,使得它是实现单例模式的最佳方式,因为枚举类实例不可通过反射的方式创建,而前面几种实现单例模式的方式虽然没有提供公开的构造方法,但是还是可以通过反射的方式创建更多实例,不仅如此,枚举实例在序列化前后也不会发生改变,而其它类型的对象在序列化之后默认是返回一个新的对象