一、单例设计模式介绍
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只能提供一个取得其对象实例的方法(静态方法)。
通俗地说:单例是需要在内存中永远只能创建一个类的实例。
单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。
单例的适用场景:
- 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
- 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
- 如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够了。
二、单例设计模式八种方式
1、单例设计模式要点
(1)某个类只能有一个实例:构造器私有化
(2)它必须自行创建这个实例,含有一个该类的静态变量/常量来保存这个唯一的实例
(3)它必须自行向整个系统提供这个实例,对外提供获取该实例对象的方法:【1】直接暴露;【2】用静态变量的 get 方法获取
2、单例设计方式
单例模式我们可以提供出 8 种写法,有很多时候我们存在饿汉式单例的概念,以及懒汉式单例的概念。
饿汉式单例的含义是:在获取单例对象之前就已经创建完成了。
饿汉式特点:无论在程序中是否会用到该实例对象,都会提前创建出来,可能造成内存浪费。(饿汉式不涉及线程安全问题,在类初始化时直接创建实例对象)
懒汉式的单例是指:在真正需要单例的时候才创建出该对象。
懒汉式特点:在程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象的时候才初始化,此时,就需要采用延迟初始化策略。
3、单例模式的八种方式:(加粗代表推荐使用)
-
饿汉式(静态常量)
-
饿汉式(静态代码块)
-
懒汉式(线程不安全)
-
懒汉式(线程安全,同步方法)
-
懒汉式(线程安全,同步代码块)
-
双重检查
-
静态内部类(懒汉式,适用于多线程)
-
枚举(饿汉式)
三、八种方式实现
1、饿汉式(静态常量)
步骤:
1)构造器私有化(防止 new)
2)定义一个静态常量保存一个唯一的实例对象(单例)
3)向外暴露一个静态的公共方法。
代码实现:
public class Singleton1 {
//1.构造器私有化,外部不能 new
private Singleton1() {
}
//2.本类内部创建对象实例
private static final Singleton1 INSTANCE = new Singleton1();
//3.提供一个公有的静态方法,返回实例对象
public static Singleton1 getInstance() {
return INSTANCE;
}
}
优缺点说明:
(1)优点:写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
(2)缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例对象,则会造成内存的浪费。
(3)这种方式基于 classLoader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类加载,这时初始化 instance 就没有达到 lazy loading 的效果。
(4)总结:这种单例模式可用,可能造成内存浪费。
2、饿汉式(静态代码块)
步骤:
1)构造器私有化(防止 new)
2)定义一个静态变量保存一个唯一的实例对象(单例),通过静态代码块初始化单例对象。
3)向外暴露一个静态的公共方法。
代码实现:
public class Singleton2 {
//1.构造器私有化,外部不能 new
private Singleton2() {}
//2.本类内部创建对象实例
private static final Singleton2 INSTANCE;
//3.在静态代码块中,创建单例对象
static {
INSTANCE = new Singleton2();
}
//4.提供一个公有的静态方法,返回实例对象
public static Singleton2 getInstance() {
return INSTANCE;
}
}
优缺点说明:
(1)这种方式和静态常量的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码块,初始化类的实例。优缺点和上面一样。
(2)这种方式可以在构造方法有参数的时候,通过静态代码块给变量进行赋值。
(3)结论:这种单例模式可用,但是可能造成内存浪费。
代码实现 2:
public class Singleton {
private static final Singleton INSTANCE;
private String info;
static {
try {
Properties pro = new Properties();
//读取外部配置文件
pro.load(Singleton.class.getClassLoader().getResourceAsStream("single.properties"));
//动态获取某个值来给属性赋值
INSTANCE = new Singleton(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Singleton(String info) {
this.info = info;
}
}
3、懒汉式(线程不安全)
步骤:
1)构造器私有化
2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,如果有就直接返回;如果没有就创建一个新的单例对象。
代码实现:
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
//提供一个静态的公有方法,当使用到该方法时,才去创建 实例对象
public static Singleton3 getInstance() {
if (instance == null) {
//说明这是第一次获取单例对象,需要真正的创建出来
instance = new Singleton3();
}
return instance;
}
}
优缺点说明:
(1)起到了 Lazy Loading 的效果,但是只能在单线程下使用。
(2)如果在多线程下,一个线程进入了 if (instance == null) 判断语句块,还未来的及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
(3)结论:在实际开发中,不要使用这种方式。
4、懒汉式(线程安全,同步方法)
步骤:
1)构造器私有化
2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,如果有就直接返回;如果没有就创建一个新的单例对象。
4)为获取单例的方法加锁: 使用 synchronized 关键字
代码实现:
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton4 getInstance() {
if (instance == null) {
//说明这是第一次获取单例对象,需要真正的创建出来
instance = new Singleton4();
}
return instance;
}
}
优缺点说明:
(1)解决了线程安全问题
(2)效率太低了,每个线程在想获得类的时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类的实例,直接 return 就行了。方法进行同步后效率太低。
使用 synchronized 关键字修改方法包装线程安全,但性能差太多,并发下只能有一个线程正在进入获取单例对象。
(3)结论:在实际开发中,不推荐使用这种方式
5、懒汉式(线程安全,同步代码块)
步骤:
1)构造器私有化
2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,如果有就直接返回;如果没有就创建一个新的单例对象。
4)为获取单例的方法内部的代码加锁: 使用 synchronized 关键字
代码实现:
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
//性能得到了优化,但是依然不能保证第一次获取对象的线程安全
public static Singleton5 getInstance() {
//判断单例对象的变量是否为 null
if (instance == null) {
//很多线程执行到这里来:A,B
//如果是第一次执行,instance为空,A,B 两个线程都会创建对象,并不安全
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
优缺点:
(1)对比与上面的方式,性能得到了优化。
(2)但本质上还是线程不安全的,多个线程都执行了 if 语句,遇到 synchronized 关键字,会阻塞在这里,所以并不能保证第一次获取对象的线程安全。
(3)结论:在实际开发中,不推荐使用。
6、懒汉式 DCL(volatile 双重检查模式 Double Chekc Lock)
步骤:
1)构造器私有化
2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
3)提供一个方法进行双重检查机制返回单例对象
4)必须使用 volatile 修饰静态的变量
代码实现:
public class Singleton6 {
// 静态属性,volatile保证可见性和禁止指令重排序
private volatile static Singleton6 instance = null;
private Singleton6() {
}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题,同时保证了效率
public static Singleton6 getInstance() {
//判断单例对象的变量是否为 null
//第一次检查
if (instance == null) {
//同步锁定代码块
synchronized (Singleton6.class) {
//第二重检查锁定
if (instance == null) {
//注意:非原子操作
instance = new Singleton6();
}
}
}
return instance;
}
}
优缺点:
(1)Double-Check 概念是多线程开发中常使用到的,在代码中,进行了两次 if(instance == null) 检查,这样就可以保证线程安全了
(2)这样,实例化代码只用执行一次,后面再次访问,判断 if(instance == null),直接 return 实例化对象,也避免了反复进行方法同步。
(3)优点:线程安全,延迟加载,效率较高
(4)结论:在实际开发中,推荐使用这种单例设计模式。
分析:为什么要使用 volatile 保证安全?
(1) 禁止指令重排序
对象实际上创建要进行如下几个步骤:
a. 分配内存空间;
b. 调用构造器,初始化实例;
c. 返回地址给引用;
所以,new Singleton() 是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的应用)在初始化对象前完成】。而 线程 C 在线程 A 赋值完时判断 instance 就不为 null,此时 C 拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程 C 会继续拿着个没有初始化的对象的数据进行操作。此时容易触发“NPE 异常 NullPointException”。
图解:
(2) 保证可见性
a. 由于可见性问题,线程 A 在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程 C 在主存中判断 instance 还是 null,那么线程 C 又将在自己的工作线程中创建一个实例,这样就创建了多个实例。
b. 如果加上了 volatile 修饰 instance 之后,保证了可见性,一旦线程 A 返回了实例,线程 C 可以立即发现 Instance 不为 null。
7、静态内部类
步骤:
1)构造器私有化
2)定义一个静态内部类,在静态内部类中定义一个实例对象的常量
3)提供一个方法进行获取内部类的常量对象。
代码实现:
public class Singleton7 {
private Singleton7() {
}
//写一个静态内部类,该中有一个静态属性 Singleton
private static class SingletonInnerClass {
private static final Singleton7 INSTANCE = new Singleton7();
}
//提供一个静态的公有方法,直接返回 静态内部类的成员变量
public static Singleton7 getInstance() {
return SingletonInnerClass.INSTANCE;
}
}
优缺点:
(1)这种方式采用了类装载的机制来保证初始化实例只有一个线程。
(2)静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance() 方法,才会装载内部类,从而完成 Singleton 的实例化。
(3)类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
(4)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。
(5)结论:推荐使用。
JVM 在类初始化阶段(即在 Class 被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以通过静态内部类的方式实现延迟初始化方案。
小结
-
静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上 JVM 的特性,这种方式又实现了线程安全的创建单例对象。
-
通过对比基于 volatile 的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于 volatile 的双重检查锁定方案有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化
8、枚举
代码实现:
public enum Singleton8 {
INSTANCE; //属性
private Singleton8() {
}
}
优缺点分析:
(1)借助 JDK1.5 中添加的枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
(2)这种方式是 Effective Java 作者 Josh Bloch 提倡的方式;
(3)结论:推荐使用。
三、单例模式在 JDK 应用的源码分析
1、JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)
2. 代码分析+代码说明
四、单例模式注意事项和细节说明
1、单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
2、当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
3、单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或消耗资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如:数据源、Session 工厂等。)
标签:13,01,instance,对象,静态,实例,线程,单例 From: https://www.cnblogs.com/niujifei/p/16859967.html