单例模式简介
90%以上的设计模式都或多或少的应用了接口和抽象类,而单例比较特殊,并没有接口的应用。
单例Singleton指仅仅被实例化一次的类。通常被用来代表那些本质上唯一的系统组件。————《Effective Java》
数据库连接获取类的对象可以是单例的。
单例的意思就是在内存中只有一个对象。而单例和static有是有区别的,static是用来修饰类中的成员变量和成员方法的,而单例则属于对象的层面。
单例的实现
思考:如何在内存中只有一个对象?——不能让外界随意的实例化(new)。封装思想中有一个叫“属性私有化,方法公开化”的概念,在需要单例的类中自己去实例化。这就是单例的两个条件:不能被外界实例化;根据封装的特征属性私有化,方法公开化。
外界不能实例化,实现的方式就是将需要设置为单例的类(以下简称:单例类)的构造器设置为private。
注意:如果电脑上安装了多个虚拟机的话,那么实际上并不是真正意义上的单例,而是每个虚拟机会有一个单例。
懒汉单例模式:
懒汉单例模式在第一次使用对象的时候才会去创建对象,但最简单的懒汉单例存在多线程安全问题,依然有可能创建多个实例。
/*** 懒汉单例模式Demo<br>懒汉单例是在运行时调用的一种行为。* 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。<br>* 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。<br>* 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。<br>* 这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。<br>* * 类名:LazySingleton<br>* 作者: mht<br>* 日期: 2018年3月18日-下午8:41:01<br>*/
public class LazySingleton {/** 属性私有化 */private static LazySingleton instance = null;/** 构造器私有化 */private LazySingleton() { }/** 公开获取单例对象 */public static LazySingleton getInstance() {if (instance == null) {instance = new LazySingleton();}return instance;}
}
饿汉单例模式:
饿汉单例会在类加载的时候即实例化对象,这与懒汉单例的创建时机截然不同,不存在线程安全问题,
但缺点是需要提前占用内存资源。
/*** 类名:EagerSingleton<br>* 作者: mht<br>* 日期: 2018年3月18日-下午8:41:01<br>*/
public class EagerSingleton {/** 使用静态常量*/private static final EagerSingleton instance = new EagerSingleton();private EagerSingleton() {System.out.println("这是构造器:LazySingleton()");}public static EagerSingleton getInstance() {return instance;}public void doSomething() {System.out.println("EagerSingleton is doing something now ...");}
}
懒汉+饿汉(静态内部类):
该方法通过私有静态内部类来实现单实例延迟加载,因为静态内部类不会因为外部类的加载而加载,所以可以在第一次使用静态类的时候才执行加载。
优点是:不仅可以达到懒汉式的延迟加载,使类的加载速度提升,避免一开始占用过多内存,又具备线程安全性,无需判断,性能上也会更快一些。
/*** 作者: mht<br>* 日期: 2018年3月19日-下午9:55:52<br>*/
public class Singleton {private Singleton () {System.out.println("Singleton...");}private static class SingletonInstance {private static final Singleton s = new Singleton();}public static Singleton getInstance() {return SingletonInstance.s;}
}
双重检查-单例模式(线程安全的懒汉式单例):
双重检查单例模式 使用双重检查同步延迟加载来创建单例的做法是一个非常优秀的做法, 其不但保证了单例(线程安全),而且切实提高了程序的运行效率。
实现原理:
Java语言提供了一种较synchronized稍弱的同步机制,volatile,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
getInstance()方法描述:
为了在保证单例的前提下提高运行效率,我们需要对单例对象dcs进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:必须使用volatile关键字修饰单例对象。
public class DoubleCheckSingleton {// 使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作。private static volatile DoubleCheckSingleton dcs;private DoubleCheckSingleton() {}public static DoubleCheckSingleton getInstance() {if (dcs == null) {synchronized (DoubleCheckSingleton.class) {// 只需在第一次创建实例时才同步if (dcs == null) {dcs = new DoubleCheckSingleton();}}}return dcs;}
}
注册单例模式:
public class RegisterSingleton<T> {/* 注册表 */private static Map<String, Object> regMap = new HashMap<>();// 类加载过程中,静态初始化static{Connection conn = new Connection();UserService us = new UserService();// 初始化注册表regMapregMap.put(conn.getClass().getName(), conn);regMap.put(us.getClass().getName(), us);}private RegisterSingleton() { }public synchronized static Object getInstance(String key) {if (key == null) return null;try {if (regMap.get(key) == null) {// 这里注意,此Demo是以类名作为key传入map中的,因此,这里的传值也是相关方法,通过类名找到类,然后在进行自动实例化regMap.put(key, Class.forName(key).newInstance());}return regMap.get(key);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}return null;}
}
测试:
package design.pattern;public class testSingleton {public static void main(String[] args) {UserService us = (UserService) RegisterSingleton.getInstance(UserService.class.getName());Connection conn1 = (Connection) RegisterSingleton.getInstance(Connection.class.getName());Connection conn2 = (Connection) RegisterSingleton.getInstance(Connection.class.getName());System.out.println(us);System.out.println(conn1);System.out.println(conn2);System.out.println(conn1 == conn2);}
}
运行结果:
design.pattern.UserService@15db9742
design.pattern.Connection@6d06d69c
design.pattern.Connection@6d06d69c
true
对单例模式应用的理解:
在spring框架中大多数对象的使用都是单例模式的,比如获取用户User对象的方法类UserDao和UserService都是单例的,包括controller,他们默认都单例的。因为实际上我们真正需要多个对象的是User而并不是获取User的方法类。
这也就充分解释了Spring注解中的@Autowired自动注入对象的实现方式。细心的我们也都可以发现,这些自动注入的对象一般都是实现某种业务的中间类对象,而并不是最终我们需要的对象,因此,为了避免频繁的创建这些“中间件”对象占用不必要的内存空间,都是以单例的形式来创建的。另外,数据库连接对象Connection也一定是单例的。
单例模式的误解:
单例会导致线程阻塞吗?不会的。
举个生活中的例子:饭店只有一个菜谱,但是有多个厨师,多个厨师共享这份菜谱,并同时做一道菜,彼此之间是没有影响的;老师在黑板上写下一个数学题,同学们在下面计算结果,黑板就是内存,数学题就是单例对象,同学们就是各个线程,计算过程之间是没有影响的。
单例模式的优点:
1.内存中只有一个对象,节省内存空间
2.避免频繁的创建和销毁对象,可以提高性能
3.避免对共享资源的多重占用,简化访问
4.为整个系统提供一个全局访问点
单例模式的应用场景:
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡驱动程序对象常被设计为单例的。实际上,这些应用都或多或少具有资源管理器的功能。
其核心在于为整个系统提供一个唯一的实例,其应用场景包括但不仅限于一下几种:
1.有状态的工具类
2.频繁访问数据库或文件的对象
参考:
《彻头彻尾理解单例模式以及其在多线程环境中的应用》