研磨23种大话设计模式------单例设计模式(第二版,一文搞懂七种单例的实现)

大家好,我是一位在java学习圈中不愿意透露姓名并苟且偷生的小学员,如果文章有错误之处,还望海涵,欢迎多多指正

如果你从本文学到有用的干货知识,请帮忙点个赞呗,据说点赞的都拿到了offer

问题引申:

单例模式—对象的创建问题
类 :一个抽象笼统的概念 描述一组对象
对象:可以有好多个 具体的个体
举例:百度搜索方法 通过对象.搜索();方法的执行—临时空间
为了有效节省内存空间的占用—让当前的类只能创建一个对象

分析过程:

对象的创建本质是调用了类中的构造方法
所以不能随便就让访问类的构造方法,将构造方法私有化,从根源解决问题
那么问题来了,外界要用对象,你类又不让我创建怎么办?
因此类应该自己在内部创建一个对象作为属性然后通过一个公有方法返回出去
并且为了保证这个对象就一份应该添加static修饰该对象属性
那么公有方法此时外界没有对象,怎么去调用呢?因此该公有方法也需添加static,外界之类通过类名访问即可
为什么不让用户直接访问对象属性而要通过公有方法呢?
怕被外界随意修改,所以该属性也应当私有化
那么问题又来了,这个对象应该什么时候创建呢?
这里分两种时候:

  • 类加载的时候就创建(立即加载)
    但可能这个对象还没用到,反而占用内存浪费空间
  • 使用的时候才创建(延迟加载)
    但要保证创建对象这件事只做一次,涉及到多线程安全的问题,后面会细说

补充:如果对象作为属性不加static,直接new 对象赋值会产生如下错误:StackOverFlowError
为什么?
因为相当于一层套一层的创建对象,而每次执行构造方法创建对象时都要占用栈内存(方法的临时执行时在栈中),而在堆内存开辟空间创建对象虽然也会占用堆内存,但堆内存远比栈内存大,所以栈内存先溢出,这也是为什么报的不是堆内存溢出的错误而是栈内存溢出错误的主要原因了

单例模式的四种实现方法:
方法一:饿汉式(立即加载的形式)线程安全但可能占用内存
package singleton;

//单例设计模式
public class SingletonPattern {
    //饿汉式(立即加载的形式)注意该属性不加static会出现StackOverFlowError
    private static SingletonPattern singleton = new SingletonPattern();
    private SingletonPattern(){}
    //方法名叫 getInstance 也可以   符合规范的写法(见名知意)
    public static SingletonPattern getSingleton(){
        return singleton;
    }
}

方法二:懒汉式(延迟加载的形式)非线程安全
package singleton;

//单例设计模式
public class SingletonPattern {

    //懒汉式(延迟加载的形式)
    private static SingletonPattern singletonPattern;
    private SingletonPattern(){}
    public static SingletonPattern getInstance(){
        if(singletonPattern == null){
            singletonPattern = new SingletonPattern();
        }
        return singletonPattern;
    }
}

方法三:懒汉式(延迟加载的形式)线程安全但效率低
package singleton;

//单例设计模式
public class SingletonPattern {

    //懒汉式(延迟加载的形式)
    private static SingletonPattern singletonPattern;
    private SingletonPattern(){}
    public static synchronized SingletonPattern getInstance(){
        if(singletonPattern == null){
            singletonPattern = new SingletonPattern();
        }
        return singletonPattern;
    }
}

补充:以上两种模式使用到synchronized是放在方法上的,故每次方法调用都需要获取锁和释放锁,锁的开销很大

方法四:双重检查锁定模式,线程安全但写法复杂,延迟加载
分析过程:

可以理解为在懒汉式的基础上做了一个变化
需要用到的知识补充:
volatile
不稳定的
修饰属性
1.属性在某一个想成操作的时候被锁定 其他的线程没法获取属性
2.属性被某一个线程修改之后 另外的线程立即可见(即可见性)
3.可以禁止指令重新排布
synchronized(两个关键字都是与线程安全有关)
同步的
修饰方法 或者 方法内的某一段代码
1.当synchronized修饰的部分被线程访问时,这个线程会被锁住

并发编程的三个条件:

1.原子性:要实现原子性方式较多,可用synchronized,lock加锁,AtomicInteger等,但volatile关键字是无法保证原子性的;
2.可见性:要实现可见性,也可用synchronized,lock,volatile关键字可用来保证可见性;
3.有序性:要避免指令重排序,synchronized,lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序 执行的有序性;

模拟不加volatile关键字可能会出现的情况:(代码如下)
public class Singleton {
    static Singleton instance
 	private Singleton(){}
    public static Singleton getInstance(){
       	if (instance == null) {
           	synchronized(Singleton.class) {
                if (instance == null) {
                	instance = new Singleton();
                }
            }	
       	}
       	return instance;
	}
}
分析原因:

假设现在有两个线程A和B,A现在先进入了第2个if判断并执行第14行代码对对象进行初始化。A线程进入到构造方法对对象进行初始化时,由 于JVM会对指令进行重排,所以初始化的代码顺序可能是:

第一种顺序:
1,分配一块内存 M;
2,在内存 M 上初始化 Singleton 对象(对属性o进行初始化和赋值);
3,然后 M 的地址赋值给 instance 变量。

补充:事实上2,3可能发生重排序,但1不会,因为2,3指令需要依托1指令的执行结果

第二中顺序(实际上优化后的执行路径可能是这样的):
1,分配一块内存 M;
2,将 M 的地址赋值给 instance 变量;
3,最后在内存 M 上初始化 Singleton 对象(对属性o进行初始化和赋值)。

执行情况假设:假设虚拟机对指令重排优化后,按第二种顺序执行。A线程现在执行到第二步将 M 的地址赋值给 instance 变量;现在cpu对线程切换并切换到B线程上,B此时若走到第一个if判断,此时instance已不为空,所以直接返回instance(此时A线程还没有完成对instance变量引用的对象完成初始化也没有执行到第16行对对象锁进行释放)。因为此时B返回的instance的属性o没有被初化或赋值,所以B线程返回的instance对象o属性为空,如果B线程对返回的instance对象的o属性进行方法调用可能出现空指针异常。
若其他执行情况可逐一分析(如A线程先执行但还没给instance赋值,此时B判断第一个if为空成立进而在synchronized外等待A释放锁,当A出来时,B肯定要再次判断第二个if,不能进来就new,因为A已经初始化instance变量并将对象地址赋值给了instance变量)

因此解决办法:对instance加上volatile关键字。

原因:volatile可以禁止JVM和处理器对指令进行重排。

总结:这个模式缩小锁的范围,减少锁的开销,同时对象的创建可能发生指令的重排序,使用volatile可以禁止指令的重排序,保证多线程环境内的系统安全

方法五:生命周期托管(单例对象别人帮我们处理,类似于Spring框架中的IOC对象控制权反转)可以线程安全也可以不安全具体看实现,本案例的实现是不安全的,延迟加载
分析过程:

自己创建一个托管类帮我们创建对象(单例的),我们只需要提供类全名(包名.类名)即可,这个方法通用,任何一个类想要实现单例且不用自己创建对象都可以找这个托管类

单例类实现代码:
package singleton;

//单例设计模式
public class SingletonPattern {

    //生命周期托管
    public SingletonPattern(){}
    //测试方法
    public void test(){
        System.out.println("黑夜问白天");
    }
}

托管类实现代码:
package singleton;

import java.util.HashMap;

//单例模式之生命周期托管
public class MySpring {

    //创建一个map集合记录每个类的实例对象 map方便找寻对应的那个实例对象
    //采用map集合是为了方便根据类全名寻找对应的对象
    private static HashMap<String,Object> beanBox = new HashMap<String,Object>();

    //设计一个方法 给定任何一个类全名(参数) 返回一个对应的实例对象(返回值)
    //第一个T 表示是泛型  接返回值得时候不需要造型,用什么类型来接就是什么类型(相当于将值返回时就帮你造了个型)
    //第二个T 表示返回的是T类型的数据
    public static <T>T getBean(String className){
        T obj = null;
        try {
            //1.直接从beanBox集合中获取
            obj = (T)beanBox.get(className);
            //2.如果obj是null 证明之前没有创建过对象
            if(obj == null){
                //3.通过类名字获取Class
                Class clazz = Class.forName(className);
                //4.通过反射创建一个对象
                obj = (T)clazz.newInstance();
                //5.将新的对象存入集合
                beanBox.put(className,obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }
}

主方法代码:
package singleton;

public class TestSingleton {

    public static void main(String[] args){
        SingletonPattern singletonPattern = MySpring.getBean("singleton.SingletonPattern");
        singletonPattern.test();
    }
}

方法六:登记式(通过静态内部类实现)线程安全延迟加载
  1. 这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

  2. 这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance显然是不合适的。这个时候,这种方式相比饿汉式就显得很合理

代码:
package singleton;

/**
 * 单例设计模式
 * 单例的根源,构造方法的私有化
 */
public class SingletonPattern {
	//静态内部类特性,什么时候显示的用到才会加载内部的东西
    private static class SingletonHolder {
        private static final SingletonPattern INSTANCE = new SingletonPattern();
    }
    private SingletonPattern (){}
    public static final SingletonPattern getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
方法七:枚举,线程安全立即加载(jdk1.5才开始出现枚举类的)

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}
总结:

一般情况下,不建议使用第 2 种和第 3 种懒汉方式,建议使用第 1 种饿汉方式。只有在要明确实现延迟加载效果时,才会使用第 6 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

结束:

之后开始慢慢的更新每一种设计模式,通过生动形象的生活现象举例带你感受设计模式的世界,其实设计模式不难,只是当我们面对某个场景时想不到用哪个设计模式该不该用设计模式,怎么用的合理…
博客内容来自腾讯课堂渡一教育拓哥,以及自己的一些理解认识,同时看了其他大牛写的设计模式技术文章综合总结出来的

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值