本次简单的讲一下单例模式.在讲的过程当中,笔者会尽可能把这个模式讲清楚,讲详细,讲简单.同时也会给出目前使用单例模式的例子,并分析这些案例的实现方式.在这个过程当中,你会发现,小小的单例模式里却有着很大的学问在.java
单例模式是为了保证在一个jvm环境下,一个类仅有一个对象.通常来讲,每讲到单例模式,你们都会想起各类实现方式(好比:懒汉式,饿汉式),这些实现方式,是经过代码的设计来强制保证的单例,能够称为强制性单例模式.固然,经过文档,经过编码约束,也能够认为是实现了一个类仅有一个对象的效果.数据库
一般,项目中的具备链接功能的类(好比:数据库链接,网络链接,打印机的链接),具备配置功能的类,工具类,辅助系统类,会须要使用单例模式.这些类大可能是建立和销毁须要消耗大量的系统资源,或者不须要建立多个对象(对象之间无差异).设计模式
在Java中,建立单例模式都有两个必不可少的步骤缓存
基于以上两条,能够简单写出三种单例模式:安全
第一种写法:经过类的静态变量来持有一个该类的对象的引用,同时使用final关键字来阻止其被再次赋值.网络
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1(){}
public static Singleton1 getInstance(){
return INSTANCE;
}
}
复制代码
第二种写法:这种方法和第一种方法大同小异,一样是使用静态变量维护该类的引用,但将对象建立的放在了静态代码块中.多线程
public class Singleton2 {
private static final Singleton2 INSTANCE ;
static {
INSTANCE=new Singleton2();
}
private Singleton2(){}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
复制代码
第三种写法:使用静态变量维持类的对象的引用(这种状况下,因为java语法的限制,将没法使用final关键字),在获取对象的方法里对对象进行判断和建立.app
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){}
public static Singleton3 getInstance() {
if(null==instance){
instance=new Singleton3();
}
return instance;
}
}
复制代码
前两种,将对象的建立时机放在了类的初始化阶段,后面一种,则将对象的建立放在了类的使用阶段.前两种被称为饿汉式,第三种被称为懒汉式.饿汉式的优势是简单易懂,缺点是没有达到懒加载的效果。若是从始至终从未使用过这个实例,就会比较浪费链接资源和内存.jvm
但懒汉式也并不复杂,能够起到懒加载的效果.因而,读者可能更愿意使用懒汉式,或者其变种(好比具备双重检查锁的懒汉式).你的理由是,节省内存,懒加载,并且还很酷.ide
但事实又是如何呢?为了弄清楚这两种单例方式,须要简单回忆一下类的生命周期.
那么问题来了,何时会对类进行初始化呢?根据类的五个生命周期阶段,咱们只须要验证在建立对象以前的那些操做可以触发类的初始化就行.笔者使用jdk1.8,默认配置,进行了简单的实验.首先在构造方法里添加打印语句,打印“init”,而后再添加一个静态方法和一个静态变量.对Singleton1进行检验.
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
//添加打印语句
private Singleton1(){
System.out.println("init");
}
public static Singleton1 getInstance(){
return INSTANCE;
}
//静态方法
public static final void otherMethod(){
}
//静态变量
public static final int staticFiled=0;
}
复制代码
测试1:仅仅进行声明
//测试1:
public class Test {
public static void main(String[] args) {
System.out.println("-------start-------");
Singleton1 singleton1 = null;
if(null==singleton1){
System.out.println("singleton1 is null");
}
System.out.println("-------end-------");
}
/* out: * -------start------- * singleton1 is null * -------end--------- */
}
复制代码
从输出上看,仅仅声明,不会触发类的初始化阶段. 测试2:调用类的静态变量
//测试2:
public class Test {
public static void main(String[] args) {
System.out.println("-------start-------");
System.out.println(Singleton1.staticFiled);
System.out.println("-------end---------");
}
/* out: *-------start------- *0 *-------end--------- */ }
复制代码
从输出上看,仅仅调用类的静态变量,不会触发类的初始化阶段. 测试3:调用类的静态方法
//测试3
public class Test {
public static void main(String[] args) {
System.out.println("-------start-------");
Singleton1.otherMethod();
System.out.println("-------end-------");
}
/* out: *-------start------- * init *-------end------- */
}
复制代码
从输出上看,仅仅调用类的静态方法,会触发类的初始化阶段.
经过上面的三个例子,能够看出饿汉式,在某种状况下,也是能够表现出懒加载的效果,而且饿汉式简单,并且不会产生线程安全的问题,在某些状况下是能够代替懒汉式的.而且随着如今硬件的发展,懒汉式的节省内存的优势也能够慢慢的忽略不计了.
在设计上,懒汉式要优于饿汉式,在使用上,可以刚好解决问题的就是好的设计.
在多线程的状况下,懒汉式会有必定修改.当两个线程在if(null==instance)语句阻塞的时候,可能由两个线程进入建立实例,从而返回了两个对象,这是一个几率性的问题,一但出现,排查和定位问题都具备运气性.对此,咱们能够加锁,以保证每次仅有一个线程处于getInstance()方法中,从而保证了线程一致性.多线程下的单例模式能够为
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){}
public static synchronized Singleton4 getInstance() {
if(null==instance){
instance=new Singleton4();
}
return instance;
}
}
复制代码
Singleton4相对于Singleton3,只是在getInstance方法上加了一个锁(静态方法以Singleton4.class对象为锁,非静态方法锁以this对象为锁).从而保证了,每次仅有一个线程进入内部的代码快.试想,一个项目中如有100处获取实例,那么jvm就会有100次进行加锁,放锁的操做,但仅有一次实现了对对象的建立,jvm加锁放锁的操做都须要对对象头进行读写操做,每一次的操做都比较耗费资源.因此该方式实现的单例的模式的效率并不高.instance不为null的几率很是很是高,但又同时要兼容多个线程下的安全性,能够在外面再加一层的判断.能够写成下面的形式
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){}
private static synchronized void doGetInstance() {
if(null==instance){
instance=new Singleton4();
}
}
public static synchronized Singleton4 getInstance(){
if(null==instance){
doGetInstance();
}
return instance;
}
复制代码
简化一下代码,能够写成以下的形式:
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (null == instance) {
synchronized (Singleton5.class) {
if (null == instance) {
instance = new Singleton5();
}
}
}
return instance;
}
}
复制代码
上面的这种形式,也就是所谓的双重检查的单例模式写法,若是多个线程同时了经过了第一次检查,而且其中一个线程首先经过了第二次检查并实例化了对象,那么剩余经过了第一次检查的线程就不会再去实例化对象.即提高了效率,又能够显得很牛逼.
但上面的形式仍是有些的问题的.还记得上面说的类的生命周期吗?这里再详细展开说明类的链接过程.
类的链接过程又分为 验证阶段,准备阶段和解析阶段.验证阶段不用多讲,在这个阶段,jvm对类的合法性进行验证(不少基于jvm的语言都有本身的工具生成java字节码,好比clojure,kotlin等).
准备阶段,则是将类里的静态变量赋予默认值,解析阶段则是将符号引用转换为直接引用.同时若是一个类被直接引用,就会触发类的初始化(处于链接过程的下一个过程).总的来讲,一个类在碰到new关键字的时候,通常经历如下三个顺序:
可在实际的状况中,为了下降cpu的闲置时间,jvm每每对指令进行从新排序以造成指令流水线.也就是说以三个部署多是乱序的,可能为
所以上面的双重检查机制就会出现问题:可能返回一个未被彻底初始化的类.
public class Singleton5 {
private static volatile Singleton5 instance;
private Singleton5() {}
public static Singleton5 getInstance() {
if (null == instance) {
synchronized (Singleton5.class) {
if (null == instance) {
instance = new Singleton5();
}
}
}
return instance;
}
}
复制代码
讲到这里,能够再讲一下Singleton1(饿汉式)的写法,上面说过,这种写法的问题是在于没有达到懒加载的效果.但其具备清晰的结构,对线程友好的特色.若是可以在其结构上进行简单的改造,使其具备懒加载的效果,那就完美了!
public class Singleton7 {
private Singleton7(){}
private static class SingletonHolder {
private static Singleton7 INSTANCE = new Singleton7();
}
public static final Singleton7 getInstance() {
return SingletonHolder.INSTANCE;
}
}
复制代码
对比Singleton1,这里作了什么改变?仅仅是惟一的一个类的对象被静态嵌套类包裹了一下.要分析这种方式有没有实现懒加载,就要分析一下语句new Singleton7();是在何时被调用了. 当使用javac 进行编译Singleton7时,会生成三个class文件:
第一个文件能够忽略,是一个空壳文件(读者能够经过反编译插件查看源代码).能够看到静态嵌套类是单独做为一个class存在的,而其中建立对象的逻辑位于嵌套类中,jvm读取嵌套类的字节码之后才能建立对象.从硬盘中读取class文件,再在内存中分配空间,是一件费事费力的工做,因此jvm选择按需加载,没有必要加载的就不加载,不必分配就不分配.
因此Singleton7从链接和初始化的时候,不会去读取静态嵌套类的class文件,固然也就不能建立Singleton7对象.在调用getInstance时,jvm不得不去加载字节码文件,但不必定须要对类进行初始化.因此结论就是:用静态嵌套类包裹对象的建立过程,能够实现懒加载的同时,又不会让静态嵌套类进行初始化!下面开始实验验证.首先对Singleton7进行修改,加入日志:
public class Singleton7 {
private Singleton7(){
System.out.println("Singleton7");
}
private static final class SingletonHolder {
SingletonHolder(){
System.out.println("SingletonHolder");
}
private static final Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonHolder.INSTANCE;
}
}
复制代码
测试类:
public class Test {
public static void main(String[] args) {
System.out.println("-------start-------");
Singleton7.getInstance();
System.out.println("-------end---------");
}
/* out: *--------start------ *Singleton7 *-------end--------- */
}
复制代码
没有输出SingletonHolder!!!,这个说明了什么? 到这里彷佛就是要大结局了:咱们彷佛已经严格且完美实现了一个类在一个jvm环境下仅有一个对象了!!!但事实真是如此吗?
上面的单例,最主要的一步是将构造方法私有化,从而外界没法new对象.但java的反射能够强制访问private修饰的变量,方法,构造函数!因此:
//测试3 public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<Singleton7> singleton7Class = Singleton7.class;
Constructor<Singleton7> constructor= singleton7Class.getDeclaredConstructor();
//这里有个万恶的方法
constructor.setAccessible(true);
Singleton7 singleton1=constructor.newInstance();
Singleton7 singleton2=constructor.newInstance();
}
/**out * Singleton7 * Singleton7 */
}
复制代码
看来咱们的单例并非安全的.在java中,有四种建立对象的方式
方式 | 说明 |
---|---|
new | 须要调用构造函数 |
反射 | 须要调用构造函数,免疫一切访问权限的限制(public,private等) |
clone | 须要实现Cloneable接口,又分深复制,浅复制 |
序列化 | 1.将对象保存在硬盘中 2.经过网络传输对象,须要实现Serializable |
而上面介绍的各类单例模式,是不能抵抗反射,clone,序列化的破坏的.
如今考虑如何保护单例模式.对于clone和序列化,能够在设计的过程当中不直接或者间接的去实现Cloneable和Serializable接口便可.对于反射,一般来讲,使用普通类难以免(能够经过在调用第二次构造函数的方式进行避免,但这并非彻底之策,详情能够自行搜索相关内容).
枚举类是Java 5中新增特性的一部分,它是一种特殊的数据类型,之因此特殊是由于它既是一种类(class)类型却又比类类型多了些特殊的约束.枚举类可以实现接口,但不能继承类,枚举类使用enum定义后在编译时就默认继承了java.lang.Enum类,而不是普通的继承Object类.枚举类会默认实现Serializable和Comparable两个接口,且采用enum声明后,该类会被编译器加上final声明,故该类是没法继承的.枚举类的内部定义的枚举值就是该类的实例.除此以外,枚举类和普通类一致.所以能够利用枚举类来实现一个单例模式
public enum Singleton8
{
INSTANCE;
//该方法无关紧要
public static Singleton8 getInstance(){
return INSTANCE;
}
//.....other method
}
复制代码
这个就怎么可以防止反射破坏类呢?能够看一下下面的代码片断
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//反射在经过newInstance建立对象时,会检查该类是否ENUM修饰,若是是则抛出异常,反射失败。
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst; }
复制代码
上面截取的是java.lang.reflect.Constructor类的newInstance,能够看出,当当前类是枚举类型时,就会抛出异常,所以枚举类能够抗得住反射攻击!
既然枚举类默认实现了Serializable,那么就可以对枚举类进行序列化操做
public class Test2 {
public static void main(String[] args) throws Exception{
File objectFile =new File("Singleton8.javaObject");
Singleton8 instance1=Singleton8.INSTANCE;
Singleton8 instance2=null;
//序列化到本地
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
objectOutputStream.writeObject(instance1);
objectOutputStream.flush();
//反序列化到内存
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
instance2= (Singleton8) objectInputStream.readObject(); objectInputStream.close();
objectOutputStream.close();
//true,说明二者引用着同一个对象
System.out.println(Objects.equals(instance1,instance2));
}
}
复制代码
在复制操做上.枚举类直接继承java.lang.Enum类,而非Object类,没法实现复制操做.
到这里,单例模式的设计方式就告一段落了,下面再给出两个简单的小案例.
对于实现线程内的单例,直观的作法是利用一个map<Long,Singleton9>来存储对象.其中key能够为线程的ID,value为每一个线程下独有的对象.咱们能够作的更好,能够用ThreadLocal来作线程的变量隔离! 线程级单例设计以下
public class Singleton9 {
private Singleton9(){}
private static final ThreadLocal<Singleton9> threadHolder = new ThreadLocal<>(){
@Override
protected Singleton9 initialValue() {
return new Singleton9();
}
};
public static final Singleton9 getInstance(){
return threadHolder.get();
}
}
复制代码
Tomcat的Servlet在须要时被建立加载,之后的请求都将利用同一个Servlet对象.是一个典型的单例多线程模式,Tomcat里的StandardWrapper里的loadServlet可看的出来.对于单例多线程,注意如下问题
程序的设计模式,实用性大于理论性.因此,只要可以刚好的解决问题的设计模式就是好的设计模式.
笔者才疏学浅,上述内容只是我的的整理,请客观的阅读,对于错误的地方,还请读者可以及时给于指出和更正,欢迎一块儿讨论!