单例设计模式(Singleton Design Pattern), 一个类只允许创建 一个对象(或者实例),那这个类就是一个单例类,这种设计模式称为单例设计模式,简称单例模式。
如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类。
实现全局ID生成器的代码 :
public class GlobalCounter {
private AtomicLong atomicLong = new AtomicLong(0);
private static final GlobalCounter instance = new GlobalCounter();
// 私有化无参构造器
private GlobalCounter() {
}
public static GlobalCounter getInstance() {
return instance;
}
public long getId() {
return atomicLong.incrementAndGet();
}
}
// 查看当前的统计数量
long courrentNumber = GlobalCounter.getInstance().getId();
我们简单的设计一个日志输出的功能
v1版本
public class Logger {
private String basePath = "D://info.log";
private FileWriter writer;
// new Logger的时候初始化writer
public Logger() {
File file = new File(basePath);
try {
writer = new FileWriter(file, true); //true表示追加写入
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}
使用 V1版本
@RestController("user")
public class UserController {
public Result login(){
// 登录成功
Logger logger = new Logger();
logger.log("tom logged in successfully.");
// ...
return new Result();
}
}
上面的版本会产生如下的问题:多个 logger实例在多个线程中同时操作同一个文件,可能产生相互覆盖的问题。 因为tomcat处理每一个请求都会使用一个新的线程(暂且不考虑多路复用)。此时日志文件就成了一个共享资源,但凡是多线程访问共享资源,我们都要考虑并发修改 产生的问题。
V2版本,对log加锁处理。 这样加锁毫无卵用,方法级别的锁可以保证new出来的同一个实例多线程下可以同步执行log方法,然而你却new了很多个Logger实例。
public synchronized void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
其实,writer方法本身也是加了锁的,我们这样加锁就没有了意义:
public void write(String str, int off, int len) throws IOException {
synchronized (lock) {
char cbuf[];
if (len <= WRITE_BUFFER_SIZE) {
if (writeBuffer == null) {
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
cbuf = writeBuffer;
} else { // Don't permanently allocate very large buffers.
cbuf = new char[len];
}
str.getChars(off, (off + len), cbuf, 0);
write(cbuf, 0, len);
}
}
加锁是一定能解决共享资源冲突问题的,我们只要放大锁的范围从【this】到 【class】,这个问题也是能解决的,代码如下:
public void log(String message) {
synchronized (Logger.class) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
从以上的内容我们发现:
同一个Logger无法并行输出到一个文件中,那么针对这个日志文件创建多个 Logger实例也就失去了意义,如果工程要求我们所有的日志输出到同一个日志文件中,并不需要创建大量的Logger实例,这样的好处有:
在编写单例代码的时候要注意以下几点:
在类加载的时候,instance 静态实例就已经创建并初始化了,所以instance 实例的创建过程是线程安全的。
public class EagerSingleton implements Serializable {
// 持有一个jvm全局唯一的实例
private static final EagerSingleton instance = new EagerSingleton();
// 避免别人随意的创建,需要私有化构造器
private EagerSingleton() {
// 防止反射入侵创建对象
/*if (instance != null) {
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}*/
}
// 暴露一个方法,用来获取实例
public static EagerSingleton getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
// 测试饿汉式单例
System.out.println("测试饿汉式单例>>>" + (EagerSingleton.getInstance() == EagerSingleton.getInstance()));
}
}
恶汉式在工作中反而应该被提倡 ,很多人觉得饿汉式不能支持懒加载,即使不使用也会浪费资源,一方面是内存资源,一 方面会增加初始化的开销。
懒汉式相对于饿汉式的优势是支持延迟加载 。
public class LazySingleton {
// 持有一个jvm全局唯一的实例
private static LazySingleton instance;
// 避免别人随意的创建,需要私有化构造器
private LazySingleton() {
// 防止反射入侵创建对象
if (instance != null) {
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}
}
// 暴露一个方法,用来获取实例
public static LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
System.out.println("测试懒汉式单例instance>>>" + (LazySingleton.getInstance() == LazySingleton.getInstance()));
}
}
当大量并发请求时,上面的写法是无法保证其单例的特性,很有可能会有超过一个线程同时执行了new Singleton(); 从而出现线程安全问题。当然可以加锁来解决, 虽然synchronized锁确实可以保证jvm中有且仅有一个单例实例存在,但是方法上加锁会极大的降低获取单例对象的并发度。
public class LazySingleton {
// 持有一个jvm全局唯一的实例
private static LazySingleton instance;
// 避免别人随意的创建,需要私有化构造器
private LazySingleton() {
// 防止反射入侵创建对象
if (instance != null) {
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}
}
// 暴露一个方法,用来获取实例 synchronized在并发场景下,会排队等待,性能一般
public static synchronized LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
System.out.println("测试懒汉式单例instance>>>" + (LazySingleton.getInstance() == LazySingleton.getInstance()));
}
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。 既要支持延迟加载、又要支持高并发的单例实现方式,双重检查锁。
public class DoubleCheckLockSingleton {
// 持有一个jvm全局唯一的实例
private static volatile DoubleCheckLockSingleton instance;
// 避免别人随意的创建,需要私有化构造器
private DoubleCheckLockSingleton() {
// 防止反射入侵创建对象
if (instance != null) {
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}
}
/**
* 暴露一个方法,用来获取实例
* cpu底层是乱序执行的,volatile如果不加可能会出现半初始化的对象, volatile保证内存可见,保证有序性。
* 现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)
*
*/
public static DoubleCheckLockSingleton getInstance() {
// 多个线程过来,一旦一个线程抢到锁并完成实例化。后面的线程就不会排队等待锁,直接返回单例对象
if (null == instance) {
synchronized (DoubleCheckLockSingleton.class) {
if (null == instance) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
System.out.println("测试双重检查锁创建单例instance>>>" + (DoubleCheckLockSingleton.getInstance() == DoubleCheckLockSingleton.getInstance()));
}
}
还有另一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。 它有点类似饿汉式,但又能做到了延迟加载。具体是怎么做到的呢?
public class InnerSingleton {
private InnerSingleton() {
}
// 对外提供公共的访问方法
public static InnerSingleton getInstance() {
return SingletonHolder.instance;
}
/**
* 定义静态内部类来持有单例对象。
* 静态在第一次使用的时候加载且只加载一次。(在第一次调用getInstance()方法的时候才会加载去实例化单例对象)
*/
private static class SingletonHolder {
private static final InnerSingleton instance = new InnerSingleton();
}
public static void main(String[] args) {
System.out.println("测试静态内部类创建单例 instance>>>" + (InnerSingleton.getInstance() == InnerSingleton.getInstance()));
}
}
SingletonHolder 是一个静态内部类,当外部类 InnerSingleton 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时, SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全, 又能做到延迟加载。
Java 枚举类本身的特性保证了实例创建的线程安全性和唯一性。具体的代码如下所示:
public enum EnumSingleton {
// INSTANCE就是单例对象,相当于 public static final EnumSingleton = new EnumSingleton();
INSTANCE;
}
更通用的写法如下:
public class EnumSingleton {
private EnumSingleton() {
}
public enum Singleton {
// SINGLETON实例化是会执行new Singleton()构造器方法,会实例化EnumSingleton单例对象
SINGLETON;
private EnumSingleton instance;
Singleton() {
instance = new EnumSingleton();
}
// 暴露一个方法,用来获取实例
public EnumSingleton getInstance() {
return instance;
}
}
public static void main(String[] args) {
// 测试枚举单例
System.out.println("测试枚举类创建单例>>>" + (EnumSingleton.Singleton.SINGLETON.getInstance() == EnumSingleton.Singleton.SINGLETON.getInstance()));
}
}
想要阻止其他人构造实例仅仅私有化构造器还是不够的,因为还可以使用反射
获取私有构造器进行构造,当然使用枚举的方式是可以解决这个问题的,对于其他的书写方案,我们通过下边的方式解决:
public class EagerSingleton implements Serializable {
// 持有一个jvm全局唯一的实例
private static final EagerSingleton instance = new EagerSingleton();
// 避免别人随意的创建,需要私有化构造器
private EagerSingleton() {
// 防止反射入侵创建对象
if (instance != null) {
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}
}
// 暴露一个方法,用来获取实例
public static EagerSingleton getInstance() {
return instance;
}
public static void main(String[] args) throws Exception {
// 反射入侵
Class<EagerSingleton> eagerSingletonClass = EagerSingleton.class;
Constructor<EagerSingleton> declaredConstructor = eagerSingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
System.out.println("测试反射入侵单例>>>" + (EagerSingleton.getInstance() == declaredConstructor.newInstance()));
}
测试结果:
# 第一次
测试反射入侵单例>>>false
# 私有构造中加入校验后
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at cn.itcast.designPatterns.singleton.EagerSingleton.main(EagerSingleton.java:56)
Caused by: java.lang.RuntimeException: 实例:【cn.itcast.designPatterns.singleton.EagerSingleton】已经存在,该实例只允许实例化一次
at cn.itcast.designPatterns.singleton.EagerSingleton.(EagerSingleton.java:23)
... 5 more
到目前为止,我们的单例依然是有漏洞的,看如下代码:
public static void main(String[] args) throws Exception {
// 序列化和反序列化入侵 https://blog.csdn.net/leo187/article/details/104332138
// 获取单例并序列化
EagerSingleton singleton = EagerSingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
out.writeObject(singleton);
// 反序列化读取实例
ObjectInputStream input = new ObjectInputStream(new FileInputStream("singleton.txt"));
Object o = input.readObject();
System.out.println("测试序列化和反序列化入侵, 是同一个实例吗?" + (singleton == o));
}
测试结果始终返回false
测试序列化和反序列化入侵, 是同一个实例吗?false
readResolve()方法可以替换从流中读取的对象,在进行反序列化时会尝试执行readResolve
方法,并将返回值作为反序列化的结果而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在, 我们只需要重写readResolve()
方法即可, 代码如下:
public class EagerSingleton implements Serializable {
// 持有一个jvm全局唯一的实例
private static final EagerSingleton instance = new EagerSingleton();
// 避免别人随意的创建,需要私有化构造器
private EagerSingleton() {
// 防止反射入侵创建对象
if (instance != null) {
throw new RuntimeException("实例:【"
+ this.getClass().getName() + "】已经存在,该实例只允许实例化一次");
}
}
// 暴露一个方法,用来获取实例
public static EagerSingleton getInstance() {
return instance;
}
/**
* readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在
*
* @return
*/
public Object readResolve() {
return instance;
}
}
再来看测试结果 , 序列化和反序列的对象是同一个.
测试序列化和反序列化入侵, 是同一个实例吗?true
在JDK或其他通用框架中很少能看到标准的单例设计模式,这也意味着单例设计模式确实很经典,但严格的单例设计确实有它的问题和局限性,我们先看看在源码中的一些案例。
Runtime类封装了运行时的环境, 每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime类实例,但可以通过 getRuntime 方法获取当前Runtime运行时对象的引用。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
//...
}
Runtime测试用例
@Test
public void testRuntime() throws IOException {
Runtime runtime = Runtime.getRuntime();
Process exec = runtime.exec("ping 127.0.0.1");
InputStream inputStream = exec.getInputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len, Charset.forName("GBK")));
}
long maxMemory = runtime.maxMemory();
System.out.println("maxMemory>>>" + maxMemory);
}
Mybaits中的org.apache.ibatis.io.VFS使用到了单例模式。VFS就是Virtual FileSystem的意思,mybatis通过VFS来查找指定路径下的资源。VFS的角色就是对更“底层”的查找指定资源的方法的封装,将复杂的“底层”操作封装到易于使用的高层模块中,方便使用.
public class public abstract class VFS {
// 使用了内部类
private static class VFSHolder {
static final VFS INSTANCE = createVFS();
@SuppressWarnings("unchecked")
static VFS createVFS() {
// ...省略创建过程
return vfs;
}
}
public static VFS getInstance() {
return VFSHolder.INSTANCE;
}
}
OOP 的三大特性是封装、继承、多态。单例将构造私有化,直接导致它无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,损失了可以应对未来需求变化的扩展性,以后一旦有扩展需求,比如写一个具有绝大部分相同功能的单例,我们不得不新建一个十分【雷同】的单例。
单例类只能有一个对象实例。如果未来某一天,一个实例已经无法满足我们的需求,我们需要创建一个,或者更多个实例时,就必须对源代码进行修改,无法友好扩展。
在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。
为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。
spring提供的单例容器,确保一个实例在容器级别的单例,并且可以在容器启动时完成初始化,他的优势如下:
1、所有的bean以单例形式存在于容器中,避免大量的对象被创建,造成jvm内存抖动严重,频繁gc。
2、程序启动时,初始化单例bean,满足fast-fail,将所有构建过程的异常暴露在启动时,而非运行时,更加安全。
3、缓存了所有单例bean,启动的过程相当于预热的过程,运行时不必进行对象创建,效率更高。
4、容器管理bean的生命周期,结合依赖注入使得解耦更加彻底、扩展性无敌。