设计模式之单例模式(Singleton)
一、定义
一个类只有一个实例,它自己负责创建自己的对象,这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式有以下三个特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
如果要实现这三点,需要满足如下三个要求:
1、单例类中含有一个该类的私有的静态实例。
2、单例类只提供私有的构造函数。
3、单例类提供一个公有的静态的函数用于获取它本身的私有静态实例。
二、单例模式示例
1、单例模式分类
单例模式根据实例的创建时机来划分可分为:饿汉式和懒汉式。
饿汉式:应用刚启动的时候,不管外部有没有调用该类的实例方法,该类的实例就已经创建好了。
懒汉式:应用刚启动的时候,并不创建实例,等到第一次被使用时,才创建该类的实例。
2、举例说明
(1)、饿汉式(线程安全,可用)
1 public class Singleton {
2 // 私有的静态实例
3 private final static Singleton instance = new Singleton();
4
5 // 私有的构造函数,防止在该类外部通过new创建实例
6 private Singleton(){
7 System.out.println("创建Singleton实例!");
8 }
9
10 // 公有的静态的函数,用于获取实例
11 public static Singleton getInstance(){
12 return instance;
13 }
14}
15
16public class Test {
17 public static void main(String[] args) {
18 Singleton instance1 = Singleton.getInstance();
19 System.out.println(instance1);
20
21 Singleton instance2 = Singleton.getInstance();
22 System.out.println(instance2);
23
24 Singleton instance3 = Singleton.getInstance();
25 System.out.println(instance3);
26 }
27}
程序运行结果:
创建Singleton实例!
com.zxj.test.Singleton@154617c
com.zxj.test.Singleton@154617c
com.zxj.test.Singleton@154617c
观察程序运行结果:只创建了一个实例,三次调用获取到的实例都是同一个。
饿汉式优点:
在类加载的时候就完成了实例化,避免了多线程的同步问题。
饿汉式缺点:
在外部没有使用到该类的时候,该类的实例就创建了,若该类的实例的创建比较消耗系统资源,并且外部一直没有调用该实例,那么这部分的系统资源的消耗是没有意义的。
(2)、懒汉式(线程不安全,不可用)
(a).单线程环境下,是没有问题的:
1public class Singleton {
2 private static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("创建Singleton实例!");
6 }
7
8 public static Singleton getInstance(){
9 if(instance == null){
10 instance = new Singleton();
11 }
12 return instance;
13 }
14}
15
16public class Test {
17 public static void main(String[] args) {
18 Singleton instance1 = Singleton.getInstance();
19 System.out.println(instance1);
20
21 Singleton instance2 = Singleton.getInstance();
22 System.out.println(instance2);
23
24 Singleton instance3 = Singleton.getInstance();
25 System.out.println(instance3);
26 }
27}
程序运行结果:
创建Singleton实例!
com.zxj.test2.Singleton@154617c
com.zxj.test2.Singleton@154617c
com.zxj.test2.Singleton@154617c
(b).多线程环境下,单例模式失效,程序中有多个实例:
1public class Test {
2 public static void main(String[] args) {
3 for(int i = 0; i < 10; i++){
4 new Thread(){
5 @Override
6 public void run(){
7 Singleton instance = Singleton.getInstance();
8 System.out.println(instance);
9 }
10 }.start();
11 }
12 }
13}
程序运行结果:
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
创建Singleton实例!
com.zxj.test2.Singleton@1329a4
com.zxj.test2.Singleton@10ada9c
com.zxj.test2.Singleton@16268e5
com.zxj.test2.Singleton@95e4d4
com.zxj.test2.Singleton@2191b2
com.zxj.test2.Singleton@173a9e8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1122c7b
com.zxj.test2.Singleton@16ab049
com.zxj.test2.Singleton@15cc318
我们来分析一下,为什么在多线程环境下,懒汉式单例模式会失效?
假设有两个线程,线程A和线程B,线程A执行到第10行,但是并没有执行这一行,这个时候,线程B执行到第9行,线程B判断结果是true,线程B执行第10行,new了一个对象,此时,线程A,这就造成执行个对象被new了两次。
假设有两个线程,线程A和线程B,线程A先得到CPU的执行权,线程A执行到第9行 ,由于instance之前并没有实例化,所以线程A判断结果为true,线程A还没有来得及执行第10行,CPU执行权就被线程B抢去了,线程B执行到第9行,由于instance之前并没有实例化,所以线程B判断结果为true,此时线程B new了一个实例。之后CPU执行权分给线程A,线程A接着执行第10行实例的创建。由此看到线程A和线程B分别创建了一个实例(存在2个实例了),这就导致了单例的失效。
那么如何保证懒汉式在多线程环境仍然保证只有一个单例呢?当然是在创建实例时进行同步控制。
(3)、懒汉式(线程安全,可用)
1public class Singleton {
2 private static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("创建Singleton实例!");
6 }
7
8 // 对整个获取实例的方法进行同步
9 public static synchronized Singleton getInstance(){
10 if(instance == null){
11 instance = new Singleton();
12 }
13 return instance;
14 }
15}
这种写法,对获取实例的整个方法用synchronized关键字进行方法同步,保证了同一时刻只能有一个线程能够访问并获得实例。
但是缺点也很明显,这里锁住的是整个方法,锁的粒度太大,造成效率低下,那应该怎么办呢?减小锁的粒度,只把创建实例这块代码上锁,见下面的双重校验锁方式。
(4)、双重校验锁(DCL,double-checked locking,线程安全,可用)
双重校验锁,双重校验说的是两个空值判断,锁说的是synchronized。
(a).双重校验锁代码
1public class Singleton {
2 private static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("创建Singleton实例!");
6 }
7 // 对必要的代码块(创建实例的代码块)进行同步
8 public static Singleton getInstance(){
9 if (instance == null) {
10 synchronized (Singleton.class) {
11 if (instance == null) {
12 instance = new Singleton();
13 }
14 }
15 }
16 return instance;
17 }
18}
1public class Test {
2 public static void main(String[] args) {
3 for(int i = 0; i < 10; i++){
4 new Thread(){
5 @Override
6 public void run(){
7 Singleton instance = Singleton.getInstance();
8 System.out.println(instance);
9 }
10 }.start();
11 }
12 }
13}
程序运行结果:
创建Singleton实例!
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
com.zxj.test2.Singleton@1a1b9f8
(b).关于双重校验锁的两个疑问
从程序运行结果可以看到,程序中只有一个实例。关于双重校验锁,小伙伴们一定会有一些疑问,下面我们就来共同一一解答。
问题1:既然有了一次空值判断,为什么还要再加一层空值判断呢?
答案:这主要涉及线程安全的问题。
这个问题的意思,就是在第9行进行实例非空判断之后,进入synchronized代码块之后就不必再进行一次非空判断了,我们来看一下,如果这样做的话,会产生什么问题?
假设有两个线程,线程A和线程B,线程A先得到CPU的执行权,在执行到第9行时,由于之前没有实例化,所以线程A判断结果为true,然后线程A获得锁进入synchronized代码块里面。
此时线程B争抢到CPU的执行权,并执行到第9行,此时线程A还没有执行实例化动作,所以此时线程B判断为true,线程B想进入同步代码块,但是发现锁还在线程A手里,所以B只能在同步代码块外面等待。
此时线程A得回CPU执行权,执行实例化动作并返回该实例,然后释放锁。
线程A释放了锁,线程B就获得了锁,若此时不进行第二次非空判断,会导致线程B也实例化创建一个实例,然后返回自己创建的实例,这就导致了2个线程访问创建了2个实例,导致单例失效。
若进行第二次非空判断,线程B发现线程A已经创建了实例,则直接返回线程A创建的实例,这样就避免了单例的失效。
问题2:即便将第9行的空值判断去掉,在多线程环境下单例模式仍然有效,那为什么多次一举加上第9行代码呢?
答案:这主要涉及多线程下的效率问题。
使用synchronized关键字进行同步,意味着同一时刻只能有一个线程执行同步块里面的代码,还要涉及到锁的争夺、释放等问题,是很消耗资源的。如果我们不加第9行,即不在进入同步块之前进行非空判断,如果之前已经有线程创建了该类的实例了,那每次访问获取实例的方法都会进入同步块,这会非常的耗费性能。如果在进入同步块之前加上非空判断,如果之前已经有线程创建了该类的实例了,那就不必进入同步块了,直接返回之前创建的实例即可,减少synchronized操作次数,从而提高 程序性能。
(c).DCL失效问题,以及解决方案
在第12行,instance = new Singleton();,其实这行代码在JVM里面的执行分三步:
1.在堆内存中分配内存空间给这个对象。
2.初始化这个对象。
3.设置instance指向刚分配的内存空间。
由于jvm的指令重排序功能,可能在2还没执行时就先执行了3,此时第12行就变成:
1.在堆内存中分配内存空间给这个对象。
2.设置instance指向刚分配的内存空间。
3.初始化这个对象。
假设有两个线程,线程A和线程B,并且第12行代码发生指令重排序。
线程A先获取到CPU执行权,并执行到第9行。
此时线程B争抢到CPU执行权,并执行完第12行的第2个步骤。
此时线程A夺回CPU执行权,由于线程B执行第12行的第2个步骤,instance已经非空了,它会被线程A直接拿来用,这样的话,就会出现异常,因为instance只是一个引用,它指向的对象还没有实例化呢,自然会出现问题了。这个就是著名的DCL失效问题。
解决DCL失效问题,有两种方案,一是使用volatile关键字禁止指令重排序,二是使用静态内部类实现单例模式。
使用volatile关键字禁止指令重排序:
1public class Singleton {
2 private volatile static Singleton instance = null;
3
4 private Singleton(){
5 System.out.println("创建Singleton实例!");
6 }
7 // 对必要的代码块(创建实例的代码块)进行同步
8 public static Singleton getInstance(){
9 if (instance == null) {
10 synchronized (Singleton.class) {
11 if (instance == null) {
12 instance = new Singleton();
13 }
14 }
15 }
16 return instance;
17 }
18}
(5)、静态内部类 (可用,推荐)
1public class Singleton {
2 private Singleton() {}
3
4 private static class SingletonInstance {
5 private static final Singleton instance = new Singleton();
6 }
7 // 静态内部类
8 public static Singleton getInstance() {
9 return SingletonInstance.instance;
10 }
11
12}
使用静态内部类实现单例模式,是在实际开发中最推荐的写法。
问题1:静态内部类,如何保证延迟加载?
当Singleton类第一次被加载时并不会立即实例化,而是当getInstance()方法第一次被调用时,才会去加载SingletonInstance类,从而完成Singleton的实例化。
问题2:静态内部类,如何保证程序中只有一个实例?
因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次。
问题3:如何保证静态内部类实现的单例模式在多线程环境是生效的?
有两点原因,一是虚拟机规范规定,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
综合这两点以及第5行代码,在多线程环境,只会有一个线程创建一个实例,其他线程用的都是这个线程创建的这个实例。
(6)、枚举 (可用、推荐)
public enum Singleton {
INSTANCE;
}
这种方式是《Effective JAVA》作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
单元素的枚举类型已经成为实现Singleton的最佳方法
-- 出自 《effective java》
(7)、在实际开发中,选择哪种单例模式的实现方式?
一般情况下,不建议使用第2种和第3种懒汉方式,建议使用第1种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第5种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第6种枚举方式。如果有其他特殊的需求,可以考虑使用第4种双重校验锁方式。
三、单例模式优点
(1)在内存里只有一个实例,减少了内存开销,特别是一个对象需要频繁创建和销毁,而且创建和销毁时的性能又无法优化时,这个时候,把它设置成单例可以提高系统性能。
(2)可以避免对资源的多重占用,比如对一个文件进行写操作,由于只有一个连接实例存在内存中,可以避免对一个资源文件同时进行写操作。
(3)设置全局访问点,严格控制访问。例如Web应用的页面计数器就可以用单例模式来实现,从而保证计数的准确性。
四、单例模式缺点
(1)不适用于变化频繁的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例模式就会引起数据的错误,不能保存彼此的状态。
(2)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(3)单例类的职责过重,在一定程度上违背了“单一职责原则”。
(3)如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,可能会导致对象状态的丢失。
五、单例模式的使用场景
(1)要求一个类在程序的生命周期当中只有一个实例。比如在Spring框架项目中,若某个类只需要有一个实例,那么只要把Spring配置文件该类的
(2)需要频繁实例化然后销毁的对象,也就是频繁的 new 对象,可以考虑用单例模式替换。
(3)创建对象时耗时过多或者耗资源过多,但又经常用到的对象,比如数据库连接。
下面我们举个例子,讲解单例模式的使用:网站在线人数统计
其实就是一个全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现(利用单例模式的第三个优点:全局访问)。
public class Counter {
// 使用静态类实现单例模式
private static class CounterHolder{
private static final Counter counter = new Counter();
}
// 私有构造器
private Counter(){
System.out.println("init...");
}
// 获取实例的方法
public static final Counter getInstance(){
return CounterHolder.counter;
}
// 使用AtomicLong类型存储计数
private AtomicLong online = new AtomicLong();
// 获取计数
public long getOnline(){
return online.get();
}
// 计数加1
public long add(){
return online.incrementAndGet();
}
}