java设计模式--你真的学会单例模式了嘛?

前言

现在是2020年5月12日22点51分,也是我重新开始学习和整理java设计模式的开始。希望我自己能坚持下来完成这23个设计模式感觉立了一个大flag ,但一篇文章就是一个进步对吧?

最近看到鲁迅写给当时年轻人加油的一句话,我也觉得在某些时候激励了我自己,送个大家一起共勉。
前途很远,也很暗,然而不要怕,不怕的人的面前才有路。

一、什么是单例模式

如果你已经了解过单例模式,那么不妨直接阅读第四大点。直接学习一下枚举来实现单例模式,你就能了解到为什么Joshua Bloch大神都推崇这样来实现单例模式

1.1 基本概念

单例这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

设计模式被大致分为了三种:创建型模式、结构型模式、行为型模式

人话:在java中使用工具类的实现或创建对象需要消耗资源,那么它只允许被创建一次。所有代码都使用这一个实例,那么它就能节约出一部分的内存

1.2单例模式的特点

1、单例类只能有一个实例,节省内存空间
2、单例类必须自己创建自己的唯一实例
3、单例类必须给所有其他对象提供这一实例
4、设计模式中最为简单的几个设计模式
5、自己来控制这个实例,符合面向对象的封装原则

1.3 使用单例的注意事项

1.利用反射的方式来实例化多个不同的实例(可以使用枚举解决)
2.在序列化和反序列换的时候也会出现多个不同的实例(可以使用枚举解决)
3.不要做断开单例类对象与类中静态引用的危险操作。
4.多线程使用单例使用共享资源时,注意线程安全问题

二、为什么需要单例模式

当一个全局使用的类频繁地创建与销毁,会消耗我们一部分系统资源来处理这个过程。那么我们就只保证一个类Class只有一个实例存在, 就可以帮助节省内存,因为它限制了实例的个数,也有利于Java垃圾回收(garbage collection)。
使用场景
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

三、单例模式的常见写法

3.1 立即加载与延迟加载

在介绍单例模式实现之前,我们先解释一下 立即加载 和延迟加载 两个概念。

  • 立即加载 : 在类加载初始化的时候就主动创建实例;

  • 延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。(内存容量有限 ,为了减少并发量,在真正需要数据的时候才进行数据加载操作,减少系统资源的消耗)

延迟加载也被使用在Hibernate3关联关系对象中默认的加载方式,在我们进行一对多查询的时候,我们设置一个lazy=true/false ,数据库就可以根据这个值来决定是否加载附属的信息。

常用实现方式:饿汉式、懒汉式、静态内部类、双重校验锁,最后会让以上几种与枚举来进行比较,从而系统的了解各种方式方式的优劣

3.1饿汉式单例

特点
1.天生就是线程安全,较为常用的一种模式,
2.会提前加载资源

public class Singleton {  
	//指向自己实例的私有静态引用,主动创建
    private static Singleton instance = new Singleton();  
    //私有构建方法
    private Singleton (){}  
    //给所有其他对象提供的自己对象的实例
    public static Singleton getInstance() {  
    return instance;  
    }  
}

3.2懒汉式单例

懒汉式天生是非线程安全的,需要我们使用同步进行保证线程安全。
手动实现两种方式:同步代码块与同步方法,而同步代码块的作用区间更小。所以同步代码块使用的次数更多

同步方法块--实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗
public class Singleton {  
    private static Singleton instance;  
     //私有构建方法
    private Singleton (){}  
    //给所有其他对象提供的自己对象的实例,使用了synchronized进行加锁
    public static synchronized Singleton getInstance() {  
    //调用该方法才进行创建
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

同步代码块--作用区间更小
public class Singleton2 {
 
    private static Singleton2 singleton2;
 
    private Singleton2(){}
 
 
    public static Singleton2 getSingleton2(){
        synchronized(Singleton2.class){  // 使用 synchronized 块,临界资源的同步互斥访问
            if (singleton2 == null) { 
                singleton2 = new Singleton2();
            }
        }
        return singleton2;
    }
}

3.3静态内部类单例

特点
1.与懒汉式加载类似,也是使用时进行加载实例。
2.不会出现线程安全问题,但效率比懒加载更高

public class Singleton {  
	//私有内部类,也是使用时在进行加载。线程安全
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

3.4双重校验锁单例(最常用)

特点
1.除去枚举,单例模式最好的实现方式。
2.同时保证了线程安全与程序的运行效率
3.字段必须使用volatile进行修饰,避免singleton=new Singleton()对象的创建在JVM中可能会进行重排序

public class Singleton {  
	//必须使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    //第一重:判断是否有实例对象,没有就使用同步操作进行创建实例对象,创建成功可以避免同步锁从而加快运行效率
    if (singleton == null) {//使用同步代码块
        synchronized (Singleton.class) {  
        //第二重是为了防止在刚使用这个类的时候(还没有创建好实例对象),有多个对象已经进入了第一重校验,为了保证单例(只创建一个对象)所以还需要一重校验
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}

3.5 小结

其实通过上面代码的实现优化,我们就发现了我们是基于两点来进行实现单例模式的:
保证线程安全的同时,减少同步的区域与次数

四、枚举实现单例模式(最佳方式)

4.1为什么不使用常规的模式实现单利

常用方式:饿汉式、懒汉式、静态内部类、双重校验锁
以上方式都存在缺陷:
1.利用反射的方式来实例化多个不同的实例
2.在序列化和反序列换的时候也会出现多个不同的实例

4.2使用枚举实现单利的原因

单利模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点
枚举:
代码简单
枚举默认就是单利且线程安全
枚举单例可以自己处理序列化

4.3使用枚举实现单利的好处:

Joshua Bloch大神《Effective Java》中有提到,因为其
1.功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,
2.单元素的枚举类型被作者认为是实现Singleton的最佳方法。

4.4实现

//使用枚举实现SingleEnum类的单利
public enum SingleEnum(){
	//枚举的实例化对象,枚举中每个实例化对象都等价与static final修饰
	//表明也只能被实例化一次
	INSTANCE;
	//枚举中构造方法默认---私有化,可以默认不写
	//保证调用枚举(SingleEnum)的实例对象(INSTANCE)才会调用
	SingleEnum(){
	system.out.println("我只会被调用一次");
	}
	//public void xxx方法()
	//public void xxx方法()
}
//获取Single的实例对象
SingleEnum.INSTANCE;
//运行结果
我只会被调用一次

上面的Singleton不会被反射获取多个实例,不用考虑序列化问题.其实更加简化

public enum SingleEnum(){
	INSTANCE;
}
//获取Single的实例对象
SingleEnum.INSTANCE;

五、单例面试题讲解

1.请思考在一个jvm中会出现多个单例吗?

在分布式系统、多个类加载器、以及序列化的的情况下,会产生多个单例,这一点是无庸置疑的。那么在同一个jvm中,会不会产生单例呢?使用单例提供的getInstance()方法只能得到同一个单例,除非是使用反射方式,将会得到新的单例d

原创文章 25 获赞 70 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ChengHuanHuaning/article/details/106087253