设计模式杂谈(1)单例模式。【使用频率:99.999%,学习难度
作者:solo陈,转载请注明出处。
个人主页:http://www.jianshu.com/users/5c2177416a84/latest_articles
设计模式是你在学习java道路上必须要学会掌握的,当然也并不是24种设计模式你都要掌握得很透彻。下面列出几项:《设计模式的好处》
一、有助于设计系统架构、增强系统健壮性、利于维护
二、有利于加强思考问题的思维、思考能力
三、有利于自己的编写代码、阅读代码的能力
当然你在编码设计过程中如果只是为了显摆一下你会某种设计模式这个是没必要的,因为过多的使用并不会给程序带来良好的阅读性,反而会增加系统的复杂度,所以应该因地制宜。其实如何在正确的地方正确的使用设计模式是初学者普遍会问的问题。比如LZ当时就会有这个的问题:学了单例模式,自己也会写,但是在什么情况下使用呢?这个就不得而知了,导致自己很多时候都很迷茫自己究竟有没有学习设计模式的必要。所以LZ写这个文章的初衷是将工作中使用到的结合起来进行说明:
下面先来说说单例模式。单例模式不论在自己编码或者源代码中都会遇见,所以此模式是你学习时候必须掌握的。该在什么时候使用呢?LZ在学习中也常常问自己,单例模式都有一个共性就是
1、这个类没有状态
有状态的类:类里面有成员变量,而且成员变量是可变的、比如struts2的action、要求是多例的、因为他是有状态的
无状态类:类里面没有成员变量,或者有成员变量但是不可变的、或者成员变量是单例的、比如struts1的action、可以是单例的。因为他是没有状态的
2、比如:工作上类似你一个AppServer类作为启动资源类并进行各种初始化工作,那么你的这个类就可以写出单例,因为这个类在使用的过程中并不需要每次都进行new来实例对象,我们只需要单独的一份就可以了。
说白了单例就是你new无数个其实都是一样的,并且在逻辑上如果new多个也会发生逻辑错误。
在网上该模式又分为懒汉、饿汉等几种形式。LZ就不进行细分了,因为LZ并不想在学习单例模式的时候再对文字进行理解记忆。下面就是几种逐渐进化的单例模式:
一、这种也是学习设计模式时老师讲解的最初级版本
//这是在不考虑并发访问的情况下标准的单例模式的构造方式,这种方式通过几个地方来限制了我们取到的实例是唯一的。
public class Singleton {
private Singleton(){}
private static Singleton singleton ;
public static Singleton getInstance (){
if(singleton == null){
singleton = new Singleton();
}
return singleton ;
}
}
这种写法是在初学是不考虑并发情况的构造方式,通过几点来确定获取唯一的实例:
1、使用private权限的构造器,使得客户端(使用者)不能够随意创建对象
2、使用static关键字来使得属性所指向的对象在每一个类中都是唯一的
3、static方法,使得客户端可以直接通过类.方法名调用。如果没有static就会使得客户端无法获取实例进行调用
上面的方法属于大学毕业阶段的代码,因为他并没有考虑到如果在多线程环境会造成的影响,如果多个线程(A\B)同时来访问getInstance这个方法,那么在if判断这里A线程判断为空,然而B线程正好在执行singleton = new Singleton(); 创建实例方法,但是并没有真正实例出对象,那么A线程也会继续执行singleton = new Singleton(); 创建实例方法。从而导致会创建多个实例。
二、有人说第一种是没考虑多线程那么就加锁
/*此种加锁方式会导致运行速度降低,当一个线程进行访问的时候,其余所有线程都将挂起等待
*/
public class StupidSynchronizedSingleton {
private StupidSynchronizedSingleton (){}
private static StupidSynchronizedSingleton badsingleton ;
public synchronized static StupidSynchronizedSingleton getInstance (){
if(badsingleton == null){
badsingleton = new StupidSynchronizedSingleton();
}
return badsingleton ;
}
}
由于该方法的做法实在是太愚蠢了,所以LZ给它取名Stupid。上面的做法是将getInstance ()进行同步来解决多线程问题,但是当访问getInstance ()时,其余的线程会进行挂起,造成无谓的等待,显然这种等待是没有必要的。
三、在第二种方法上适当的修改就可以解决那种无谓的等待了。
//这种实现方式实现了双重锁机制
/**
* 首先要明白在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
但是在new实例的时候有可能是先将对象分配给内存,在初始化。这个时候返回的synsingleton就会出现未知的错误
*
*/
public class SynchronizedSingleton {
private SynchronizedSingleton (){}
private static SynchronizedSingleton synsingleton ;
public static SynchronizedSingleton getInstance (){
if(synsingleton == null){//1
synchronized (SynchronizedSingleton.class) {
if(synsingleton == null){//2
synsingleton = new SynchronizedSingleton();
}
}
}
return synsingleton ;
}
}
上面方法相比较第二种方法做的同步就要正确得多了,并没有在方法上直接进行同步,而是判断了变量是否为null之后再进行同步,否则就直接进行返回,从而减少了在有实例情况下的等待时间。
假设synsingleton为null(1),此时有A/B线程同时执行到synchronized块,假设A线程抢占到资源再次执行synsingleton为null(2)当判断为true时,就进行实例化对象,否则返回。B之后进入同步块是同样。再次执行判断的原因是确保多个线程进入注释1后代码的逻辑正确性。从上面代码中可以看到有两次判断是否为null,这就是所谓的双重锁机制。
上面代码从表面上看是没有任何问题的,但是如果你了解JVM创建对象的逻辑步骤你就会发现上面做法也会出现问题。具体造成问题的原因看代码上面的注释。所以为了避免我们在创建对象时发生的此种问题,我们最好是交给JVM进行。
四、将创建对象的时机将给JVM加载类的时候进行(类加载这里就不详细说明了,有时间单独写一个我对类加载过程的理解的一篇文章)
/*属性为static的会在类加载的时候初始化,所以在初始化进行一半的时候,别的线程 *是无法使用的,这个是jvm保证的
*/
public class InnerSingleton {
private InnerSingleton (){};
public static InnerSingleton getInstance (){
return SingletonInstance.singleton;
}
private static class SingletonInstance {
private static final InnerSingleton singleton = new InnerSingleton();
}
}
首先static的成员变量会在类加载的时候进行初始化,所以singleton在代码调用之前就已经实例化好了。
五、枚举量来实现单例模式:
1、 自由序列化;
2、 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);
3、 线程安全;
public class EnumSingleton {
private EnumSingleton() {}
private enum InstanceHolder {
INSTANCE;
private EnumSingleton value;
private InstanceHolder() {
value = new EnumSingleton();
}
}
public static EnumSingleton getInstance() {
return InstanceHolder.INSTANCE.value;
}
}
枚举形式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,但是枚举是1.5版本之后的新特性,所以这种方式很少用到
以上就是java单例模式的各种写法,当然在实际运用中你可以使用四、五方法来创建,如果是应聘,面试官叫你写单例模式你可以都写出来然后讲出各自的优缺点,这样相信你对单例模式的掌握已经熟练了。
上面就是LZ对单例模式的理解,感谢各位的收看。这篇文章也是LZ在简书上写的第一篇,后续也会继续分享自己对Java知识的理解,当然LZ并不是什么大牛,也是在不断的学习过程中分享自己理解,有什么问题可以在文章下发留言进行交流。有错的地方LZ也会改正。谢谢!