深入理解设计模式中的单例模式

深入理解设计模式中的单例模式

一、什么是单例模式

 二、应用场景

三、优缺点

四、单例模式的几种实现方式

1、懒汉式,线程不安全

 2、懒汉式,线程安全

3、饿汉式

 4、双检锁/双重校验锁(DCL,即 double-checked locking)

 总结


一、什么是单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

我们将创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo 类使用 SingleObject 类来获取 SingleObject 对象。

 

 二、应用场景

举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制 同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。 从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点: 适用场景: 1.需要生成唯一序列的环境 2.需要频繁实例化然后销毁的对象。 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 4.方便资源相互通信的环境
 

三、优缺点

优点:
1.在内存中只有一个对象,节省内存空间;
2.避免频繁的创建销毁对象,可以提高性能;
3.避免对共享资源的多重占用,简化访问;
4.为整个系统提供一个全局访问点。

缺点:
1.不适用于变化频繁的对象;
2.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共 享连接池对象的程序过多而出现连接池溢出;
4.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

 

四、单例模式的几种实现方式

1、懒汉式,线程不安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

public class testSingleton1 {
    public static void main(String[] args) {
        Singleton t1 = Singleton.getInstance();
        Singleton t2 = Singleton.getInstance();
        System.out.println(t1==t2);
    }
}

class Singleton{
    private Singleton(){};

    private static Singleton singleton=null;

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

运行结果:

 2、懒汉式,线程安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

public class testSingleton2 {
    public static void main(String[] args) {
        Singleton2 t1 = Singleton2.getInstance();
        Singleton2 t2 = Singleton2.getInstance();
        System.out.println(t1==t2);
    }
}

class Singleton2{
    private Singleton2(){};

    private static Singleton2 singleton=null;

    public static synchronized Singleton2 getInstance(){
        if(singleton==null){
            singleton=new Singleton2();
        }
        return singleton;
    }
}

运行结果:

3、饿汉式

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

public class testSingleton3 {
    public static void main(String[] args) {
        Singleton3 t1 = Singleton3.getInstance();
        Singleton3 t2 = Singleton3.getInstance();
        System.out.println(t1==t2);
    }
}

class Singleton3{
    private Singleton3(){};

    private static Singleton3 singleton=new Singleton3();

    public static  Singleton3 getInstance(){
        return singleton;
    }
}

运行结果:

 4、双检锁/双重校验锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多线程安全:

实现难度:较复杂

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

/**  volatile是什么,可以保证有序性吗?
 *  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,
 *     这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存中。
 *  2)禁止进行指令重排序
 *     volatile不是原子性操作
 *  保证部分有序性:
 *      当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行
 *      且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
 *  使用volatile一般用于状态标记量和单例模式的双检锁。
 */
public class testSingleton4 {
    public static void main(String[] args) {
        Singleton4 t1 = Singleton4.getInstance();
        Singleton4 t2 = Singleton4.getInstance();
        System.out.println(t1==t2);
    }
}

class Singleton4{
    private Singleton4(){};

    /**  volatile是什么,可以保证有序性吗?
     *  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,
     *     这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存中。
     *  2)禁止进行指令重排序
     *     volatile不是原子性操作
     *  保证部分有序性:
     *      当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行
     *      且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
     *  使用volatile一般用于状态标记量和单例模式的双检锁。
     */
    private volatile static Singleton4 singleton;

    public static  Singleton4 getInstance(){
        if(singleton==null){
            synchronized (Singleton4.class){
                if(singleton==null){
                    singleton=new Singleton4();
                }
            }
        }
        return singleton;
    }
}

运行结果:

 总结

单例模式的实现方法还有很多。上述是比较经典的实现方式,也是我们应该掌握的几种实现方式。
从这四种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:
1.尽量减少同步块的作用域;
2.尽量使用细粒度的锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值