23种设计模式——单例模式

1、单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

使用场景:当想控制实例的数量,节省系统资源的时候。

2、网站计数的单例实现

实现单例模式有多种写法,这里列举常用的三种实现方式,考虑到网站登录的高并发场景下,将重点关注多线程环境下的安全问题。

2.1、饿汉模式
  • 在线程启动之初就进行实例的创建
  • final修饰实例,天生的线程安全
  • 无论线程中是否用到该实例,都存在
/**
 * @ClassName: SimpleSingleton
 * @Description:饿汉式单例模式
 * @Author: DiTian
 * @Date: 2021/7/17 9:34
 */
public class SimpleSingleton {
    // 单例对象
    private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton();
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private SimpleSingleton() {
    }

    public static SimpleSingleton getInstance() {
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }
}


/**
 * @ClassName: Login
 * @Description:单例模式的应用--登录线程
 * @Author: DiTian
 * @Date: 2021/7/17 9:35
 */
public class Login implements Runnable{
    // 登录名称
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
        // 饿汉式单例
        SimpleSingleton simpleSingleton=  SimpleSingleton.getInstance();
        simpleSingleton.setCount();
        System.out.println(getLoginName()+"登录成功:"+simpleSingleton.toString());
    }
}
 
/**
 * @ClassName: App
 * @Description: 单例模式--主程序
 * @Author: DiTian
 * @Date: 2021/7/17 9:36
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("网站共有"+SimpleSingleton.getInstance().getCount()+"个用户登录");

    }
}

输出结果:在10个线程并发情况下,获取到的是同一个对象引用地址,即该单例模式是有效的。
在这里插入图片描述

2.2、懒汉模式
/**
 * @ClassName: LazySingleton
 * @Description:懒汉式单例--未应用线程同步技术
 * @Author: DiTian
 * @Date: 2021/7/14 21:39
 */
//该模式的特点是类加载时没有生成单例,只有当第一次调用getInstance方法时才去创建这个单例
public class LazySingleton {
    // 单例对象
    private static LazySingleton APP_INSTANCE;
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (APP_INSTANCE == null) {
            APP_INSTANCE = new LazySingleton();
        }
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

}

/**
 * @ClassName: Login
 * @Description:单例模式的应用--登录线程
 * @Author: DiTian
 * @Date: 2021/7/17 9:35
 */
public class Login implements Runnable{
    // 登录名称
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
        // 懒汉式单例
        LazySingleton lazySingleton =LazySingleton.getInstance();
        lazySingleton.setCount();
        System.out.println(getLoginName()+"登录成功:"+lazySingleton);

    }
}

/**
 * @ClassName: App
 * @Description: 单例模式--主程序
 * @Author: DiTian
 * @Date: 2021/7/17 9:36
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("网站共有"+LazySingleton.getInstance().getCount()+"个用户登录");

    }
}

输出结果:因为未使用线程同步技术,所以在多线程情况下,出现了多个实例,单实例失效

在这里插入图片描述

我们使用线程同步技术对懒汉式模式进行改进:volatile、synchronized

/**
 * @ClassName: LazySingleton
 * @Description:懒汉式单例--未应用线程同步技术
 * @Author: DiTian
 * @Date: 2021/7/14 21:39
 */
//该模式的特点是类加载时没有生成单例,只有当第一次调用getInstance方法时才去创建这个单例
public class LazySingleton {
    // 单例对象
    private static volatile LazySingleton APP_INSTANCE;
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (APP_INSTANCE == null) {
            APP_INSTANCE = new LazySingleton();
        }
        return APP_INSTANCE;
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

}


输出结果:在10个线程并发情况下,获取到的是同一个对象引用地址,即该单例模式是有效的。

在这里插入图片描述

2.3、通过枚举类实现单例模式

《Effective Java》 *推荐使用枚举的方式解决单例模式*。这种方式解决了最主要的;线程安全、自由串行化、单一实例。

/**
 * @ClassName: EnumSingleton
 * @Description:通过枚举实现单例模式
 * @Author: DiTian
 * @Date: 2021/7/17 9:53
 */
public enum EnumSingleton implements Serializable {
    // 单例对象
    APP_INSTANCE;
    // 计数器
    private AtomicLong count = new AtomicLong(0);

    // 单例模式必须保证默认构造方法为私有类型
    private EnumSingleton() {
    }

    public AtomicLong getCount() {
        return count;
    }

    public void setCount() {
        count.addAndGet(1);
    }

}



/**
 * @ClassName: Login
 * @Description:单例模式的应用--登录线程
 * @Author: DiTian
 * @Date: 2021/7/17 9:35
 */
public class Login implements Runnable{
    // 登录名称
    private String loginName;

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public void run() {
        // 懒汉式单例
        EnumSingleton enumSingleton =EnumSingleton.APP_INSTANCE;
        enumSingleton.setCount();
        System.out.println(getLoginName()+"登录成功:"+enumSingleton);

    }
}



/**
 * @ClassName: App
 * @Description: 单例模式--主程序
 * @Author: DiTian
 * @Date: 2021/7/17 9:36
 */
public class App {
    public final static int num = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[num];
        for (int i = 0; i < num; i++) {
            Login login = new Login();
            login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
            threads[i] = new Thread(login);
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println("网站共有"+EnumSingleton.APP_INSTANCE.getCount()+"个用户登录");

    }
}

输出结果:在10个线程并发情况下,获取到的是同一个对象引用地址,即该单例模式是有效的。
在这里插入图片描述

3、单例模式优缺点与场景

3.1 优点
  1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
  3. 提供了对唯一实例的受控访问。
  4. 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时 单例模式无疑可以提高系统的性能。
  5. 允许可变数目的实例。
  6. 避免对共享资源的多重占用。
3.2 缺点
  1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  2. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  3. ​ 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  4. ​ 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
3.3 注意事项
  • 1.使用时不能用反射模式创建单例,否则会实例化一个新的对象
  • 2.使用懒单例模式时注意线程安全问题
  • 3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

1.需要频繁实例化然后销毁的对象。 
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 
3.有状态的工具类对象。 
4.频繁访问数据库或文件的对象。 

以下都是单例模式的经典使用场景:

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

1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件

   2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~ 
   3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。 
   4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。 
   5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 
   6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。 
   7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。 
   8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。 
   9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。 
   10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例. 

4、实现单利模式的原则和过程

1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例
2.单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式)
3.单例模式要素:
a.私有构造方法
b.私有静态引用指向自己实例
例模式实现的具体例子,一个操作系统只能有一个文件系统。
10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

4、实现单利模式的原则和过程

1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例
2.单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式)
3.单例模式要素:
a.私有构造方法
b.私有静态引用指向自己实例
c.以自己实例为返回值的公有静态方法

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值