单例模式:
或许最简单的设计范式就是“单子”(Singleton) ,它能提供对象的一个(而且只有一个)实例。单子在Java库中得到了应用,但下面这个例子显得更直接一些:
//: SingletonPattern. java
// The Singleton design pattern: you can
//never instantiate more than one.
//Since this isn't inherited from a Cloneable
//base class and cloneability isn't added,
//making it final prevents cloneability from
//being added in any derived classes:
final class Singleton{
private static Singleton s = new Singleton(47);
private int i;
private Singleton(int x){ i = x; }
public static Singleton getHandle(){
return s;
}
public int getValue(){ return i;}
public void setValue(int x){ i = x; }
}
public class SingletonPattern {
public static void main(String[] args) {
Singleton s = Singleton.getHandle();
System.out.println(s.getValue());
Singleton s2 = Singleton.getHandle();
s2.setValue(9);
System.out.println(s.getValue());
try{
//Can't do this:compile-time error.
//Singleton s3 = (Singleton)s2.clone();
}catch(Exception e){
}
}
}
创建单子的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构建器都设为private (私有),而且至少要创建一个构建器,以防止编译器帮我们自动同步一个默认构建器(它会自做聪明地创建成为“友好的”——friendly, 而非private)。
此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,getHandle() 会产生指向Singleton的一个句柄。剩下的接口(getValue()和setValue())属于普通的类接口。
Java也允许通过克隆(Clone) 方式来创建一个对象。在这个例子中,将类设为final可禁止克隆的发生。由于Singleton是从0bject直接继承的,所以clone ()方法会保持protected (受保护)属性,不能够使用它(强行使用会造成编译期错误)。然而,假如我们是从一个类结构中继承,那个结构已经过载了clone ()方法,使其具有public属性,并实现了Cloneable,那么为了禁止克隆,需要过载clone(),并掷出一一个CloneNotSupportedException (不支持克隆违例)。亦可过载clone(),并简单地返回this。那样做会造成一定的混淆,因为客户程序员可能错误地认为对象尚未克隆,仍然操纵的是原来的那个。
注意我们并不限于只能创建一个对象。亦可利用该技术创建一个有限的对象池。但在那种情况下,可能需要解决池内对象的共享问题。如果不幸真的遇到这个问题,可以自己设计一套方案,实现共享对象的登记与撤消登记。
单例模式定义:确保一个类最多只有一个实例,并提供一个全局访问点。
单例模式可以分为两种:预加载(饿汉模式)和懒加载(懒汉模式)
1.预加载(饿汉模式):
顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。
public class PreloadSingleton {
public static PreloadSingleton instance = new PreloadSingleton();
//其他的类无法实例化单例类的对象
private PreloadSingleton(){
};
public static PreloadSingleton getInstance() {
return instance;
}
}
很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。
2.懒加载(懒汉模式):
为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton(){
};
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
3.单例模式和线程安全
(1)预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。
(2)懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。
不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:
memory=allocate();//1:初始化内存空间
ctorInstance(memory);//2:初始化对象
instance=memory();//3:设置instance指向刚分配的内存地址
jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。
时间 | 线程A | 线程B |
t1 | A1:分配对象的内存空间 | |
t2 | A3:设置instance指向内存空间 | |
t3 | B1:判断instance是否为空 | |
t4 | B2:由于instance不为null,线程B将访问instance引用的对象 | |
t5 | A2:初始化对象 | |
t6 | A4:访问instance引用的对象 |
4.保证懒加载(懒汉模式)的线程安全
我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。
此外我们通过第三点的讨论知道new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。
public class penguin {
private static volatile penguin m_penguin = null;
// 避免通过new初始化对象
private void penguin() {}
public void beating() {
System.out.println("打墨墨");
};
public static penguin getInstance() {
if (null == m_penguin) {
synchronized(penguin.class) {
if (null == m_penguin) {
m_penguin = new penguin();
}
}
}
return m_penguin;
}
}
5.懒汉模式实现要点
- 单例使用volatile修饰;
- 单例实例化时,要用synchronized 进行同步处理;
- 双重null判断。
6.适用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
7.优缺点:
优点:
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
- 避免对共享资源的多重占用。
缺点:
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
8.volatile问题
大家有没有注意到,单例模式中用到了关键字volatile,在PHP和Go中没有类似的关键字,但是JAVA必须加,当初还有疑问,我们先看一下volatile的作用:
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
下面直接总结一下volatile的作用:
- 它会强制将对缓存的修改操作立即写入主存,让所有的线程可见;
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。