单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
主要解决:
一个全局使用的类频繁地创建与销毁。
何时使用:
当您想控制实例数目,节省系统资源的时候。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
介绍
##意图:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
如何解决:
判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
使用场景:
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
案例实现:
- 方式一:懒汉式
注意:如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
/**
* Author:YANKAI
* Date:2019/4/22
* Time:16:26
* Desc:单例模式:懒汉式单例
*/
class LazySingleton {
private static volatile LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
注意:如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
- 方式二:饿汉式
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
/**
* Author:YANKAI
* Date:2019/4/22
* Time:16:34
* Desc:单例模式:饿汉式单例
*/
class HungrySingleton {
private static HungrySingleton sqlFactory = new HungrySingleton();
private HungrySingleton() {
}
public static synchronized HungrySingleton getInstance() {
return sqlFactory;
}
}
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
- 方式三:双重加锁
/**
* Author:YANKAI
* Date:2019/4/22
* Time:16:34
* Desc:单例模式:双重检查加锁法
*/
class SQLFactory {
private static volatile SQLFactory sqlFactory = null;
private SQLFactory() {
}
public static SQLFactory getInstance() {
if (sqlFactory == null) {
synchronized (SQLFactory.class) {
if (sqlFactory == null) {
sqlFactory = new SQLFactory();
}
}
}
return sqlFactory;
}
}
为什么需要两次判断 if (sqlFactory == null)?
第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。
第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
需要注意的是, private static volatile SQLFactory sqlFactory = null;;需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。