又一个WordPress站点

圣剑锻造师单例模式 设计模式-那点事

单例模式 设计模式-那点事
前言
第一次写文章,排版还是挺难的啊!如有不足,请多多指正,谢谢!

关键这几天天气无常,太冻了,大家做好防冻措施
最简单的解释
一个类只能创建一个对象。
代码结构:
1、私有构造器;
2、单例对象;
3、静态工厂方法;
代码如下:
public class Single1 { private Single1() { System.out.println("生成Single1实例一次查尔斯曼森 !!马加爵遗书 !"); } //--------------------懒汉--------------------- private static Single1 instance = null; public static Single1 getInstance() { //当多线程时,多个线程极有可能同时通过这个条件判断 if(instance == null){instance = new Single1(); } return instance; } //-------------------饿汉------------------------// private static Single1 instance = new Single1();// // public static Single1 getInstance() {// return instance;// }}关键点
要让一个类只能创建一个对象,就不能随便new对象,即私有构造器;
instance是Single1类的静态成员,庞祖云 初始值可为null,也可为new Single1()。
区别:
null:还没有构建,需要构建并返回,这种模式就属于懒汉模式;
new Single1():一开始就构建好,直接返回,这种模式就属于饿汗模式;
优缺点(通俗讲):
懒汗模式:懒的人等饭吃。优:延迟加载,需要用的时候才加载(时间换空间);缺:多线程下极有可能创建多个实例,线程不安全
饿汗模式:饿的人主动找饭吃梁镱凡 。优:线程安全,运行效率高;缺:即使外部没用到也会被创建,毫无意义的耗资源(空间换时间)
3、getInstance()是获取单例模式的唯一途径
发现问题
懒汉模式为什么线程会不安全?
分析
当线程1和线程2同时通过条件判断,这种情况下就会创建多个实例了
解决
既然是线程安全问题,就加个锁
看代码:
public class Single2 { private static Single2 instance = null; private Single2(){} public static Single2 getInstance() { if(instance == null){//1、synchronized同步锁必要的代码块,解决线程安全synchronized (Single2.class) { if(instance == null){ instance = new Single2(); }} } return instance; } //2、对整个方法进行同步锁// public synchronized static Single2 getInstance() {// if(instance == null){//instance = new Single2();// }// return instance;// }}
关键点:
1、为了防止new Singleton()被执行多次异界贸易商,要在new之前加上Synchronized 同步锁,锁住整个类(注意叶建灵,这里不能使用对象锁,以后解释)文王梦熊 。
2、
(1)代码块同步锁。此方法为什么要多一个判空操作?当线程1创建完并返回实例后斯库亚德,线程2也进来了,如果不判空甘心情愿简谱,那还会再次创建实例,这样就白搭了。这就叫双重监测机制。
(2)方法同步锁。不建议次方法。锁的粒度太大,很多线程同时访问的时候导致阻塞很严重。
就是这么简单!
真以为就那么简单常胤 啊鲜于贞雅??蒋俐玮?你还是太年轻宽霖法师 !

发现问题
此处还有一个漏洞,到底是什么呢?
分析
这里涉及到JVM编译器的指令重排。
所谓指令重排,就比如instance = new Single2(),会被编译器编译成如下指令:
memory = allocate(); //1、分配对象的内存空间
ctorInstance(memory); //2、初始化对象
instance = memory; //3、设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化忻尚永,指令重排成下面的顺序:
memory = allocate(); //1、分配对象的内存空间
instance = memory; //3、设置instance指向刚分配的内存地址
ctorInstance(memory); //2、初始化对象
当线程1执行完1、3时,instance对象还没初始化,但是已经不指向null了,条件判断为false,如果线程2抢占了CPU的资源狼吞虎咽造句,那么就会返回一个没有初始化完的instance对象
解决:在instance对象前面增加一个修饰符volatile。
volatile:简单地说就是防止编译器对代码进行优化。也就是让指令按照上面的1、2、3顺序执行。这样就不会出现其他顺序了
代码如下:
public class Single3 { private volatile static Single3 instance = null; private Single3(){} public static Single3 getInstance() { if(instance == null){//1、synchronized同步锁必要的代码块,解决线程安全synchronized (Single3.class) { if(instance == null){ instance = new Single3(); }} } return instance; }}
发现问题
还有没其他实现方式呢?
答案:
有的。静态内部类
look the code:
public class Single4 { private static class LazyHolder { private static final Single4 INSTANCE = new Single4(); }private Single4(){} public static Single4 getInstance(){ return LazyHolder.INSTANCE; }}
关键点:
1、外部无法访问到LazyHolder,只有调用getInstance()才能获得单例对象。
2、INSTANCE对象初始化的时机并不是在单例类Single4被加载的时候,而是在调用getInstance()方法,使得静态内部类LazyHolder被加载的时候。
因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
发现问题
构建对象除了new之外像小朵一样,反射也能构建对象,那么反射是不是能攻破单例模式呢?
分析
try { //获取Single4的构造器 Constructor<Single4> con = Single4.class.getDeclaredConstructor(); //设为可访问 con.setAccessible(true); //构建多个对象 Single4 instance1 = con.newInstance(); Single4 instance2 = con.newInstance(); //验证是否不同对象 System.out.println(instance1.equals(instance2)); //运行结果为false } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); }
解决
用枚举实现单例
代码如下:
public enum SingleEnum { INSTANCE; private Single5 instance; SingleEnum(){ instance = new Single5(); } public Single5 getInstance(){ return instance; }}class Single5{}
那么我们再次执行一下那个反射,看看结果如何
try { //获取构造器 Constructor<SingleEnum> con = SingleEnum.class.getDeclaredConstructor(); //设为可访问 con.setAccessible(true); //构建多个对象 SingleEnum single1 = con.newInstance(); SingleEnum single2 = con.newInstance(); //验证是否不同对象 System.out.println(single1.equals(single2)); //运行结果报错} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace();}
报错信息:
java.lang.NoSuchMethodException: pattern.single.SingleEnum.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at pattern.single.Test1.testSingle5(Test1.java:39) at pattern.single.Test1.main(Test1.java:10)
关键点:
1、枚举基本用法
enum MyEnum{ A,B;}
创建enum时,编译器是这样编译的
class MyEnum extends Enum{ public static final MyEnum A; public static final MyEnum B;}
我们可以把MyEnum看作一个类圣剑锻造师,而把A,B看作类的MyEnum的实例。当然请叫我秦三世,这个构建实例的过程不是我们做的,一个enum的构造方法限制是private的,也就是不允许我们调用卢恬儿。
2、枚举单例不仅能防止反射构建实例,还能保证线程安全。缺点就是该枚举类被加载时就构建了单例,所以不是懒加载
单元素的枚举类型已经成为实现Singleton的最佳方法。
--- ---《Effective Java》
总结

上图为借用【程序员小灰】的
其实认真看还是很简单的
涉及到的重要知识点
1、类锁和对象锁
2、classloader的加载机制
3、JVM相关知识
参考
漫画:什么是单例模式?(整合版):http://blog.csdn.net/bjweimengshu/article/details/78716839
【程序员小灰】补充:
1、volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。
2、使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
3、对于其他方式实现的单例模式王婉茹 ,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。
作者:admin | 分类:全部文章 | 浏览:54 2018 07 01  
« 上一篇 下一篇 »