单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
介绍
**意图:**保证一个类仅有一个实例,并提供一个访问它的全局访问点。
**主要解决:**一个全局使用的类频繁地创建与销毁。
**何时使用:**当您想控制实例数目,节省系统资源的时候。
**如何解决:**判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
**关键代码:**构造函数是私有的。
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
- 4、整个window种只有一个时间组件
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
**缺点:**没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
**注意事项:**getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
几种实现方式
懒汉式
懒汉式即指在第一次使用时才创建对象。
public class Singleton {
//组合一个自己的对象引用,private保护其不被直接访问修改,static保护本类唯一
private static Singleton singleton;
//构造函数私有化,使不能从外部创建本类对象
private Singleton() {
}
//获得单例实例的静态方法作外部访问点
//用synchronized修饰为同步,才能确保多线程环境下只创建这一个对象
public static synchronized Singleton getInstance() {
return null == singleton ? singleton = new Singleton() : singleton;
}
}
在这种方式下,因为将外部访问点整个设置为同步的,所以多线程环境下工作效率很低。当getInstance()
操作的同步对整个系统的性能不是很关键时,如需要避免饿汉创建该对象造成的内存浪费,不妨使用这种方式。
饿汉式
饿汉式是在类加载时才创建这个对象,但应注意饿汉式不一定懒加载。可以将懒加载视为在第一次调用getInstance()
方法时创建这个对象,懒汉式是能保证懒加载的,饿汉式不能。
public class Singleton {
//在类加载时创建对象,饿汉式
private static Singleton singleton=new Singleton();
//构造函数私有化,使不能从外部创建本类对象
private Singleton() {
}
//调用到此方法时类一定已经加载过了,直接返回
//不需要synchronized同步
public static Singleton getInstance() {
return singleton;
}
}
在这种方式中,巧妙利用了Java的类装载过程来保证了线程安全,因为这个类只加载一次,所以这个对象一定是唯一的。并且因为没有synchronized
对访问点的限制,这种方式的访问效率比较高。
除了要注意内存浪费之外,还应注意到**“类装载时”不一定是”第一次调用访问点时”**,因为类中还可能存在其它的static方法在此前调用导致对象被创建,所以饿汉式不能保证懒加载,可能早在调用其它静态方法时就把这个对象创建好了。
双检锁方式
使用双重校验锁(Double Checked Locking),可以结合懒汉式和饿汉式的优点。既不浪费内存(做到懒加载),又不至于让外部访问点性能下降太多。
public class Singleton {
//volatile保证有线程对该变量修改时,另一个线程中该变量的缓存行无效,读取时直接到内存读
//总之,若一个线程修改了某个变量的值,新值对其他线程来说是立即可见的,在访问点内检查时要用到
private volatile static Singleton singleton;
//构造函数私有化,使不能从外部创建本类对象
private Singleton() {
}
//使用DCL锁保证线程安全,不需要对整个方法synchronized同步
public static Singleton getInstance() {
//如果该线程发现该对象未创建
if (null == singleton) {
//那么首先要和其它线程竞争本类的锁
synchronized (Singleton.class) {
//获得锁以后,才能执行这部分代码
//这时再次检查是否为null
//如果还是null,说明自己是第一个竞争到锁的,本线程负责创建对象
if (null == singleton)
singleton = new Singleton();
//如果不是null了,说明自己这份锁已经是别人用过,创建好对象以后释放出来的
//这时对象已经被创建过了,本线程什么都不用做,直接释放锁即可
}
}
//至此,对象一定唯一地创建过了,直接返回
return singleton;
}
}
登记式
登记式是用一个线程安全的容器(网上很多登记式都用HashMap
,这是线程不安全的,用ConcurrentHashMap
才是正确的选择)来对要单例化的实例进行登记,当使用时直接从这个容器中取出即可。
可以单独设置一个类来管理要登记的单例对象,也可以为单例对象类自己设置登记容器。下面演示一下前者,即设置一个单独的类来管理登记。
//单例管理类
public class SingletonManager {
//线程安全的容器,饿汉式保证容器对象本身为单例
private static Map map = new ConcurrentHashMap();
//外部访问点,传入类名,返回该类的单例对象.该类会被登记进入上面的容器进行单例管理
//在类中务必保证构造方法私有化,对这一点这个管理类是无法控制的,需要自己保证
public static Object getInstance(String className) {
//如果还没登记到容器
if (!map.containsKey(className)) {
//用反射的方式创建对象(因为已经构造函数私有化),并登记到容器中
try {
map.put(className, Class.forName(className).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//从容器中获取管理的单例对象并返回
return map.get(className);
}
}
静态内部类
即使用静态内部类来实现,这种方式是对饿汉式的改进。既然饿汉式因为在类加载时就会创建对象,从而导致不能保证懒加载,那么不妨增加一个静态内部类,并且只在访问点方法中调用这个静态内部类,如此在这个静态内部类加载时创建对象就不会受到其它静态方法的影响了。
public class Singleton {
//构造函数私有化,使不能从外部创建本类对象
private Singleton(){
}
//这样避免了饿汉式在创建
private static class singletonHolde{
//该对象不会在外部类加载时便创建,避免受外部类其它静态方法影响
private static Singleton singleton = new Singleton();
}
//外部访问点
public static Singleton getInstance(){
//第一次引用内部类时才会加载创建这个对象,而只要保证此必在这方法内,就做到了懒加载
return singletonHolde.singleton;
}
}
枚举
枚举的构造函数本身就是私有的,而且可以自由序列化、线程安全、保证单例。使用枚举是实现单例模式的最佳方式。以前对枚举不太了解,实际上枚举就是一个final类,也一样可以有其它属性和方法,当成普通类来用实现单例极为简便。
public enum Singleton {
INSTANCE;//枚举对象天然就是单例
//枚举类也一样可以有其它属性
private int id = 2019;
//枚举类也一样可以有其它方法
public void sayId() {
System.out.println("id是" + id);
}
}
//测试一下
public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.sayId();
System.out.println(Singleton.INSTANCE == Singleton.INSTANCE);
}
}
输出:
id是2019
true
如果有什么疑问可以加群一起探讨学习