您现在的位置是:首页 >学无止境 >二十三种设计模式第四篇--单例模式网站首页学无止境
二十三种设计模式第四篇--单例模式
今天,我们来学习单例模式,说到单例模式,我相信没有哪个初学者不认识他,他在我们项目中也用的贼多。并且单例模式,是java中最简单的一种设计模式,单例模式,也是创建型模式的一种。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意事项
1、单例类只能有一个实例。 2、单例类必须自己创建自己的唯一实例。 3、单例类必须给所有其他对象提供这一实例。
单例模式意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
- Spring 的 ApplicationContext
- Spring依赖注入Bean实例默认是单例的。DefaultSingletonBeanRegistry中的getSingleton()方法
- Mybatis 中的 ErrorContext 与 ThreadLocal
- java.awt.DeskTop 类允许一个Java应用程序启动本地的另一个应用程序去处理URI或文件请求,使用了单例模式中的懒汉式, 而且是容器单例模式
- JDK Runtime 饿汉单例
java.util.logging.LogManager作为全局日志管理器负责维护日志配置和日志继承结构, 使用了单例模式中的饿汉式。
单例模式的优缺点:
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理系统的首页页面缓存)。
2、避免对资源的多重占用(比如经常对文件进行读写文件操作)。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
单例模式的常见使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
单例模式
第一个单例(最简单)
public class Demo_1 {
//手撕一个单例模式
private static Demo_1 instance = new Demo_1();
private Demo_1(){}//构造一个私有的单构造函数
public static Demo_1 getInstance() {
return instance;
}
public void showMesage() {
System.out.println("这个就是一个基本的单例了");
}
}
让我们来创建一个单例对象:
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("你好,我的世界!");
}
}
让我们来创建单例对象的测试类
public class SingletonPatternDemo {
public static void main(String[] args) {
//不合法的构造函数
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject(); 这里是错误的实例
//获取唯一可用的对象 正确的方式
SingleObject object = SingleObject.getInstance();
//显示消息
object.showMessage();
SingleObject obj2=SingleObject.getInstance();
System.out.println( object+" "+obj2);
}
}
好了,上述代码,我们就已经创建一个最基本的单例对象和其测试类了,接下来我将围绕单例模式的类型进行开展了。
单例模式类型
-
饿汉式单例
-
懒汉式单例
-
注册式单例
饿汉式单例
它是在类加载的时候就立即初始化,并且创建单例对象
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好
缺点:类加载的时候就初始化,不管你用还是不用,我都占着空间,浪费了内存,有可能占着茅坑不拉屎,绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题
public class HungrySingleton1 {
//先静态、后动态
//先属性、后方法
//先上后下
private static final HungrySingleton1 hungrySingleton = new HungrySingleton1();
private HungrySingleton1(){}
public static HungrySingleton1 getInstance(){
return hungrySingleton;
}
}
//饿汉式静态块单例
public class HungryStaticSingleton2 {
//以final保证可见
private static final HungryStaticSingleton2 hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton2();
}
private HungryStaticSingleton2(){}
public static HungryStaticSingleton2 getInstance(){
return hungrySingleton;
}
}
public class Test1 {
public static void main(String[] args) {
HungrySingleton1 h1= HungrySingleton1.getInstance();
HungrySingleton1 h2= HungrySingleton1.getInstance();
System.out.println( h1+" "+h2);
HungryStaticSingleton2 h3= HungryStaticSingleton2.getInstance();
HungryStaticSingleton2 h4= HungryStaticSingleton2.getInstance();
System.out.println( h3+" "+h4);
}
}
懒汉式单例
这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题,完美地屏蔽了这两个缺点。
//懒汉式单例
//在外部需要使用的时候才进行实例化
public class LazySimpleSingleton1 {
private LazySimpleSingleton1(){}
//静态块,公共内存区域
private static LazySimpleSingleton1 lazy = null;
//先不叫锁操作
// public static LazySimpleSingleton1 getInstance(){
// if(lazy == null){
// lazy = new LazySimpleSingleton1();
// }
// return lazy;
// }
//加锁操作
public synchronized static LazySimpleSingleton1 getInstance(){
if(lazy == null){
lazy = new LazySimpleSingleton1();
}
return lazy;
}
/*
这段代码其实是分为三步执行:
*/
}
public class LazyDoubleCheckSingleton2 {
//注意volatile:内存可见性.
private volatile static LazyDoubleCheckSingleton2 lazy = null;
private LazyDoubleCheckSingleton2(){}
public static LazyDoubleCheckSingleton2 getInstance(){
//为什么要双重检查 lazy==null
if(lazy == null){
//在些阻塞并不是基于整 个LazySimpleSingleton2的阻塞,而是在getInstance内部的阻塞,。
synchronized (LazyDoubleCheckSingleton2.class){ //性能上稍逊
if(lazy == null){
lazy = new LazyDoubleCheckSingleton2();
//1.分配内存给这个对象
//2.初始化对象
//3.设置lazy指向刚分配的内存地址
//4.初次访问对象
}
}
}
return lazy;
}
}
public class LazyInnerClassSingleton3 {
//默认使用LazyInnerClassGeneral的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private LazyInnerClassSingleton3(){
}
//static 是为了使单例的空间共享保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton3 getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
// 当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的
private static final LazyInnerClassSingleton3 LAZY = new LazyInnerClassSingleton3();
}
}
测试:
public class Test_LazySimpleSingleton1 {
public static void main(String[] args) {
Thread t1=new Thread( new ExectorThread() );
Thread t2=new Thread( new ExectorThread() );
t1.start();
t2.start();
}
}
//通过断点来观察 懒汉单例的问题
class ExectorThread implements Runnable{
@Override
public void run() {
//在断点上右击, 选择 suspend -> thread 模式.
LazyInnerClassSingleton3 singleton=LazyInnerClassSingleton3.getInstance();
System.out.println( Thread.currentThread().getName()+":"+singleton );
}
}
注册式单例
ContainerSingleton: 容器式单例 spring的方案
EnumSingleton: 枚举常量式单例
Spring中经常使用这种方式。
让我们先定义一个枚举类:
//常量中去使用,常量不就是用来大家都能够共用吗?
//通常在通用API中使用
public enum EnumSingleton1 {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton1 getInstance(){
return INSTANCE;
}
}
//利用 jad工具反向编译 EnumSingleton1的 字节码.
/*
public final class EnumSingleton1 extends Enum
{
......
public static final EnumSingleton1 INSTANCE;
private Object data;
private static final EnumSingleton1 $VALUES[];
static //通过 static 创建了实例,实际上是一个饿汉式单例
{
INSTANCE = new EnumSingleton1("INSTANCE", 0);
$VALUES = (new EnumSingleton1[] {
INSTANCE
});
}
}
那么序列化能否破坏枚举式单例模式呢?
我们来分析一下 ObjectInputStream的 readObject() 方法:
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
readEnum() 方法:
Enum<?> en = Enum.valueOf((Class)cl, name);
通过类名和类对象找到唯一一个枚举对象.因此反射是无法破坏枚举式单例的.
cl指的是 EnumSingleton1 反射实例
name指的是: INSTANCE
*/
//Spring中的做法,就是用这种注册式单例
public class ContainerSingleton2 {
private ContainerSingleton2(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getInstance(String className){
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
public class Test1_EnumSingleton {
public static void main(String[] args) {
EnumSingleton1 s1=null;
EnumSingleton1 s2=EnumSingleton1.getInstance();
try(FileOutputStream fos=new FileOutputStream( "EnumSingleton1.obj" );
ObjectOutputStream oos=new ObjectOutputStream( fos ) ){
oos.writeObject( s2 );
oos.flush();
}catch (Exception ex){
ex.printStackTrace();
}
System.out.println("序列化完成");
try(
FileInputStream fis=new FileInputStream( "EnumSingleton1.obj");
ObjectInputStream ois=new ObjectInputStream( fis );
){
s1=(EnumSingleton1)ois.readObject();
}catch(Exception ex){
ex.printStackTrace();
}
System.out.println( s1+" "+s2);
System.out.println( s1==s2);
}
}
说实在的,二十三种设计模式,要想从了解的熟练使用,那就需要去了解没种设计模式的具体案例,借助具体相关的案例,去完成设计模式的拓展使用,这个就需要长期对设计模式的知识面的拓展。编程这条路,学无止境,不断学习,然后发现自己眼见如此之低,不断的发现自己学的东西如此浅显,不断地发现,原来编程不止是CURD,更是一种思维的体现,不同的人有不同的思维,不同的思维针对不同问题有不同的解决方式,当哪天,你可以从CV工程师变成业务工程师,然后从业务工程师变成架构师,你会发现新的世界。
好了,废话说多了,有点过了,既然单例模式已经有了,那么我们来聊聊如何打破单例模式。
打破单例模式:
利用反射打破单例模式
懒汉模式单例
//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
//完美地屏蔽了这两个缺点
public class LazyInnerClassSingleton {
//默认使用LazyInnerClassGeneral的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private LazyInnerClassSingleton(){
if( LazyHolder.LAZY!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
//static 是为了使单例的空间共享保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
//因为加了final , 保证了
// 当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
懒汉模式的反射打破方式
public class Test2_innerclassSingleton {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//破坏单例
Class cls= LazyInnerClassSingleton.class;
//获取构造方法
Constructor con=cls.getDeclaredConstructor(null);
//修改访问权限
con.setAccessible(true);
//初始化
Object o1=con.newInstance();
Object o2=con.newInstance();
System.out.println( o1+" "+o2);
}
}
反序列化打破单例模式
一个对象创建好了后,有时需要将对象序列化后存到磁盘,下次再反序列化出来,转为内存对象, 此时,反序列化后的对象会重新分配内存,但如果反序列化后的对象为单例对象,就违反了单例初衷。
//反序列化时导致单例破坏
public class SeriableSingleton implements Serializable {
//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
//将内存中状态给永久保存下来了
//反序列化: 将已经持久化的字节码内容,转换为IO流
//通过IO流的读取,进而将读取的内容转换为Java对象, 在转换过程中会重新创建对象new
//SeriableSingleton,会先初始化内部类
//如果没使用的话,内部类是不加载的
private SeriableSingleton(){
if( SeriableSingleton.LazyHolder.LAZY!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
//static 是为了使单例的空间共享保证这个方法不会被重写,重载
public static final SeriableSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return SeriableSingleton.LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
//因为加了final , 保证了
// 当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的
private static final SeriableSingleton LAZY = new SeriableSingleton();
}
//解决方案: 增加一个 readResolve()方法即可.
private Object readResolve(){
return SeriableSingleton.LazyHolder.LAZY;
}
}
public class Test1_serialize_error {
public static void main(String[] args) {
SeriableSingleton s1=null;
SeriableSingleton s2=SeriableSingleton.getInstance();
try(FileOutputStream fos=new FileOutputStream( "seriableSingleton.obj" );
ObjectOutputStream oos=new ObjectOutputStream( fos ) ){
oos.writeObject( s2 );
oos.flush();
}catch (Exception ex){
ex.printStackTrace();
}
System.out.println("序列化完成");
try(
FileInputStream fis=new FileInputStream( "seriableSingleton.obj");
ObjectInputStream ois=new ObjectInputStream( fis );
){
s1=(SeriableSingleton)ois.readObject();
}catch(Exception ex){
ex.printStackTrace();
}
System.out.println( s1+" "+s2);
}
}
那么我们如何避免反序列化破坏单例模式呢?
解决方案加入 readResolve() 的源码分析:
1. ObjectInputStream类 -> readObject () 在这里调用了重写的 readObject0()方法
2. 在readObject0()中
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
3. readOrdinaryObject()方法中
Object obj;
try {
//只要有无参构造方法,就调用.
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//上面这一段代码已经完成了对象的创建
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
但在这里,它判断 了一下 是否有 readResolve() 方法,如果有,则调用.
4. 那么问题来了, 这个 desc.hasReadResolveMethod() 方法用来判断 readResolve()是否存在又是在哪里创建的呢?
它是在 ObjectStreamClass的构造方法中在初始化阶段就创建好了:
readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);
最后,虽然通过 增加 readResolve()方法返回实例以解决单例模式被破坏的问题,但实际上底层此对象是被实例化过两次.
对于上述注册式单例模式的补充,使用枚举来以防序列化打破单例模式
public class Test2_EnumConstructor {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class cl= EnumSingleton1.class;
Constructor c= cl.getDeclaredConstructor( );
c.newInstance();
//以上代码异常: NoSuchMethodException: com.yc.designpattern.DMA3_单例模式.singleton.register5.EnumSingleton1.<init>()
//Enum类中没有这个无参构造方法,只一个protected修饰的带两个参数的构造方法
Enum e;
/*
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
*/
}
}
public class Test3_EnumConstructor2 {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Class clz=EnumSingleton1.class;
Constructor c=clz.getDeclaredConstructor( String.class, int.class);
c.setAccessible(true);
EnumSingleton1 es1= (EnumSingleton1) c.newInstance("tom",666);
/*
IllegalArgumentException: Cannot reflectively create enum objects 不能用反射的方式来创建对象
为什么呢? 查看一下 反射中 Constructor中的 newInstance()方法.
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
它在newInstance()中做了强制性判断 , 如果修饰符为 Enum,则抛出异常.
这种枚举型单例是effective java中推荐的实现写法。
小结: jdk橘生淮南的语法特殊性及反射为枚举式单例保驾。
*/
}
}
使用ThreadLocal创建单例
/**
* ThreadLocal不能保证其创建的对象是全局唯一的,但能保证单个线程中是唯一的。
* 线程id Map
* A Map
* B Map
* C Map
*/
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
public class Test1 {
public static void main(String[] args) {
System.out.println( ThreadLocalSingleton.getInstance() );
System.out.println( ThreadLocalSingleton.getInstance() );
System.out.println( ThreadLocalSingleton.getInstance() );
System.out.println( ThreadLocalSingleton.getInstance() );
}
}