单例模式(Singleton Pattern)简介

定义

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点(静态方法)。单例模式属于创建型模式。常见的ServletContext、ServletContextConfig、ApplicationContext以及数据库的连接池等都是以单例形式存在的。

常见的单例模式

  • 饿汉式单例
    饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就是实例化了,不会存在访问安全问题。
    优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
    缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存空间。

代码示例如下:

public class HungrySingleton { 
	private static final HungrySingleton hungrySingleton = new HungrySingleton();
	private HungrySingleton(){}
	//全局访问点
	public static HungrySingleton getInstance(){ 
    	return hungrySingleton; 
	}
} 

还可以利用静态代码块来实现:

public class HungryStaticSingleton { 
	private static final HungryStaticSingleton hungrySingleton;
	static { 
		hungrySingleton = new HungryStaticSingleton(); 
	} 
	private HungryStaticSingleton(){} 
	public static HungryStaticSingleton getInstance(){ 
		return hungrySingleton; 
	}
} 
  • 懒汉式单例
    懒汉式单例是在被外部类调用时才创建实例。
    示例如下:
public class LazySimpleSingleton { 
	private LazySimpleSingleton(){} 
	private static LazySimpleSingleton lazy = null; 
	public static LazySimpleSingleton getInstance(){ 
		if(lazy == null){ 
			lazy = new LazySimpleSingleton(); 
		} 
	return lazy; 
	} 
} 

不过,上面的单例存在线程安全隐患,为了保证线程安全,我们可以采用双重检查锁的机制:

public class LazyDoubleCheckSingleton { 
	private volatile static LazyDoubleCheckSingleton lazy = null;
	private LazyDoubleCheckSingleton(){} 
	public static LazyDoubleCheckSingleton getInstance(){ 
		if(lazy == null){ 
			synchronized (LazyDoubleCheckSingleton.class){ 		
				if(lazy == null){ 
					lazy = new LazyDoubleCheckSingleton();
				} 
			}
		} 
		return lazy; 
	}
} 

但是,用到synchronized关键字,总归是要上锁,对程序性能还是存在一定影响的。我们可以从类初始化角度来考虑,采用静态内部类的方式:

public class LazyInnerClassSingleton { 
	private LazyInnerClassSingleton(){}
	public static final LazyInnerClassSingleton getInstance(){ 	 	
		return LazyHolder.LAZY; 
	}
	private static class LazyHolder{ 
		private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); 
	}
} 

静态内部类实现的单例模式是线程安全的,也不会在类加载时就创建实例,而是在调用getInstance()方法时才进行创建,达到了懒加载的效果。看起来好像是很完美的方法了,但是,我们依然可以通过反射或序列化来破坏单例
,代码如下:

public class ReflectTest {
    public static void main(String[] args) {
    	try {
            	Class<?> clazz = LazyInnerClassSingleton.class;
            	Constructor c = clazz.getDeclaredConstructor(null);
            	c.setAccessible(true);
            	Object instance1 = c.newInstance();
            	Object instance2 = c.newInstance();
            	System.out.println(instance1 == instance2);
        	} catch (Exception e){
            	e.printStackTrace();
        }
    }
}

运行结果如下:
在这里插入图片描述
通过结果看,这两个实例不是同一个,这就违背了单例模式的原则了。
那么,接下来就来了解下枚举式单例。

  • 枚举式单例
    枚举式单例模式是注册式单例的一种,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。枚举式可以有效避免反射和序列化的破坏,同时也是《Effective Java》书中推荐的一种单例实现写法,利用枚举的特性,不仅实现起来比较简单,还可以利用JVM来帮我们保证线程安全和单一实例的问题。
    代码实现如下:
public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){return INSTANCE;}
}

验证如下:
通过反射创建:

public class EnumSingletonTest {
    public static void main(String[] args) {
        try {
        	Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            Object o = c.newInstance();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

结果如下:
在这里插入图片描述很显然, Cannot reflectively create enum objects,jdk不允许通过反射来创建枚举类型。一旦使用反射,便会抛出异常。

通过序列化创建:

public class EnumSingletonTest { 
	public static void main(String[] args) { 
		try { 
			EnumSingleton instance1 = null;
			EnumSingleton instance2 = EnumSingleton.getInstance(); 
			instance2.setData(new Object());
			FileOutputStream fos = new FileOutputStream("EnumSingleton.obj"); 
			ObjectOutputStream oos = new 	
			ObjectOutputStream(fos); oos.writeObject(instance2); 	
			oos.flush();
		    oos.close();
			FileInputStream fis = new 		
			FileInputStream("EnumSingleton.obj"); 
			ObjectInputStream ois = new ObjectInputStream(fis); 
			instance1 = (EnumSingleton) ois.readObject(); 
			ois.close();
			System.out.println(instance1.getData()); 	
			System.out.println(instance2.getData()); 
			System.out.println(instance1.getData() == instance2.getData());
		}catch (Exception e){
		 	e.printStackTrace(); 
	}	
}
} 

结果如下:
在这里插入图片描述
但是枚举式单例也存在一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式单例模式的另一种写法,即容器式单例模式。

  • 容器式单例
    代码如下:
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    public static Object getInstance(String className){
        Object instance = null;
        if(!ioc.containsKey(className)){
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e){
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }
}

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。

  • ThreadLocal线程单例
    ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。下面我们来看代码:
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    private ThreadLocalSingleton(){}
    public static ThreadLocalSingleton getInstance(){
        return threadLocaLInstance.get();
    }
}

测试代码:

public class ThreadLocalSingletonTest {
    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}

结果我们会发现,在主线程main中无论调用多少次,获取到的实例都是同一个,在两个子线程中分别获取到了不同的实例。那么ThreadLocal是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值