创建型模式之单例模式
创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程,即创建对象的同时隐藏创建逻辑。
1、使用场景
什么是单例模式呢,单例模式(Singleton)又叫单态模式,它出现目的是为了保证一个类在系统中只有一个实例,并提供一个访问它的全局访问点。从这点可以看出,单例模式的出现是为了可以保证系统中一个类只有一个实例而且该实例又易于外界访问,从而方便对实例个数的控制并节约系统资源而出现的解决方案。
使用单例模式当然是有原因,有好处的了。在下面几个场景中适合使用单例模式:
1、有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;
2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
3、频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;
4、有状态的工具类对象。
-
1、网站的计数器,通过单例模式可以很好实现
-
2、配置文件访问类
项目中经常需要一些环境相关的配置文件,比如短信通知相关的、邮件相关的。如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能,如果用单例模式,则只需要读取一遍就好了。
如果我们使用Spring框架,可以用 @PropertySource 注解读取properties文件,默认就是单例模式。public class SingleProperty { private static Properties prop; private static class SinglePropertyHolder{ private static final SingleProperty singleProperty = new SingleProperty(); } /** * config.properties 内容是 test.name=kite */ private SingleProperty(){ System.out.println("构造函数执行"); prop = new Properties(); InputStream stream = SingleProperty.class.getClassLoader() .getResourceAsStream("config.properties"); try { prop.load(new InputStreamReader(stream, "utf-8")); } catch (IOException e) { e.printStackTrace(); } } public static SingleProperty getInstance(){ return SinglePropertyHolder.singleProperty; } public String getName(){ return prop.get("test.name").toString(); } public static void main(String[] args){ SingleProperty singleProperty = SingleProperty.getInstance(); System.out.println(singleProperty.getName()); } }
-
3、数据库连接池、线程池
数据库连接池的实现,也包括线程池。为什么要做池化,是因为新建连接很耗时,如果每次新任务来了,都新建连接,那对性能的影响实在太大。所以一般的做法是在一个应用内维护一个连接池,这样当任务进来时,如果有空闲连接,可以直接拿来用,省去了初始化的开销。用单例模式,正好可以实现一个应用内只有一个线程池的存在,所有需要连接的任务,都要从这个连接池来获取连接。如果不使用单例,那么应用内就会出现多个连接池,那也就没什么意义了。如果你使用 Spring 的话,并集成了例如 druid 或者 c3p0 ,这些成熟开源的数据库连接池,一般也都是默认以单例模式实现的。
2、volatile双重检查锁机制
public class SingleInstance {
private static volatile SingleInstance singleInstance;
private Singleton(){};
public static SingleInstance getSingleInstance(){ //1
//非空则跳过,因为只有首次初始化才有安全问题,保证了初始化之后,线程不会阻塞,提高了性能
if (singleInstance == null) { //2. 以一次检查
synchronized(SingleInstance.class){ //3 加锁
//voliatile能保证可见性,但不能保证原子性,加锁保证线程并发情况下,也只有一个实例
if (singleInstance == null) { //4 第二次检查
singleInstance = new SingleInstance();//5 创建实例
}
}
}
return singleInstance;
}
}
注:原子性,简单的说,就是多线程下,简单的赋值和读取操作,具体请参考Java内存模型
优缺点:
- 双重检查锁机制,兼顾性能与安全,初始化之后,不会发生线程阻塞
这是单例模式优先推荐的写法
Java指令执行的过程:
- 1.将变量从主存复制到线程的工作内存中;
- 2.然后进行读操作;
- 3.有赋值指令时进行赋值操作;
- 4.将结果写入主存中;
以上4步都是原子性的,但组合到一起,多线程操作时不能保证整体原子性,这也就是线程并发安全问题的原因。
volatile修饰词作用:
- 1.某一线程对volatile修饰的变量进行修改后,会强制将结果写入主存,并使其它线程缓存行失效(失效后,读操作不能从工作内存中直接读取,从步骤1开始),即保证3和4指令执行过程的整体原子性,并通知其它线程。
- 2.禁止指令重排(代码的编写顺序和指令执行的顺序不一致),一定程度上保证了有序性。
上述的singleInstance类变量假如没有用volatile关键字修饰的,则会导致这样一个问题:
多线程情况下,在B线程执行第5步时,A线程执行到第4步,由于重排序的原因,B线程还没有完成instance引用的对象的初始化,但是A线程已经读取到singleInstance不为null,这时候就会导致空指针异常。
第5步的代码创建了一个对象,这一行代码可以分解成3个操作:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
根源在于代码中的2和3之间,可能会被重排序。例如:
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
这在单线程环境下是没有问题的,但在多线程环境下会出现问题,像上面的,B线程会看到一个还没有被初始化的对象。
2和3的重排序不影响线程A的最终结果,但会导致线程B在第二步判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。
注:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
面试中有时会让现场手写一个简单的单例模式,如果能写出volatile方式,并且能把volatile机制讲清楚,会加分不少。
3、恶汉式
class SingleInstance2 {
private static final SingleInstance3 singleInstance3 = new SingleInstance3();
public static SingleInstance3 getSingleInstance3() {
return singleInstance3;
}
}
优缺点:
- 优点:既能保证并发安全,也能保证性能
- 缺点:类加载时就初始化,浪费内存
4、加锁懒汉式
class SingleInstance3 {
private static SingleInstance2 singleInstance2;
public static synchronized SingleInstance2 getSingleInstance2(){
if (singleInstance2 == null) {
singleInstance2 = new SingleInstance2();
}
return singleInstance2;
}
}
优缺点:
- 缺点:牺牲了性能(初始化后,仍会发生线程阻塞问题),保证了并发安全