单例模式
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。 单例模式是创建型模式。ServletContext、ServletContextConfig、Spring中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;
}
}
懒汉式单例模式
为了解决饿汉式带来的内存浪费问题,于是出现懒汉式单例模式。
LazySimpleSingletion:
public class LazySimpleSingletion {
private LazySimpleSingletion(){}
private static LazySimpleSingletion lazy = null;
public static LazySimpleSingletion getInstance(){
if(lazy == null){
lazy = new LazySimpleSingletion();
}
return lazy;
}
}
但是这样写会有新的问题,多线程环境下,会出现线程安全问题。这时候可以加上 synchronized 使变成线程同步
public class LazySimpleSingletion {
private LazySimpleSingletion(){}
private static LazySimpleSingletion lazy = null;
public synchronized static LazySimpleSingletion getInstance(){
if(lazy == null){
lazy = new LazySimpleSingletion();
}
return lazy;
}
}
但是这时候又会有线程阻塞,大幅降低性能。那么有没有更好的方式,可以又线程安全又性能提升呢? 这就是双重检查锁机制的单例模式:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//检查是否要阻塞
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//检查是否要重新创建实例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
//指令重排序的问题 需要加 volatile 修饰
}
}
}
return instance;
}
}
但是用到 synchronized 关键字总归要上锁,对性能存在一定的影响。所以就没有更好的方案吗?我们可以从类初始化的角度来考虑,采用静态内部类的方式:
public class LazyStaticInnerClassSingleton {
// 使用LazyStaticInnerClassSingleton的时候,默认会初始化内部类
// 如果没有使用,则内部类不会加载
private LazyStaticInnerClassSingleton(){}
// static是为了使单例模式空间共享,保证不会重写、重载
public static final LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
// 默认不加载
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
这种方式兼顾饿汉式的内存浪费问题和 synchronized 的性能问题。但是这种方式反射可以破坏。优化一下代码。
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
以上便大功告成。但是看似完美的单例写法还是可能被破坏。
序列化破坏单例:
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘。下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
public class SeriableSingleton implements Serializable {
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来
//转化成一个Java对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
// 保证再序列化的情况下也能实现单例模式
private Object readResolve(){ return INSTANCE;}
}
想要保证再序列化的情况下也能实现单例模式,需要加上 readResolve() 方法即可。
具体原因可以看JDK源码 ObjectInputStream类的readResolve()
通过JDK源码可以看到,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有返回。如果创建对象的动作发生频率加快,就意味着内存分配开销也会增大。所以想从根本上解决问题,可以看下面的注册式单例。
注册式单例模式
注册式单例模式:(登记式单例模式)就是将每个实例都登记到某一个地方,使用唯一的标识获取实例。
枚举式单例模式
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 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();
}
}
经过调试发现,在主线程中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例。那么 ThreadLocal 是如何实现这一的效果呢?
单例模式为了达到线程安全的目的,会给方法上锁,以实际换空间。ThreadLocal将所有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
单例模式在源码中的应用
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
...
}
总结
单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。