单例模式概念
单例模式表示一个类在任何情况下有且仅有一个实例,并且提供一个全局访问点
单例模式写法
- 饿汉式单例
- 懒汉式单例
- 注册时单例
- ThreadLocal单例
饿汉式单例
饿汉式单例意为在类加载的时候就初始化一个实例对象。 代码如下:
public class HungrySingleton {
//也可通过静态代码进行初始化
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
}
饿汉式单例的优缺点:
- 缺点:由于饿汉式单例是在类加载的时候对象就创建好了,不管你使没使用过,这样会造成内存资源的浪费
- 优点:执行速度快,因为在一开始就创建好了,使用到的时候直接从内存中拿,不需要进行初始化操作。另外没有线程安全问题
懒汉式单例
懒汉式单例就是一开始不创建,当你需要这个对象的时候才进行创建 代码如下:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(instance == null){ //代码1
instance = new LazySingleton(); //代码2
}
return instance;
}
}
懒汉式单例优缺点:
- 缺点:线程不安全。
- 优点:避免内存资源的浪费。其实在上述代码这种情况下这句话是不对的,因为在多线程并发获取实例对象的情况下,会创建多次实例对象,这样怎么会更加的占用内存资源
懒汉式单例线程不安全情况分析
首先分析下多线程获取实例会出现哪些情况:
获取结果一致
这种情况就是多个线程获取到的对象都是同一个,但是这样的结果又有两种情况。
第一个情况是例如A、B两个线程顺序访问,获取到的对象始终是最先拿到对象的线程所创建的。
第二个情况就是A、B两个线程同时进入代码1时发现都是null,此时都进入代码2准备执行,这个时候A线程执行完代码2但还未返回,这个时候B线程执行完代码2将之前A线程创建的对象给覆盖了,这个时候A线程返回的就是B线程创建的对象。从结果看获取到的都是一个对象,但是对象创建了两次
获取结果不一致
例如A、B两个线程执行到代码1时发现都是null,于是就准备执行代码2,此时A线程执行完代码2并返回实例,然后B线程执行完代码2并返回实例,这样两次结果就不一致
所以其实不管获取到的结果是否一致,都会存在线程安全的问题。因此要对上述代码进行加锁,控制其并发访问
线程安全的懒汉式模式
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
private SynchronizedLazySingleton(){}
public synchronized static SynchronizedLazySingleton getInstance(){
if(instance == null){
instance = new SynchronizedLazySingleton();
}
return instance;
}
}
这种写法虽然避免了线程安全问题,但是由于synchronized是作用在方法级别上的,锁的粒度比较大,影响性能,一旦有多个线程进行访问,则会造成多个线程处于阻塞状态
双重检查锁模式
public class DoubleCheckLockSingleton {
private **volatile** static DoubleCheckLockSingleton instance;
private DoubleCheckLockSingleton(){}
public static DoubleCheckLockSingleton getInstance(){
//判断对象是否为空,从而决定是否阻塞
if(instance == null){ //代码1
synchronized (DoubleCheckLockSingleton.class) {
//判断对象是否为空,从而决定是否要进行创建对象,避免二次创建对象
if(instance == null) { //代码2
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
public String getName(){
return "Mary";
}
}
双重检查锁机制避免了锁的粒度过大的问题,将锁的粒度降到最低。但为什么需要两个条件判断呢?这是因为如果没有代码2的话,一旦两个线程同时进入到代码1处判断,发现都是null,然后进入抢占锁阶段,不管是谁先抢到锁,都会创建两次实例,这样又出现了问题,所以需要加上代码2。
那为什么要加volatile关键字呢?这是如果不加volatile关键字,就会出现DCL半对象问题,接下来说明下什么是DCL半对象问题,以及怎样解决?
DCL半对象问题
如果多个对象获取到DoubleCheckLockSingleton对象,并且调用getName()方法,有可能会出现空指针异常的问题,这是因为获取到的对象还是个未初始化的对象。
出现这样的结果是因为new操作不是一个原子性操作,会发生指令重排序的情况。new的操作会有以下几个指令
- JVM分配一块内存A
- 在内存A上初始化对象
- 将内存A的地址赋给变量instance
但由于JVM会发生指令重排序,有可能顺序是1、3、2,这样的话会导致其他线程得到的对象是一个未初始化的对象,在调用getName()方法时会出现空指针异常。而volatile的作用就是禁止指令重排序,并且保证线程可见。这样就能解决这个问题。
懒汉式模式静态内部类写法
public class LazyInnerSingleton implement Serializable {
private LazyInnerSingleton(){}
public static LazyInnerSingleton getInstance(){
return InnerSingle.instance;
}
private static class InnerSingle{
private static LazyInnerSingleton instance = new LazyInnerSingleton();
}
}
这种写法是利用java语法的特性,静态内部类在启动时不会加载,而是在用到的时候才进行加载。因此如果没用到这个对象,那么这个对象就不会创建,只有调用getInstance()方法时才会加载InnerSingle这个类,进而实例化LazyInnerSingleton对象。
暴力反射破坏单例
虽然上述几个单例写法都声明了私有构造器,但是通过暴力反射能够破坏单例。
public class ReflectSingletonTest {
public static void main(String[] args) {
LazyInnerSingleton innerSingleton = LazyInnerSingleton.getInstance();
System.out.println(innerSingleton);
Class clazz = LazyInnerSingleton.class;
try {
Constructor c = clazz.getDeclaredConstructor(null);
//暴力反射
c.setAccessible(true);
Object obj = c.newInstance();
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果如下:
解决方式就是在私有构造器中加个判断即可,这样当第二次创建对象的时候就会直接抛出异常
private LazyInnerSingleton(){
if(InnerSingle.instance != null){
throw new RuntimeException("已有实例,禁止构建");
}
}
序列化破坏反射
即使在私有构造器中加了一个判断,但通过序列化与反序列还是能够破坏单例,如下述实例:
public class SerializableSingletonTest {
public static void main(String[] args) throws Exception {
LazyInnerSingleton lazyInnerSingleton = LazyInnerSingleton.getInstance();
FileOutputStream fos = new FileOutputStream("LazyInnerSingleton.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(lazyInnerSingleton);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyInnerSingleton.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
System.out.println(lazyInnerSingleton);
System.out.println(obj);
}
}
执行结果:
解决方式是加上在类中加上readResolve()方法
private Object readResolve(){
return InnerSingle.instance;
}
原因是因为readObject()方法会判断是否含有readResolve()方法,如有就使用这个方法返回的对象,如果没有就新建一个对象
注册式单例
注册式单例就是把实例登记到一个地方,使用唯一标识进行标记。主要有枚举式单例和容器式单例
枚举式单例
public enum EnumSingleton {
INSTANCE;
private String data;
public void setData(String data){
this.data = data;
}
public String getData(){
return this.data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
枚举式单例能够确定上下文中只有一个,并且都无法通过暴力反射和序列化反射来破坏单例。 首先看下暴力反射会出现什么情况,具体代码就不写了,将上述暴力反射代码修改下就行。直接看结果:
可以看到,这里直接报错了,大概意思就是没有这个方法(无参构造器)。这一因为枚举类他的父类是Enum,这个类中没有无参构造器,可以看下Enum类的源码,发现他只有一个带有两个参数的构造器,如下
那我们在反射时加入参数调试下:
调试结果如下:
这个错误明确的告知我们不能利用反射来创建枚举对象。所以,利用枚举来创建单例是最合适的。
容器式单例
虽然枚举式单例看起来比较合适,但是还是那个问题,一旦项目中需要大批量的单例,就会很耗内存资源,因为在加载的时候就会进行初始化。接下来看下容器式单例。
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> container = new HashMap<>();
public static Object getInstance(String instanceName){
if(!container.containsKey(instanceName)){
try {
Object obk = Class.forName(instanceName).newInstance();
container.put(instanceName,obk);
return obk;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return container.get(instanceName)
}
}
容器式单例适合创建大批量的单例,并且在需要的时候才会进行加载,还是比较适合的,但是这里同样有线程安全的问题,可以利用双重检查锁来解决。但是有个问题,就是这里如果使用ConcurrentHashMap有没有线程安全的问题呢?由于ConcurrentHashMap源码没怎么看过,这个问题暂时本人无法解决,之后找个时间看下其源码,然后在补充下,先在这里打个标识。
ThreadLocal单例
所谓ThreadLocal单例就是不能保证全局唯一,但能保证在单个线程中是唯一的,并且天然线程安全。他的写法是这样的。
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}
public static ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return threadLocal.get();
}
}