(单例模式) singleton pattern
场景:
线程池/缓存/对话框/处理偏好设置/注册表/日志 打印机/显卡驱动 java的Calender
面临问题:
程序的行为异常
资源使用过量
结果不一致
全局变量:
程序开始时创建,对象消耗资源又使用的较少或者没有使用 ==> 资源浪费
利用静态类变量,静态方法,访问修饰符也可以实现同样效果
单例:
确保程序中使用的全局资源只有一份,管理共享的资源
现在实现一个巧克力工厂的锅炉
/** * 巧克力锅炉 * @author Dougest * 2017年6月28日 上午10:32:03 * */ public class ChocolateBoiler { private volatile boolean empty; private volatile boolean boiled;//沸腾 public ChocolateBoiler() { empty = true; boiled = false; } //空了就填东西 public void fill() { if(isEmpty()) { empty = false; boiled = false; } } //烧沸就排出来 public void drain() { if(!isEmpty() && isBoiled()) empty = true; } //开始煮 public void boil() { if(!isEmpty() && !isBoiled()) boiled = true; } public boolean isEmpty() { return empty; } public boolean isBoiled() { return boiled; } }
这个时候如果存在多余一个的锅炉就会发生很可怕的事,现在把他设计成
单件
public class ChocolateBoiler { private volatile boolean empty; private volatile boolean boiled;//沸腾 private ChocolateBoiler() { empty = true; boiled = false; } public static ChocolateBoiler getInstance(){ return new ChocolateBoiler(); } //空了就填东西 public void fill() { if(isEmpty()) { empty = false; boiled = false; } } //烧沸就排出来 public void drain() { if(!isEmpty() && isBoiled()) empty = true; } //开始煮 public void boil() { if(!isEmpty() && !isBoiled()) boiled = true; } public boolean isEmpty() { return empty; } public boolean isBoiled() { return boiled; } }
很简单
单例模式
确保一个类只有一个实例,并提供一个全局的访问点
注意:
单件模式的类也可以是一般的类,具有一般的数据和方法
getInstance()是一个静态方法,任何地方都可以访问
他可以延迟实例化
继续看上面的代码,尽管我们使用单件模式来改造他,但是多线程中fill()方法仍然可以在加热过程中加入原料,造成材料溢出
public class ChocolateBoiler { private volatile boolean empty; private volatile boolean boiled;//沸腾 private static ChocolateBoiler chocolateBoiler; private ChocolateBoiler() { empty = true; boiled = false; } public static synchronized ChocolateBoiler getInstance(){ if(chocolateBoiler == null) { chocolateBoiler = new ChocolateBoiler(); } return chocolateBoiler; }
改善
1 加锁似乎能够避免这个问题,但是一旦设置好chocolateBoiler 变量就不再需要同步这个方法了,之后的每次调用这个方法,
同步都是一种累赘(拖累性能==>同步一个方法会吧程序执行效率降低100倍)
我们试着改善多线程(如果你对getInstance()负担无所要求,没有频繁使用,可以不用考虑)
2 如果程序总是被创建并且使用单例对象或者在创建和运行时负担不是太重,可以"eagerly"的创建对象
public class Singleton { private static Singleton singleton = new Singleton(); private Singleton(){} public static Singleton getInstance() { return singleton; } }
利用这个做法,我们依赖JVM在加载这个类时马上创建此唯一实例
JVM保证在任何线程访问singleton静态变量之前,一定先创建此实例
3.使用双重检查加锁,在getInstance()中减少使用同步
double-checked locking,首先检查是否实例已经被创建了,如果未创建,才同步创建,这样一来,只有第一次同步,是我们想要的
public class Singleton { //volatile关键字 在1.4版本以上双重检查加锁失效 private volatile static Singleton singleton; private Singleton(){} public static Singleton getInstance() { if(singleton == null) {//不存在则进入同步块 synchronized(Singleton.class) { if(singleton == null) //不存在则实例化 singleton = new Singleton(); } } return singleton; } }
在这里我要特别解释一下上面代码中的volatile关键字
Java内存模型(jmm)并不限制处理器的重排序,执行 singleton = new Singleton();时,他可能并不是原子语句,执行过程中经历了三大步骤(详情可见下面反汇编内容):
1.为对象分配内存
2.初始化实例对象
3.引用instance指向分配的空间
处理器会进行指令重排序优化(关于处理器可见我另外一篇文章--
链接),所以并不能保证这三个步骤按顺序执行
当优化重排后执行顺序为1,3,2时,这样在线程1遇到3时,singleton 已经不是null值了;但是执行到2时判断判断singleton !=null,而直接返回singleton 引用,但是此时singleton 还没有初始化完毕,因此造成程序崩溃
而这个volatile的作用就相对避免了这种情况(但是不能百分之百避免)
至于这个关键字的作用,我就懒得自己总结了,贴一段国外资料--
链接:
Java中的volatile
Java支持volatile
关键字,但它被用于其他不同的用途。当volatile
用于一个作用域时,Java保证如下:
- (适用于Java所有版本)读和写一个volatile变量有全局的排序。也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。
- (适用于Java5及其之后的版本)volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁。
使用volatile
会比使用锁更快,但是在一些情况下它不能工作。volatile
使用范围在Java5中得到了扩展,特别是双重检查锁定现在能够正确工作。
上面的代码可以大大的减少getInstance()的时间消耗
有兴趣可以cmd切到文件夹下编译成class文件,然后使用javap命令反汇编看一下,如下
public class com.hola.design.singleton.Singleton {
public static com.hola.design.singleton.Singleton getInstance();
Code:
0: getstatic #2 //从类中获取字段
3: ifnonnull 37 //不等于null则跳转
6: ldc #3 // 把常量中的项压入栈顶
8: dup //复制栈顶部的一个字长的内容
9: astore_0 //将引用类型存入局部变量0中
10: monitorenter // 进入并获取对象监视器
11: getstatic #2 // 从类中获取字段
14: ifnonnull 27 //不等于null则跳转
17: new #3 // 创建类实例
20: dup //复制栈顶部的一个字长的内容
21: invokespecial #4 // 调用需要特殊处理的实例方法
24: putstatic #2 // 设置类中静态字段的值
27: aload_0 // 从局部变量0中装载引用类型值
28: monitorexit //释放并退出对象监视器
29: goto 37 //无条件跳转
32: astore_1 //将引用类型或returnAddress类型值存入局部变量1
33: aload_0 //将引用类型存入局部变量0中
34: monitorexit //释放并退出对象监视器
35: aload_1 //从局部变量1中装载引用类型值
36: athrow //跑出异常
37: getstatic #2 // Field singleton:Lcom/hola/design/singleton/Singleton; 从类中获取字段
40: areturn //从方法中返回引用类型的数据
Exception table:
from to target type
11 29 32 any
32 35 32 any
}
具体javap的东西可以自行我不多解释
这里讲一下我
个人对单例模式的理解
单例模式只是将一个对象做成唯一的存在,整个系统都能访问;
不管系统有多大,我们只存在这一个对象
为了保证这个对象被创建时只有一个,我们对他进行了特定的操作
这个对象本身没有什么迷惑的地方,我们创建他只是为了调用他的某个方法,使用他的某个功能
这个功能也可能会对资源操作,涉及到资源可能就会线程问题,涉及到线程问题就会想到并发,锁,同步异步等等
---个人理解结束---
问题:两个类加载器可能有机会创建各自的单例??
有可能 每个类加载器都定义了一个命名空间,如果有两个以上的类加载器有可能会加载同一个类 == > 从程序上看一个类会被加载多次
单例也会如此,如果程序中有多个类加载器就要小心了
解决办法:自行指定类加载器,并指定同一个类加载器
要点:
单件模式确保程序中一个类最多有一个实例
单件模式支持提供访问这个实例的全局点
java中实现单件模式一定要有私有构造器,一个静态方法,一个静态变量
确定性能和资源上的限制,然后小心的选择适当的方案实现单件,已解决多线程的问题(我们必须认定所有的程序都是多线程的)
jdk1.4以下双重检查加锁会失效
多个类加载器可能会导致多个单件实例
JVM1.2之前版本必须建立单件注册表,避免垃圾回收