在Java语言中,我们知道有23种设计模式,这23种设计模式在Java语言涉及的框架源码中都有体现,比如在Spring框架中BeanFactory用到工厂模式,Spring的AOP切面结合Java的反射技术用到代理模式,Spring中的访问resource下的资源用到策略模式,Spring的bean默认是单例用到单例模式等等,那么接下来就重点为大家介绍下单例模式。
一、单例模式-是什么
单例模式-属于创建类型的一种常用的软件设计模式。单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
二、单例模式-要点
在Java语言中,对于单例模式的设计给出了如下要点:
1、某个类只能有一个实例。
2、它必须自行创建这个实例。
3、它必须自行向整个系统提供这个实例。
具体体现:
1、单例模式的类只提供私有的构造函数。
2、类定义中含有一个该类的静态私有对象。
3、该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对
三、单例模式-应用场景
单例模式应用的场景一般出现在以下条件下: 在资源共享的情况下,避免由于资源操作时导致的性能损耗,如日志文件,应用配置等。在控制资源的情况下,方便资源之间的互相通信,如线程池等。
1、网站的计数器
2、应用配置
3、线程池
4、数据库配置、数据库连接池
5、应用程序的日志应用
四、单例模式-模式类型
在Java中创建单例对象,有多种方式:
1、懒汉式:在真正需要使用对象时才去创建该单例类对象。
2、饿汉式:在类加载时已经创建好该单例对象,等待被程序使用。
3、同步锁:在懒汉式创建单例对象时,会存在线程安全问题,我们使用同步锁解决线程安全问题。
4、双重检查锁:对懒汉式创建单例对象线程安全问题进一步做了优化,双重检查锁避免整个方法被锁住,只对需要锁的代码部分加锁。
5、静态内部类:静态内部类只有在调用时才会加载,它保证了Singleton 实例的延迟初始化,又保证了实例的唯一性。
6、内部枚举类:这种方式不仅能避免多线程同步问题,而且还能防止反射或者反序列化操作重新创建新的对象。
五、代码解析
1. 饿汉式创建单例对象
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
public class Singleton{
// new 一个单例对象
private static final Singleton singleton = new Singleton();
// 私有构造
private Singleton(){}
// 静态函数 - 用于获取实例对象
public static Singleton getInstance() {
return singleton;
}
}
第三行代码已经实例化好了一个Singleton对象在内存中,不会有多个Singleton对象实例存在。
依赖于static静态关键字,类在加载时会在堆内存中创建一个Singleton对象,当类被卸载时,Singleton对象也随之消亡了。
2. 懒汉式创建单例对象
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空)。若已实例化直接返回该类对象,否则先执行实例化操作。
public class Singleton {
// 静态私有对象
private static Singleton singleton;
// 私有构造函数
private Singleton(){}
// 静态函数 - 用于获取实例对象
public static Singleton getInstance() {
// 判空
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
上述代码是很不错的单例模式,不过它不是完美的,但是这并不影响我们使用这个“单例对象”。
3. 懒汉式如何保证只创建一个对象
使用懒汉式创建单例对象,可能会出现的问题:
如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。
// 方法上加锁
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
// 同步代码块加锁
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
解决方法就是在方法上加锁或者使用同步代码块加锁,程序如上述代码。
上述代码规避了两个线程同时创建Singleton对象的风险。
但引来一个新问题:
每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
因此,我们需要继续优化性能,目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例。所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁。
4. 双重检查锁(提高同步锁的效率)
// 优化代码如下
public class Singleton {
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance() {
// 线程A和线程B同时看到
singleton = null,
如果不为null,则直接返回
singleton if (singleton == null) {
// 线程A或线程B获得该锁进行初始化
synchronized(Singleton.class) {
// 其中一个线程进入该分支,另外一个线程则不会进入该分支
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
上述代码完美地解决了并发安全+性能低效问题。
因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)。
5. 使用关键字volatile保证懒汉式线程安全
我们知道new一个对象在JVM执行时的指令是无法保证顺序性的。可能会出现分配对象内存后,先将对象引用赋值给变量,再调用构造器方法,执行初始化操作,这样在多线程环境下就容易导致对象还没有初始化完成就去使用,从而造成空指针问题。
因此另外一种保证懒加载线程安全的方式是使用关键字volatile保证对象实例化过程的顺序性。
/**
* 使用volatile关键字保证懒汉式线程安全问题
*/
public class volatileSingleton {
// 使用volatile关键字修饰
private static volatile volatileSingleton instance = null;
// 构造私有化
private volatileSingleton() {
};
// 获取实例对象
public static synchronized volatileSingleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new volatileSingleton();
}
}
}
return instance;
}
}
6. 静态内部类
这种方式是引入了一个内部静态类(static class),静态内部类只有在调用时才会加载,它保证了Singleton 实例的延迟初始化,又保证了实例的唯一性。
它把Singleton 的实例化操作放到一个静态内部类中,在第一次调用getInstance() 方法时,JVM才会去加载InnerObject类,同时初始化Singleton 实例,所以能让getInstance() 方法线程安全。
特点是:即能延迟加载,也能保证线程安全。
/**
* 静态内部类
*/
public class TestSinglon {
// 构造私有
private TestSinglon() {
}
// 静态内部类
private static class InnerObject{
private static TestSinglon singlon= new TestSinglon();
}
private static TestSinglon getSinglon(){
return InnerObject.singlon;
}
}
静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到反序列化对象时,默认的方式运行得到的结果就是多例的。
7. 内部枚举类实现(防止反射攻击和反序列化)
前面列举的几种获取单例对象的方式都是基于类来进行创建并获取单例对象的,这些方式不能有效的避免通过反射和反序列化操作破坏单例对象这种情况。
而通过枚举类型创建单例对象就有很明显的好处,就是可以防止反射或者反序列化操作使单例对象失效。
/**
* 内部枚举类创建单例
*/
public class SingletonFactory {
// 创建内部枚举类
private enum EnumSingleton{
Singleton;
private TestSinglon testSinglon;
private EnumSingleton() {
testSinglon = new TestSinglon();
}
public TestSinglon getTestSinglon(){
return testSinglon;
}
}
// 通过枚举类获取单例对象
public static TestSinglon getInstance(){
return EnumSingleton.Singleton.getTestSinglon();
}
}
总结
上述为大家列举了单例模式创建对象的方式。对于饿汉式它是在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。而在使用懒汉式获取单例实例对象时就会出现线程安全的问题。而解决懒汉式线程安全问题小编推荐是使用双重检查锁的方式,可以很好的解决并发安全和性能底下问题。而对于通过枚举类创建单例对象这种方式,虽然可以防止单例对象被破坏,但是它缺失了类的特性,没有延迟加载,所以使用较少。