尺有所短,寸有所长;不忘初心,方得始终。
一、什么是单例模式
单例模式属于创建型模式,是 Java 中最简单的设计模式之一,单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。
主要作用:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
单例模式解决了两个问题:
二、单例模式的应用场景
使用单例模式的共性:
-
需要生成唯一序列的环境 -
需要频繁实例化然后销毁的对象。 -
创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 -
方便资源相互通信的环
使用场景:
-
要求生产唯一序列号。 -
WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。 -
创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。 -
多线程的线程池的设计 -
web开发中读取配置文件(HttpApplication )
三、单例模式的优缺点
优点:
-
在内存中只有一个对象,节省内存空间; -
仅在首次请求单例对象时对其进行初始化,避免频繁的创建销毁对象,提高性能; -
避免对共享资源的多重占用,简化访问(比如写文件操作);
缺点:
-
单例模式没有层级关系,不利于在原单例类上的扩展,不适用于变化频繁的对象 -
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
四、单例模式结构与实现
4.1 单例模式结构
单例类声明了一个名为 getInstance
获取实例的静态方法来返回其所属类的一个相同实例。
单例的构造函数必须对客户端 (Client) 代码隐藏。调用 获取实例
方法必须是获取单例对象的唯一方式。

4.2 单例模式实现方式
-
在类中添加一个私有静态成员变量用于保存单例实例。
-
声明一个公有静态构建方法用于获取单例实例。
-
在静态方法中实现延迟初始化。该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
-
将类的构造函数设为私有,禁止被外部实例化。
-
在客户端代码中对单例的构造函数的调用替换为对其静态构建方法的调用。
五、单例模式的八种实现
单例模式的实现均是在实例化对象的方式有所不同,通过静态方法的方式获取对象,调用实例化对象的其他方法
public static void main(String[] args) {
// 获取实例化单例对象
Singleton1 instantiate = Singleton1.getInstantiate();
// 调用改对象的其他方法
int nextId = instantiate.getNextId();
System.out.println(nextId);
}
5.1 饿汉式(静态变量)
饿汉式表示未使用到单例就直接创建对象,即创建实例在使用到之前就完成了。
优点:类装载的时候就完成实例化,避免了线程同步问题。
缺点:在类装载的时候就完成实例化。如果未使用这个实例,则会造成内存的浪费。
/**
* @author
* @date 2021/10/1915:45
* 饿汉式单例
*/
public class Singleton1 {
// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();
// 构造方法私有化,禁止被外部实例化
private Singleton1(){}
/**以自己实例为返回值的静态的公有方法,静态工厂方法*/
public static Singleton1 getInstantiate(){
return singleton1;
}
}
5.2 饿汉式(静态代码块)
静态代码块跟静态变量方法的实现差不多,只是创建对象放到了静态代码块中,优点在于初始化单例时可以做些其他初始化操作。
/**
* @author
* @date 2021/10/1916:11
*/
public class Singleton2 {
// 构造方法私有化,禁止被外部实例化
private Singleton2(){}
// 指向自己实例的私有静态引用,
private static Singleton2 singleton2;
// 通过静态代码块实例化
static {
singleton2 = new Singleton2();
//其他初始化操作... 比如给属性赋值
}
/**以自己实例为返回值的静态的公有方法,静态工厂方法*/
public static Singleton2 getInstantiate(){
return singleton2;
}
}
5.3、懒汉式(线程不安全)
延迟加载:懒汉式是使用到单例时才会创建实例,,后续使用到的都是同一个实例。懒汉式可以减少资源消耗和内存占用。
这种方式获取实例方法没有任何同步限制,在多线程环境下,可能会带来线程安全问题,所以一般不使用。
/**
* @author
* @date 2021/10/1916:16
*/
public class Singleton3 {
// 构造方法私有化,禁止被外部实例化
private Singleton3(){}
// 指向自己实例的私有静态引用
private static Singleton3 singleton3;
/**以自己实例为返回值的静态的公有方法,静态工厂方法*/
public static Singleton3 getInstantiate(){
//如果未实例化,创建实例
if (singleton3 == null) {
singleton3 = new Singleton3();
}
//返回实例
return singleton3;
}
}
5.4、懒汉式(线程安全,同步方法)
可以通过synchronized关键字修饰获取实例的方法,使其成为同步方法,这样就可以保证线程安全问题
/**
* @author
* @date 2021/10/1916:16
*/
public class Singleton4 {
// 构造方法私有化,禁止被外部实例化
private Singleton4(){}
// 指向自己实例的私有静态引用
private static Singleton4 singleton4;
/**以自己实例为返回值的静态的公有方法,静态工厂方法*/
public static synchronized Singleton4 getInstantiate(){
//如果未实例化,创建实例
if (singleton4 == null) {
singleton4 = new Singleton4();
}
//返回实例
return singleton4;
}
}
5.5、懒汉式(线程安全,同步代码块)
如果获取实例的方法中除了实例化单例,还会有其他额外的操作,给整个方法加上同步锁耗费过大,因此可以采用同步代码块的方式,减小锁的锁定范围,减少系统消耗。
/**
* @author
* @date 2021/10/1916:16
*/
public class Singleton5 {
// 构造方法私有化,禁止被外部实例化
private Singleton5(){}
// 指向自己实例的私有静态引用
private static Singleton5 singleton5;
/**以自己实例为返回值的静态的公有方法,静态工厂方法*/
public static Singleton5 getInstantiate(){
//同步代码块
synchronized (Singleton5.class) {
if (singleton5 == null) {
singleton5 = new Singleton5();
}
}
//其他初始化操作... 比如给属性赋值
//返回实例
return singleton5;
}
}
5.6、懒汉式(双检锁/双重校验锁DCL)
懒汉式(线程安全,同步代码块)存在效率问题。每个线程都需要等待同步块执行结束才能执行,可能对象已经初始化结束了,但是同步锁未释放。因此我们可以使用双重检测机制
变量需要使用volatile
修饰,避免CPU指令的重排序导致的对象未完全初始化结束的引用逸出。
/**
* @author
* @date 2021/10/1916:16
*/
public class Singleton6 {
// 构造方法私有化,禁止被外部实例化
private Singleton6(){}
// 指向自己实例的私有静态引用
//这里使用volatile修饰是避免CPU指令的重排序导致的对象未完全初始化结束的引用逸出
private static volatile Singleton6 singleton6;
/**以自己实例为返回值的静态的公有方法,静态工厂方法*/
public static synchronized Singleton6 getInstantiate(){
//第一次判断不加锁,提高执行效率,避免了不必要的同步
if (singleton6 == null) {
synchronized (Singleton6.class) {
if (singleton6 == null) {
singleton6 = new Singleton6();
}
}
}
//其他初始化操作... 比如给属性赋值
//返回实例
return singleton6;
}
}
5.7、登记式/静态内部类
通过类加载的机制来实现单例模式
-
静态内部类在主类加载的时候不会被加载,即实现了懒汉模式,减少内存占用。 -
类加载的过程是 JVM
层面保证了线程安全性,即同一个类只能加载一次。
/**
* @author
* @date 2021/10/1916:16
*/
public class Singleton7 {
// 构造方法私有化,禁止被外部实例化
private Singleton7(){}
//2.创建静态私有内部类,定义单例属性实例(静态内部类在主类装载时不会被装载)
private static class SingletonInstance {
private static final Singleton7 Singleton7 = new Singleton7();
}
//3.提供一个公有的静态方法返回实例对象
public static Singleton7 getInstance() {
return SingletonInstance.Singleton7;
}
}
5.8、枚举式
上述七种方式在反序列化(将一个单例对象写到磁盘上,再读回来)时,会重新创建实例得到一个新的实例,而 枚举类天生单例,线程安全,且为懒汉式。
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
/**
* @author
* @date 2021/10/1916:16
*/
public enum Singleton8 {
INSTANCE;
}
六、单例模式在JDK中的使用
JDK
中的Runtime
类,每个JVM进程都对应一个Runtime
实例,保存JVM
的运行中的一些参数信息。因此JDK
设计Runtime
类为单例形式。
public class Runtime {
// 构造方法私有化,禁止被外部实例化
private Runtime(){}
//饿汉式 创建Runtime实例
private static Runtime currentRuntime = new Runtime();
//获取Runtime实例
public static Runtime getRuntime() {
return currentRuntime;
}
}
七、与其他模式的关系
-
外观模式一般可以转换为单例模式, 因为一般情况下只需要一个外观对象。
-
当对象的所有共享状态简化为一个享元对象时,享元模式就和单例模式类似。但本质不同:
-
单例只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
-
单例对象可以是可变的。享元对象是不可变的。
-
抽象工厂模式、生成器模式、原型模式都可以用单例来实现
八、总结
通过这八种实现,可以看出要想实现效率高的线程安全的单例,必须注意满足:
-
尽量减少同步块的作用域。
-
尽量使用细粒度的锁。
原文始发于微信公众号(星河之码):设计模式(1):单例模式(Singleton Pattern)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/27215.html