创建型设计模式,确保一个类只有一个实例,并提供该实例的全局访问点。
使用一个私有构造函数,一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
实现
1、饿汉式-线程安全
public class singleton{
private static Singleton instance = new singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
优点:直接实例化instance不会产生线程安全问题,最简单、最经典,绝对线程安全。
缺点:直接实例化丢失了延迟实例化带来的节约资源的好处。
2、懒汉式-线程不安全
public class singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
优点:私有静态变量instance被延迟实例化,从而节约资源。
缺点:此实现在多线程环境下不安全,如果多个线程同时进入if(instance == null),且此时instance为null,那么会有多个线程执行instance = new Singleton();语句,将导致instance多次实例化。
3、懒汉式-线程安全
public class Singleton{
private static Singleton instance;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
优点:只需要对getInstance()方法加锁,就可以保证一个时间点只能有一个线程进入该方法,从而避免了多次实例化instance。
缺点:当一个线程进入该方法,其它试图进入该方法的线程都必须等待,即使instance已经实例化。这将导致线程阻塞时间过长,因此有性能问题,不推荐使用。
4、双重校验锁-线程安全
public class Singleton{
private volatile static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance==null){
instance = new Singelton();
}
}
}
return instance;
}
}
instance只需被实例化一次,之后就可以直接使用。加锁操作只需对实例化那部分代码加锁,只有当instance没有被实例化,才需要进行加锁。
双重校验锁先判断instance是否已经实例化,没有实例化才对实例化语句加锁。
if(instance == null){
synchronized(Singleton.class){
instance = new Singelton();
}
}
考虑上述实现,只使用一个if语句。在instance==null的情况下,如果两个线程都进入if语句块,虽然if语句块内有加锁操作,但两个线程都会执行instance = new Singelton();这条语句,只是先后问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个if语句;第一个if语句避免instance已经被实例化之后的加锁操作,而第二if语句进行了加锁,所以只有一个线程进入,就不会出现instance == null时两个线程同时进行实例化操作。
instance采用volatile关键字修饰也很有必要,instance=new Singleton();这段代码其实分三步执行:
- 1.为instance分配内存空间
- 2.初始化instance
- 3.将instance指向分配的内存地址
但是由于JVM具有指令重排的特性,执行顺序有可能变成1>3>2。指令重排在单线程环境下不会出问题,但在多线程环境下会导致一个线程获得还没初始化的实例。如,线程t1执行了1和3,此时t2调用getInstance()后发现instance不为空,因此返回instance,但此时instance还未被初始化。使用volatile可以禁止JVM的指令重排,保证在多线程环境下也能正常运行。
5、静态内部类实现
public class Singleton{
private Singleton(){
}
private static class SingletonHelper{
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHelper.instance;
}
}
当Singleton类被加载,静态内部类SingletonHelper没有被加载进内存。只有当调用getInstance()方法从而触发SingletonHelper.instance时SingletonHelper才会被加载,此时初始化instance实例,并且JVM能保证instance只被实例化一次。这种方法不仅具有延迟加载的好处,而且由JVM提供了对线程安全的支持。
6、枚举实现
在上述实现中,反射可以通过setAccessible()方法将私有构造函数的访问级别设置为public,然后调用构造函数从而实现实例化,也就是反射攻击,需要在构造函数中添加防止多次实例化的代码。
public enum Singleton{
INSTANCE;
private String objName;
public String getObjName(){
return objName;
}
public void setObjName(String objNmae){
this.objName = objName;
}
}
此实现可以防止反射攻击,它由JVM保证只会实例化一次。该实现无偿地提供了序列化机制,在多次序列化和反序列化之后,不会得到多个实例。而其它实现需要使用transient修饰所有字段,并且实现序列化和反序列化方法。
- 枚举类的特性
枚举类可以实现接口,但不能继承接口,也不能被继承;
枚举类是final的,所以不能继承;
枚举类的构造方法是私有的;
枚举成员是静态,final和public的;
枚举成员是枚举类的实例。 - 枚举反编译源码探求
自定义的枚举类会自动继承java.lang.Enum类,实现了序列化接口
每个成员变量都会被转换为public static final的枚举类型的实例
自动添加private的构造函数
7、注册式单例
spring中的单例采用此方式。
public class Singleton{
//构建采用ConcurrentHashMap,用于充当缓存注册表
private static final Map<String,Object> singletonMap = new ConcurrentHashMap<>();
//静态代码块只加载一次
static{
//实例化bean
Singleton instance = new Singleton();
//并注册到注册表,key为类的完全限定名,value为实例化对象。
singletonMap.put(instance.getClass().getName(),instance);
}
private Singleton(){
}
public static Singleton getInstance(String beanName){
if(beanName == null){
return null;
}
//从注册表中获取,如果没有直接创建
if(singletonMap.get(beanName)==null){
try{
//如果为空,通过反射进行实例化
singletonMap.put(beanName,Class.forName(beanName).newInstance());
}catch(Exception e){
e,printStackTrace();
}
}
//从缓存表中回去,如果缓存命中直接返回
return (Sigleton)singletonMap.get(beanName);
}
}
采用ConcurrentHashMap是出于线程安全的考虑。