设计模式之单例模式

本文详细介绍了单例模式的概念、特点及其实现方式,包括饿汉式、懒汉式等,并对比了各种方式的优缺点。此外,还介绍了单例模式在实际开发中的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

设计模式之单例模式

概述

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

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

优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当需要频繁创建和销毁的对象时,节省创建时间,提高性能
5.允许可变数目的实例。
6.避免对共享资源的多重占用。

缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

说明:
单例的三大要点:

  • 线程安全

  • 延迟加载

  • 序列化与反序列化安全

适用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。
5.设计数据库连接池时一般采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
6. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制

设计模式思想

一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,在单例中通常使用getInstance这个名 称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们 还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

设计模式的写法

饿汉式

package com.zd.singleton;


/**
 * 饿汉式单例
 * @author Administrator
 *饿汉法就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建.
 *好处是编写简单,但是无法做到延迟创建对象。
 */
public class HungrySingleton {
    /*
     * 饿汉式单例类.在类初始化时,已经自行实例化  
     */
    private static HungrySingleton instance = new HungrySingleton();
    /* 私有构造方法,防止被实例化 */
    private HungrySingleton() {

    }
    /* 静态工程方法,创建实例 */
    public static HungrySingleton getInstance() {
        return instance;

    }
      public void showMessage(){
          System.out.println("饿汉式单例");
       }
}

饿汉式是最简单的实现方式,这种实现方式适合那些在初始化时就要用到单例的情况,这种方式简单粗暴,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。
优点
1.线程安全
2.在类加载的同时已经创建好一个静态对象,调用时反应速度快
缺点
1.资源效率不高,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化 。
2.不能实现懒加载。

懒汉式 单线程写法 线程不安全

package com.zd.singleton;


/**
 * 单线程写法 不安全 懒汉式
 * 
 * @author Administrator
 *         由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,如果是null就new一个出来,最后返回singleton对象.
 *         这种方法可以实现延时加载,但是有一个致命弱点:线程不安全。如果有两条线程同时调用getSingleton()方法,就有很大可能导致重复创建对象。
 */
public class SingleThreadSingleton {

    /* 持有私有静态实例,防止被引用,此处赋值为 null,目的是实现延迟加载 */
    private static SingleThreadSingleton instance=null;
    /* 私有构造方法,防止被实例化 */
    private SingleThreadSingleton() {
        // TODO Auto-generated constructor stub
    }
    /* 静态工程方法,创建实例 */
    public static SingleThreadSingleton getInstance() {

        if (instance == null) {//多个线程判断instance都为null时,在执行new操作时多线程会出现重复情况
            instance = new SingleThreadSingleton();
        }

        return instance;

    }
     public void showMessage(){
          System.out.println("单线程写法 不安全 懒汉式");
       }
}

优点:
避免了饿汉式的那种在没有用到的情况下创建事例,资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法。
缺点:
线程不安全。虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程时不能正常工作。在多线程访问的时候,很可能会造成多次实例化,虽然后面创建的实例会覆盖先创建的实例,但是还是会存在拿到不同对象的情况。

懒汉式 线程安全

package com.zd.singleton;


/**
 * 单线程写法 安全 懒汉式
 * 
 * @author Administrator
 *         这种写法考虑了线程安全,将对singleton的null判断以及new的部分使用synchronized进行加锁。
 *         同时,对singleton对象使用volatile关键字进行限制,保证其对所有线程的可见性,并且禁止对其进行指令重排序优化.
 *         缺点:其效率低下。因为每次调用getSingleton()方法,都必须在synchronized这里进行排队,而真正遇到需要new的情况是非常少的。
 */
public class SingleThreadSingleton2 {
    /* 持有私有静态实例,防止被引用,此处赋值为 null,目的是实现延迟加载 */
    private static SingleThreadSingleton2 instance=null;
    /* 私有构造方法,防止被实例化 */
    private SingleThreadSingleton2() {
        // TODO Auto-generated constructor stub
    }
    /* 静态工程方法,创建实例 */
    public static SingleThreadSingleton2 getInstance() {

        synchronized (SingleThreadSingleton2.class) {
            if (instance == null) {
                instance = new SingleThreadSingleton2();
            }
        }
        return instance;
    }

     public void showMessage(){
          System.out.println("懒汉式单线程写法安全 ");
       }
}

这种方式在getInstance()方法上加了同步锁,所以在多线程情况下会造成线程阻塞,把大量的线程锁在外面,只有一个线程执行完毕才会执行下一个线程。
优点:
线程安全
缺点:
效率低下,无法实际应用。因为每次调用getSingleton()方法,都必须在synchronized这里进行排队,而真正遇到需要new的情况是非常少的。

双重检查锁 兼顾线程安全和效率的写法

package com.zd.singleton;

/**
 * 懒汉式 双重检查锁 兼顾线程安全和效率的写法
 * 
 * @author Administrator 在getSingleton()方法中,进行两次null检查.
 *         在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。
 */
public class SingleThreadSingleton3 {
    /* 持有私有静态实例,防止被引用,此处赋值为 null,目的是实现延迟加载 */
    private static volatile SingleThreadSingleton3 instancle = null;
    /* 私有构造方法,防止被实例化 */
    private SingleThreadSingleton3() {
        // TODO Auto-generated constructor stub
    }
    /* 静态工程方法,创建实例 */
    public static SingleThreadSingleton3 getInstance() {
        if (instancle == null) {
            synchronized (SingleThreadSingleton3.class) {
                if (instancle == null) {
                    instancle = new SingleThreadSingleton3();
                }
            }
        }

        return instancle;

    }
     public void showMessage(){
          System.out.println(" 双重检查锁单例");
       }
}

这种写法被称为“双重检查锁。在getSingleton()方法中,进行两次null检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。
优点:
线程安全
资源利用率高
缺点:
在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。
被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。volatile的第二层语义是禁止指令重排序优化。禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。jdk1.5的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题

静态内部类法

package com.zd.singleton;

/**
 * 静态内部类法
 * 
 * @author Administrator
 *把Singleton实例放到一个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,
 *并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的.
 */
public class StaticInnerClassSingleton {
    private static class SingletonHolder {
        private static StaticInnerClassSingleton instancle = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton() {
        // TODO Auto-generated constructor stub
    }

    public static StaticInnerClassSingleton getInstancle() {
        return SingletonHolder.instancle;
    }
     public void showMessage(){
          System.out.println("静态内部类法单例");
       }
}

优点
资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法 。
缺点
第一次加载时反应不够快 。

上面提到的所有实现方式都有共同的缺点:

  • 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。

以上的测试代码:

package com.zd.singleton;

public class TestSingleton {

    public static void main(String[] args) {

        HungrySingleton hungrysingleTon = HungrySingleton.getInstance();
        hungrysingleTon.showMessage();

        SingleThreadSingleton singleton1=SingleThreadSingleton.getInstance();
        singleton1.showMessage();

        SingleThreadSingleton2 singleton2=SingleThreadSingleton2.getInstance();
        singleton2.showMessage();

        SingleThreadSingleton3 singleton3=SingleThreadSingleton3.getInstance();
        singleton3.showMessage();

        StaticInnerClassSingleton classsingleton=StaticInnerClassSingleton.getInstancle();
        classsingleton.showMessage();
    }

}

以上的测试打印结果:

饿汉式单例
单线程写法 不安全 懒汉式
懒汉式单线程写法安全 
 双重检查锁单例
静态内部类法单例

枚举写法

package com.zd.singleton;



/**
 * 枚举写法
 * @author Administrator
 *使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
 *因此,Effective Java推荐尽可能地使用枚举来实现单例。
 */
/*
 * 上面提到的所有实现方式都有两个共同的缺点:
 * 都需要额外的工作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化一个序列化的对象实例时都会创建一个新的实例。
 * 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
 */

public enum EnumerateSingleton {

     //定义一个枚举的元素,它就是 Singleton 的一个实例
    INSTANCE;  
     public void doSomeThing() {  
         // do something...
         System.out.println("枚举法写单例");
     }  

     public static void main(String[] args) {
        EnumerateSingleton singlrton=EnumerateSingleton.INSTANCE;
        singlrton.doSomeThing();
    }
}

使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。

总结

单例模式使用开发中经常使用的一种设计模式。以上是我近段时间学习单例,从各位同行博客中学习总结出来的内容。站在巨人的肩膀上才能看的更远。一般情况下,不建议使用懒汉方式,建议使用饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用静态内部类法方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双重检查锁方式。
写博客是为了帮助开发者学习使用技术,同时巩固自己所学技术。如果此篇博客有助于您的学习,那是我的荣幸!如果此篇博客有任何瑕疵,请多多指教!在此感谢您的学习和指教!

参考文献:

【Java】设计模式:深入理解单例模式 ]
你真的会写单例模式吗——Java实现
单例模式的优缺点和使用场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值