一、什么是单例模式
单例模式定义很简单:Ensure a class has onliy one instance,and provide a globle point of access to it.
一个类中能创建一个实例,并且自行实例化并向整个系统提供这个实例。
那我们什么时候会用到单例模式呢??
-
那我们想想既然一个类中只能创建一个实例了,那么可以说这是跟类的状态与对象无关的了。
-
频繁创建对象、管理对象是一件耗费资源的事,我们只需要创建一个对象来用就足够了!
学过Java Web的同学可能就知道:
-
Servlet是单例的
-
Struts2是多例的
-
SpringMVC是单例的
那既然多例是频繁创建对象、需要管理对象的,那Struts2为什么要多例呢??
-
主要由于设计层面上的问题,Struts2是基于Filter拦截类的,ognl引擎对变量是注入的。所以它要设计成多例的~
能使用一个对象来做就不用实例化多个对象!这就能减少我们空间和内存的开销~
那有可能有的人又会想了:我们使用静态类.doSomething()和使用单例对象调用方法的效果是一样的啊。
-
没错,效果就是一样的。使用静态类.doSomething()体现的是基于对象,而使用单例设计模式体现的是面向对象。
单例模式的优点:
-
减少内存开支
-
减少系统性能开销
-
避免对资源的多重占用
-
单例模式可以在系统设置全局的访问点,优化和共享资源访问。
单例模式的缺点:
-
单例模式一般没有接口,拓展很困难。
-
单例模式对测试时不利的。
-
单例模式与单一职责原则有冲突。
单例模式的使用场景:
-
要求生成唯一序列号的环境;
-
在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
-
创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
-
需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
二、分类
编写单例模式的代码其实很简单,就分了三步:
-
将构造函数私有化
-
在类的内部创建实例
-
提供获取唯一实例的方法
1.饿汉式:
package com.whohim.firstSimgleton;
//饿汉式单例
public class Emperor {
private static final Emperor emperor = newEmperor();
private Emperor() {
//世俗和道德约束你,目的就是不希望产生第二个皇帝
}
public static Emperor getInstance() {
return emperor;
}
//皇帝发话了
public static void say() {
System.out.println("我就是皇帝某某某...");
}
}
package com.whohim.firstSimgleton;
public class Minister {
public static void main(String[] args) {
for(int i=0 ;i<3;i++) {
Emperor emperor = Emperor.getInstance();
emperor.say();
}
//三天见的皇帝都是同一个人
}
}
2.简单懒汉式
package com.whohim.firstSimgleton;
//懒汉式单例
public class LazyEmperor {
private static LazyEmperor lazyEmperor = null;
// 限制产生多个对象
private LazyEmperor() {
}
// 通过该方法获得实例对象
public static LazyEmperor getLazyEmperor() {
if (lazyEmperor == null) {
lazyEmperor = new LazyEmperor();
}
return lazyEmperor;
}
public static void say() {
System.out.println("我就是懒汉皇帝某某某...");
}
}
该单例模式在低并发的情况下不会出现问题,若系统压力增大,并发量增加时则可能在内存中出现多个实例,破坏了最初的预期。线程A执行 lazyEmperor = newLazyEmperor(),但是!还没有获得对象时(上吊也要喝口水是不是?),此时,线程B也来了,执行到if (lazyEmperor== null)判断为真,两个线程继续执行下去,结果两个线程都获得了对象,在内存中出现了两个对象!
要解决也很简单,我们只要加锁就行了:
package com.whohim.firstSimgleton;
//懒汉式单例
public class LazyEmperor {
// 限制产生多个对象
private static LazyEmperor lazyEmperor = null;
private LazyEmperor() {
}
// 通过该方法获得实例对象
public static synchronized LazyEmperor getLazyEmperor() {
if (lazyEmperor == null) {
lazyEmperor = new LazyEmperor();
}
return lazyEmperor;
}
public static void say() {
System.out.println("我就是懒汉皇帝某某某...");
}
}
3.双重检测机制(DCL)懒汉式
package com.whohim.firstSimgleton;
public class DlcLazyEmperor {
private DlcLazyEmperor() {
}
private static volatile DlcLazyEmperor dlcLazyEmperor = null;
public static DlcLazyEmperor getDlcLazyEmperor() {
if (dlcLazyEmperor == null) {
// 将锁范围缩小,提高性能
synchronized (DlcLazyEmperor.class) {
// 再判断一次是否为空
if (dlcLazyEmperor == null) {
dlcLazyEmperor = new DlcLazyEmperor();
// 其实锁住这里面,有一次判断为空就够了,
// 外面那个判断为空主要为了提高性能
}
}
}
return dlcLazyEmperor;
}
public static void say() {
System.out.println("我就是DLC懒汉皇帝某某某...");
}
}
4.静态内部类懒汉式
还可以使用静态内部类这种巧妙的方式来实现单例模式!它的原理是这样的:
-
当任何一个线程第一次调用getInstance()时,都会使LazyHolder被加载和被初始化,此时静态初始化器将执行StaticInnerClassLazy的初始化操作。(被调用时才进行初始化!)
-
初始化静态数据时,Java提供了的线程安全性保证。(所以不需要任何的同步)
package com.whohim.firstSimgleton;
public class StaticInnerClassLazy {
private StaticInnerClassLazy() {
}
// 使用内部类的方式来实现懒加载
private static class LazyHolder {
// 创建单例对象
private static final StaticInnerClassLazy INSTANCE= new StaticInnerClassLazy();
}
// 获取对象
public static final StaticInnerClassLazy getInstance() {
return LazyHolder.INSTANCE;
}
public static void say() {
System.out.println("我就是StaticInnerClass懒汉皇帝某某某...");
}
}
静态内部类这种方式是非常推荐使用的!很多人没接触过单例模式的都不知道有这种写法,这种写法很优化也高效!
5.枚举方式实现
使用枚举就非常简单了:
public enum whohim {
WHOHIM,HIMHIM;
}
那这种有啥好处??枚举的方式实现:
-
简单,直接写就行了
-
防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候(安全)!
这种也较为推荐使用!
参考: 《设计模式之禅》、https://mp.weixin.qq.com/s/dU_Mzz76h-qQZvrgeSe44g