由简入繁阐述单例模式

一. 前言

在常用模式中,单例模式是唯一一个能够用短短几十行代码完整实现的模式,所以,单例模式常常出现在面试题中. 在此,在前人的基础上,对其做个总结.

本文主要围绕以下几个问题展开:
1. 单例模式是什么? (what)
2. 什么时候会用到? 使用过程中,单例模式有什么优势? (why)
3. 怎么实现单例模式? (how)

二. 概述及应用场景

1. 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。这就提出了一个问题:如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?客户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是类设计者的责任,而不是类使用者的责任。

从另一个角度来说,单例模式其实也是一种职责型模式。因为我们创建了一个对象,这个对象扮演了独一无二的角色,在这个单独的对象实例中,它集中了它所属类的所有权力,同时它也肩负了行使这种权力的职责!

2. 日常生活中的例子

  • 我们使用的电脑下的回收站就是典型的例子。在整个系统运行过程中,回收站一直维护着仅有的一个实例.
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步.
  • 还有应用程序的日志,日志是共享的,因为只有一个实例去操作,所以内容才同步.

从以上可看出,
单例模式应用场景一般具备以下条件:

(1) 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置等等.
(2) 控制资源的情况下,方便资源之间的互相通信。如线程池等。

3. 使用单例的优点

  • 单例类只有一个实例
  • 共享资源,全局使用
  • 节省创建时间,提高性能

不足:

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

三. 模型图

这里写图片描述

四. 实现Singleton模型的多种解法

1.懒汉式,线程不安全

由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以禁止他人创建实例.我们定义一个静态的实例,在需要的时候创建该实例.

public class Singleton(){
    private static Singleton instance;
    private Singleton (){}

    public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
    }
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

2.懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不完美.我们每次线程调用getInstance() 方法时,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免.

3.双重检验锁

我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例,而当实例已经创建之后,我们不需要再做加锁操作.于是,对第二种解法可以做进一步改进:

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

这种实现方式对多线程来说是安全的,同时线程不是每次都加锁,只有判断对象实例没有被创建时它才加锁. 但是这样的代码实现起来比较复杂,容易出错,是否有更优秀的解法.

4.饿汉式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的

public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();

    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

这种方式和名字很贴切,饥不择食,在类装载的时候就创建,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。

5.静态内部类

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

public class Singleton { 
    private Singleton(){
    }

    public static Singleton getInstance(){  
        return SingletonHolder.Instance;  
    }  
    private static class SingletonHolder {  
        private static final Singleton Instance = new     Singleton();  
    }  
}

第一次加载Singleton类时并不会初始化Instance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化Instance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

6.枚举

《Effective Java》中作者推荐了一种更简洁方便的使用方式,就是使用「枚举」。

public enum Singleton {
     //定义一个枚举的元素,它就是 Singleton 的一个实例
     INSTANCE;  

     public void doSomeThing() {  
         // do something...
     }  
 }

使用方法如下:

public static void main(String args[]) {
    Singleton singleton = Singleton.instance;
    singleton.doSomeThing();
}

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

五. 代码实现

这是一个简单的计数器例子,四个线程同时进行计数。

package singleton;
/**
 * 执行线程
 * @author dingding
 *
 */

public class CountClient {
    public static void main(String[] args) {
        CountMutilThread cmt0 = new CountMutilThread("Thread 0");
        CountMutilThread cmt1 = new CountMutilThread("Thread 1");
        CountMutilThread cmt2 = new CountMutilThread("Thread 2");
        CountMutilThread cmt3 = new CountMutilThread("Thread 3");
        CountMutilThread cmt4 = new CountMutilThread("Thread 4");

        cmt0.start();
        cmt1.start();
        cmt2.start();
        cmt3.start();
        cmt4.start();

    }

}
package singleton;


/**
 * 多线程计数
 * @author dingding
 * Date:2017-5-27
 */

public class CountMutilThread extends Thread{
    public  CountMutilThread(String name) {
        super();
        this.setName(name);  //设置线程名称
    }

    @Override
    public void run(){
        //构造显示字符串
        String result = "";

        //创建单例实例
        CountSingleton countSingleton = CountSingleton.getInstance();

        //循环调用四次
        for (int i=1;i<5;i++){
            //countSingleton.add();
            result += Thread.currentThread().getName()+"-->";
            result += "当前的计数值:";
            result += countSingleton.getCounter();
            result += "\n";

            System.out.println(result);
            result = "";
        }

    }
}   
package singleton;
/**
 * 单例模式-利用静态内部类
 * @author dingding
 *
 */

public class CountSingleton {
    private int totNum = 0; //存储计数值
    private CountSingleton (){}  

    private static class SingletonHolder {  
        private static final CountSingleton INSTANCE = new CountSingleton();  
    }  

    public static final CountSingleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  

//    public void add(){
//      totNum = totNum+1;
//    }
//    
    //计数加1,获取当前计数值
    public int getCounter(){
        totNum = totNum+1;
        return totNum;
    }

}

最终输出结果:

Thread 2-->当前的计数值:2

Thread 2-->当前的计数值:6

Thread 2-->当前的计数值:7

Thread 4-->当前的计数值:5

Thread 1-->当前的计数值:4

Thread 3-->当前的计数值:3

Thread 0-->当前的计数值:1

Thread 0-->当前的计数值:12

Thread 0-->当前的计数值:13

Thread 0-->当前的计数值:14

Thread 3-->当前的计数值:11

Thread 3-->当前的计数值:15

Thread 3-->当前的计数值:16

Thread 1-->当前的计数值:10

Thread 4-->当前的计数值:9

Thread 2-->当前的计数值:8

Thread 4-->当前的计数值:18

Thread 1-->当前的计数值:17

Thread 1-->当前的计数值:20

Thread 4-->当前的计数值:19

六. 总结

一般来说,线程安全的单例模式常用有5种写法:[懒汉]、[饿汉]、[双重检验锁]、[静态内部类]、[枚举]。

很多时候取决人个人的喜好,我比较钟爱双重检验锁,觉得这种方式可读性高、安全、优雅,有时为了方便,也会使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

参考资料

  1. 单例模式
  2. 单件模式(Singleton Pattern)
  3. 【Java】设计模式:深入理解单例模式
  4. 如何正确地写出单例模式
  5. 设计模式之——单例模式(Singleton)的常见应用场景
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值