高并发下线程安全的单例模式

3,168 阅读4分钟

前言

单例模式是为了确保一个类只存在一个实例,并为整个系统提供一个全局的访问点的一种设计模式。 所以其特点不言而喻:

  1. 在任何时候,单例有且只有一个实例
  2. 单例能够提供一种被全局调用的能力

应用场景

那么什么时候应该使用单例,或者说应该把什么样的类变成单例呢? 在操作系统中,具有资源管理器功能的服务,为了避免状态的不一致,通常会被设计成单例。例如在Android中,LayoutInflater就是一个单例,通过CS调用获取LayoutInflaterService从而加载布局。

实现方式

饿汉式单例(线程安全)

在方法未调用前就创建了单例,此种方式虽然线程安全但会造成一定程度上的内存浪费

public class EagerSingleton {  
        // jvm保证在任何线程访问uniqueInstance静态变量之前一定先创建了此实例  
        private static EagerSingleton uniqueInstance = new EagerSingleton();  
  
        // 私有的默认构造子,保证外界无法直接实例化  
        private EagerSingleton() {  
        }  
  
        // 提供全局访问点获取唯一的实例  
        public static EagerSingleton getInstance() {  
                return uniqueInstance;  
        }  
}

懒汉式单例(非线程安全)

只在要使用单例的时候才创建单例,此方法避免了单例提前创建带来的内存浪费,但也有不足,线程不安全。

public class LazySingleton {  
        private static LazySingleton uniqueInstance;  
  
        private LazySingleton() {  
        }  
  
        public static LazySingleton getInstance() {  
                if (uniqueInstance == null)  
                        uniqueInstance = new LazySingleton();  
                return uniqueInstance;  
        }  
}

加锁的懒汉式单例(线程安全但效率低下)

加入同步锁,保证多线程访问数据同步,但因同步锁的作用域过大,导致执行效率急剧下降。所以考虑缩小同步锁的作用域。

public class LazySafetySingleton {  
        private static LazySafetySingleton uniqueInstance;  
  
        private LazySafetySingleton() {  
        }  
  
        public synchronized static LazySafetySingleton getInstance() {  
                if (uniqueInstance == null)  
                        uniqueInstance = new LazySafetySingleton();  
                return uniqueInstance;  
        }  
}

DCL(Double Check Locking )双检锁单例模式(线程安全的前提下,效率较高也不浪费内存空间)

现今CPU有乱序执行的能力(JVM能够根据CPU特性,适当重新排序使机器指令更符合CPU的执行特点,最大限度发挥机器的性能)

其中volatile的功能:

  • 避免编译器将变量缓存在寄存器里
  • 编码编译器调整代码执行的顺序

如下,

  1. volatile就是为了保证uniqueInstance变量在线程间的可见性
  2. 外层检查避免每一次获取对象都对资源进行加锁,影响性能
  3. 内层的检查保证对象在并发时不会重复创建
public class DoubleCheckedLockingSingleton {  
        // java中使用双重检查锁定机制,由于Java编译器和JIT的优化的原因系统无法保证我们期望的执行次序。  
        // 在java5.0修改了内存模型,使用volatile声明的变量可以强制屏蔽编译器和JIT的优化工作  
        private volatile static DoubleCheckedLockingSingleton uniqueInstance;  
  
        private DoubleCheckedLockingSingleton() {  
        }  
  
        public static DoubleCheckedLockingSingleton getInstance() {  
                if (uniqueInstance == null) {  
                        synchronized (DoubleCheckedLockingSingleton.class) {  
                                if (uniqueInstance == null) {  
                                        uniqueInstance = new DoubleCheckedLockingSingleton();  
                                }  
                        }  
                }  
                return uniqueInstance;  
        }  
}

静态内置类实现单例模式

DCL解决了懒汉式单例多线程安全问题,使用以下这种方式也能达到同样的效果,

public class MySingleton {
	
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}

序列化与反序列化的单例模式实现

import java.io.ObjectStreamException;
import java.io.Serializable;
 
public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;
 
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
	
   /**
     * from stackoverflow: 
     * 
     * readResolve is used for replacing the object read from the stream. 
     * The only use I've ever seen for this is enforcing singletons:
     * when an object is read, replace it with the singleton instance. 
     * This ensures that nobody can create another instance by serializing and deserializing the singleton.
     * @return
     * @throws ObjectStreamException
     */
	protected Object readResolve() throws ObjectStreamException {
		System.out.println("called readResolve method!");
		return MySingletonHandler.instance; 
	}
}

static代码块实现单例

public class MySingleton{
	 
	private static MySingleton instance = null;
	 
	private MySingleton(){}
 
	static{
		instance = new MySingleton();
	}
	
	public static MySingleton getInstance() { 
		return instance;
	} 
}

枚举实现单例模式

public enum Foo {
    INSTANCE;
    public void print() {
        System.out.println("Hello world!");
    }
}

借用《Effective Java》中说的,

这种方法在功能上等同于公共领域方法,除了它更简洁,免费提供序列化机制,甚至在面对复杂的序列化或反射攻击时也提供了防止多重实例化的坚定保证。 尚未被广泛采用的单元素枚举类型是实现单例的最佳方法。

所以Java 5之后,还是推荐大家使用枚举来实现单例。