Java设计模式之单例设计模式

Java设计模式之单例模式

  1. 单例模式简介:

    单例模式是应用最广泛的模式之一,相信很多学过java的同学都会使用的这一种设计模式,在应用该模式时, 单例对象的类必须保证只有一个实例存在。因为在很多时候,我们整个系统只需要一个全局变量,这样有利于我们协调整体的行为。如果在一个应用中,该应用只有一个ImageLoader实例,而这个实例中又含有线程池、缓存系统、网络请求等,很消耗资源,因此我们就没有必要让它构造多个ImageLoader实例,这种不能自由构造对象的情况,就是单例模式的使用场景。

  2. 单例模式的定义:

    确保只有一个类只有一个实例,并且自身构造的实例可以给提供给整个系统使用。

  3. 单列模式的使用场景:

    确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个 ,例如创建一个对象需要消耗的资源过多,如果访问IO和数据库等资源,这时就要考虑使用单例模式。

  4. 单列模式UML类图:
    这里写图片描述
    角色介绍:
    (1)Client——高层客户端
    (2)Singleton——单例类

    实现单例模式主要有如下几个关键点:

    • 构造函数私有化(private),不对外开放
    • 通过一个静态方法或者枚举返回单例类对象
    • 确保单例类的对象有且只有一个,尤其在多线程的模式下
    • 确保单例类对象的反序列化时不会重新构建对象

    通过将单例类的构造方法私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类会
    暴露一个公有的静态方法 客户端通过该静态方法获取一个单例类的唯一对象,在获取这个单例对象的
    过程中需要确保线程安全,即在多线程环境下构造单例类的对象也是有且只有一个,这也是单例模式实
    现过程的一个难点,后面会将会设计到多线程下创建该实例对象。

  5. 单例模式的简单应用:

    比如一个学校,其中之允许有一个校长,可以有多个行政领导。

Leader.java

        /**
         * 领导
         */
        public abstract class  Leader {
            public abstract void work();
        } 

HeaderMaster.java

        /**
         * 校长
         */
        public class HeaderMaster extends Leader{
            private static final HeaderMaster master = new HeaderMaster();
            private HeaderMaster(){

            }
            public static HeaderMaster newInstance(){
                return master;
            }
            @Override
            public void work() {
                System.out.println("校长工作");
            }
        }

ClericalLeader1.java

        /**
         * 行政领导1
         */
        public class ClericalLeader1 extends Leader {
            @Override
            public void work() {
                System.out.println("行政领导1");
            }
        }

    ClericalLeader2.java

        /**
         * 行政领导2
          */
        public class ClericalLeader1 extends Leader {
            @Override
            public void work() {
                System.out.println("行政领导2");
            }
        }

School.java

        /**
         * 学校类
         */
        public class School {
            private List<Leader> leaders;
            public School(){
                leaders = new ArrayList<>();
            }
            public void addLeader(Leader leader){
                leaders.add(leader);
            }

            public void showLeader(){
                for (Leader leader : leaders) {
                    System.out.println(leader);
                }
            }
        }

Test.java

        public class Test {
            public static void main(String[] args) {
                School  school = new School();
                //获取校长实例
                HeaderMaster master1 = HeaderMaster.newInstance();
                HeaderMaster master2 = HeaderMaster.newInstance();
                //获取行政领导实例
                ClericalLeader1 leader1 = new ClericalLeader1();
                ClericalLeader2 leader2 = new ClericalLeader2();

                school.addLeader(master1);
                school.addLeader(master2);
                school.addLeader(leader1);
                school.addLeader(leader2);
                school.showLeader();
            }
        }
运行结果:

    com.itrealman.single.HeaderMaster@1540e19d
    com.itrealman.single.HeaderMaster@1540e19d
    com.itrealman.single.ClericalLeader1@677327b6
    com.itrealman.single.ClericalLeader2@14ae5a5

从上面的代码,我们可以看出,校长实例是不能new的,领导的实例可以通过new来构造一个实例,而
从输出的结果看 校长类的实例对象都是同一个对象,而leader1,leader2两个实例对象是不一样的。
这个实现的核心在于将HeaderMaster类的构造方法私有化,是的外部程序不能通过构造方法来构
造HeaderMaster对象,而是通过一个静态方法返回一个静态对象。 对于以上的代码,我采用的是
饿汉模式来实现的,所谓饿汉模式,就是在声明静态对象时就已经初始化了。

 
6. 懒汉模式

HeaderMaster.java

        /**
         * 校长
         */
        public class HeaderMaster extends Leader{
            private static HeaderMaster master;
            private HeaderMaster(){

            }
            public static HeaderMaster newInstance(){
                if(master == null){
                    master = new HeaderMaster();
                }
                return master;
            }
            @Override
            public void work() {
                System.out.println("校长工作");
            }
        }

懒汉模式就是在需要使用的时候加载该对象,在一定的程度上延时加载,节省了一定的内存。

 
7. 多线程创建单例模式:

HeaderMaster.java

    /**
     * 校长
     */
    public class HeaderMaster extends Leader{
        private static HeaderMaster master;
        private HeaderMaster(){

        }
        public static synchronized HeaderMaster newInstance(){
            if(master == null){
                master = new HeaderMaster();
            }
            return master;
        }
        @Override
        public void work() {
            System.out.println("校长工作");
        }
    }

在这里相信大家已经注意到了,在newInstance方法中加了一个synchronized关键字,也就是newInstance
是一个同步方法,这就是上面所说的在多线程的情况下保证单例对象唯一性的手段。细想一下,我们可能会发现
一个问题,及时master已经被初始化(第一次调用时就会被初始化master),每次用newInstance方法都会
进行同步,这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。

总结一下,懒汉单例模式的优点是单例只有在使用时才会被实例化,延时了对象的加载,在一定的程度上节约了资源;
缺点是第一次 加载时需要及时进行实例化,相对来说反应会稍慢点,最大的问题是每次调用newInstance都进行同步,
造成了不必要的同步开销,一般来说不建议使用这种模式。

8.DCL(Double Check Lock)实现单例

从上面的懒汉模式例子中我们可以看出其效率,资源消耗等一些缺点,那么我们有什么更好的办法去同时
解决能在一定的程度上节省资源,然后不造成成同步开销呢?答案是有的,那就是采用我们的DCL方式来
解决这一问题。

HeaderMaster.java

    /**
     * 校长
      */
    public class HeaderMaster extends Leader{
        private static HeaderMaster master;
        private HeaderMaster(){

        }
        public static HeaderMaster newInstance(){
            if(master == null){
                //断点区域
                synchronized  (HeaderMaster.class){
                    master = new HeaderMaster();
                }
            }
            return master;
        }
        @Override
        public void work() {
            System.out.println("校长工作");
        }
    }

从上面的这个例子来看,你是否觉得,我们每次调用newInstance时,只有初始化的时候调用了一次
同步代码块呢?那么以后的每一次我们都不需要去重复调用同步代码块,而造成不必要的开销,如果
对线程不了解的同学可能觉得上面的代码基本已经没有什么问题,其实不然,上面的代码还是存在安
全性的,那么哪里会存在安全性能,这里我为了方便,就在上加了一个断点区域,大家可以看一下,
当多线程调用newInstance方法获取单例实例时,当第一个线程判断master为null,他运行到断点
区域时,系统资源被第二个线程抢走了,那么此时线程一进入阻塞状态,当线程2又一次运行到if判断
语句时,由于第一个线程资源被抢夺,程序一直在断点区域停止,线程一的master并没有进入同步代码块,
所以master并没有实例对象,此时线程2的master也为空,则刚好可以通过if判断条件,当线程2执行完后,
就会获得一个实例对象了,那么此时线程2释放资源,线程1有获取了系统资源,从阻塞状态变成就绪状态,
然后开始执行,此时由于锁没有任何对象持有,所以他也进入了同步方法快,然后又获取了一个对象,
那么此时返回的就是另外一个对象了,所以你会发现,这里的单例对象不止一个,而是两个,这样就不
符合单例设计模式的原则了,为了解决这样的问题,我们通常要加双判断。

HeaderMaster.java

    /**
     * 校长
     */
    public class HeaderMaster extends Leader{
        private static HeaderMaster master;
        private HeaderMaster(){

        }
        public static HeaderMaster newInstance(){
            if(master == null){
                //断点区域
                synchronized  (HeaderMaster.class){
                    if(master == null){
                        master = new HeaderMaster();
                    }
                }
            }
            return master;
        }
        @Override
        public void work() {
            System.out.println("校长工作");
        }
    }

这样代码基本上已经达到了一定的安全性和效率性。

DCL的优点:资源利用率高,第一次执行newInstance时单例对象才会被实例化,效率高。缺点是
第一次加载时反应稍慢,也由于java内存模型的原因偶尔会失败,在高并发环境下也有一定的缺陷
虽然发生概率很小。不过DCL模式是使用的最多的单例实现方式了,它能够在需要的时才实例化单例
对象,并且能够在绝大多数场景下保证单例对象的唯一性,除非你的代码在并发场景比较复杂或低于
JDK6版本下使用,否则,这种方式一般都能够满足需求。

9.静态内部类加载单例模式

DCL虽然在一定的程度上解决了资源的消耗、多余的同步、线程安全等问题,但是,他还是在某些情况
下出现失效的问题。 这个问题被称为双重检查锁定(DCL)失效,在《Java并发编程实践》一书的最后
谈到了这个问题,并指出这种优化是不合理的,不赞成使用,而建议使用如下的代码代替:

HeaderMaster.java

/**
 * 静态内部类单例模式
 */
 public class HeaderMaster extends Leader{
    private HeaderMaster(){

    }
    public static HeaderMaster newInstance(){
        return SingleHolder.master;
    }
    private static class SingleHolder{
        private static final HeaderMaster master = new HeaderMaster();
    }
    @Override
    public void work() {
        System.out.println("校长工作");
    }
}

通过上面的代码可以看到,当第一次加载HeaderMaster类时并不会初始化master,只有在第一次调用HeaderMaster的newInstance方法时才会导致master被初始化,因此,第一次调用newInstance方法会导致虚拟机加载SingleHolder类,这种方式不仅能够确保线程安全,也能能够保证单例对象唯一性,同时也延时了单例的实例化,所以只是推荐使用的单例模式实现方式。
 
10.枚举单例
  在前面这几种单例模式实现方式中,这些实现不是稍显麻烦就是会在某些 情况下出现问题。还有没有更简单的实现方式呢?看看下面的实现:
  

public enum SingletonEnum {
    INSTANCE;
    public void doSometing(){
        System.out.println("do sth");
    }
}

  你没有看错,就是枚举,写法简单是枚举单例最大的有点,枚举在Java中与普通类是一样的,不仅能够有字段,还能有自己的方法,最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。在我们上述的几种情况中,在一个情况下他们会出现重新创建对象的情况,那就是反序列化。
  通过序列化可以将一个单例的实例对象写到磁盘中,然后再读回来,从而有效的获取一个实例。即使构造方法是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用了该类的构造方法。反序列化操作提供一一个很特别的钩子方法,类中具有一个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制反序列化。在上面的几个示例中,如果要杜绝单例对象在被反序列化时重新生成对象,那么必须加入如下的方法:
  

    private Object readResolve() throws ObjectStreamException {
        return master;
    }

也就是在readResolve中将master对象返回,而不是默认的重新生成一个新的对象,对于枚举,并不会存在这个问题,因为即使反序列化它也不会重新生成新的实例。下面看一下DCL反序列话时没有加readResolv方法的返回的单例对象和加了readResolve方法时的返回的单例对象。

ObjectOrder.java测试类:

public class ObjectOutputStreamDemo {
    public static void main(String[] args) {
        System.out.println("序列化单例对象前的地址:"+HeaderMaster.newInstance());
        try {
            //先序列化到本地G盘下的objectOrder.txt文件中
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("G:/objectOrder.txt"));
            oos.writeObject(HeaderMaster.newInstance());
            //然后从本地G盘的objectOrder.txt中反序列化回来
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("G:/objectOrder.txt"));
            Object o = ois.readObject();
            System.out.println("反序列化后的单例对象地址:" + o);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

没有加readResolve的DCL代码:

public class HeaderMaster extends Leader implements Serializable{
        private static HeaderMaster master;
        private HeaderMaster(){

        }
        public static HeaderMaster newInstance(){
            if(master == null){
                synchronized (HeaderMaster.class){
                    if(master ==  null)
                        master = new HeaderMaster();
                }
            }
            return master;
        }
        @Override
        public void work() {
            System.out.println("校长工作");
        }
    }

运行结果如下:

序列化单例对象前的地址:com.itrealman.single.HeaderMaster@1540e19d
反序列化后的单例对象地址:com.itrealman.single.HeaderMaster@7699a589

从这个结果就可以看出,反序列化之后的单例对象明显和没序列化之前的对象地址不相同

当我们在程序中加入readResolve()方法后:

public class HeaderMaster extends Leader implements Serializable {
    private static HeaderMaster master;

    private HeaderMaster() {

    }

    public static HeaderMaster newInstance() {
        if (master == null) {
            synchronized (HeaderMaster.class) {
                if (master == null)
                    master = new HeaderMaster();
            }
        }
        return master;
    }
    //关键语句***************************
    private Object readResolve() throws ObjectStreamException {
        return master;
    }
    //**********************************

    @Override
    public void work() {
        System.out.println("校长工作");
    }
}

运行结果如下:

序列化单例对象前的地址:com.itrealman.single.HeaderMaster@1540e19d
反序列化后的单例对象地址:com.itrealman.single.HeaderMaster@1540e19d

你会发现,反序列话之后,他任属于同一个对象。地址都是相同的。

 
11. 使用容器实现单例模式

在学习了上述各类单例模式的实现后,再来看看一种另类的实现,具体代码如下:
  

/**
 * 容器实现单例模式
 */
public class HeaderMaster extends Leader{
    private static Map<String,Object> objectMap = new HashMap<>();
    private HeaderMaster(){

    }
    public static void registerInstance(String key,Object instance){
        if(!objectMap.containsKey(key)){
            objectMap.put(key,instance);
        }
    }
    public static Object getInstance(String key){
        return objectMap.get(key);
    }
    @Override
    public void work() {
        System.out.println("校长工作");
    }
}

  在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也用户隐藏了具体实现,降低了耦合度。
  不管以哪种形式实现单例模式,它的核心原理都是将构造方法私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生产实例对象等问题。选择哪种实现方式取决于项目本身,如是否复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值