序言:针对java初级工程师以上的级别(熟悉java基础,会用ssm或者ssh增删改查)!单例三大特性:自由序列化,线程安全,保证单例。
1、java设计模式是个什么东西
你了解java的框架吗?请问框架是干嘛的?我的理解是,古代带兵打仗,有侦察兵,有粮草兵,有打仗的兵,负责整个军队打胜仗的各个关键!同理,java的框架也是如此,分别是:sprinmvc/struts2负责连接前段,mybatis/hibernate负责操作数据库,还有就是连接这两个框架做到承上启下的作用,明白了吗?
然后再讲下什么是设计模式!它是个什么东西呢,我的理解是,军队打仗,我大唐雄兵百万,如何攻打杨广皇帝的千万大军呢,哼哼,使用我的一字长蛇阵还是我的北斗七星阵,如果这些阵法打不赢话,那我们就用十面埋伏阵,干的他不要不要的!所以,设计模式就相当于一个阵法,在程序中就相当于一个最优的解决方案!
2、单例设计模式的种类
总共五种:懒汉、饿汉、双重校验锁、枚举、静态内部类
我大LPL队伍出征LCK那天,到底选谁呢,有的人说还有其他的种类,在我看来都是按照这五大种设计有一部分的改进而已,我大EDG不是还有一个而对叫EDE吗?LCK队伍中原来不是还有一个SSW 和SSB吗,所以根基大致相同,就是这五种,如果别人说六种,多出来的那一种也没有多大的区别。
第五步在讲具体代码实现!
3、单例设计模式需要用的接口 Serializable
嗯哼?这特么单例设计模式还要实现接口?不不不!我的意思是这个设计模式实现了序列化,会造成什么影响呢?你可以实现序列化接口,当然也可以不实现,但是!这曾经是一道面试题,来自《自如网》一家目前最大的租房公司的最基础面试题之一,肯定比链家还要强势一点。
implements Serializable 序列化
概念:把java对象转换为字节序列的过程,什么场景用?你编写程序把这个对象数据放到一个txt文件中,就要把这个对象数据转换为字节码文件,写到文件中,然后读取该文件的时候把字节码文件转换为对象数据存储到数据库中,这就是序列化与反序列化。
但是我们都会想,单列设计模式序列化怎么了?有啥影响呢?这就对了,你有这样的想法就说明你思考了,我问你,反序列化在获取这个单列设计模式的对象,但是你有没有想过,我反序列化可以获取多个单列设计模式的对象,然后你再想一下,单列设计模式的一个作用,那就是我只能设计一个对象,所以多个对象和一个对象之间,不成立,这就是问题,你可以说我不实现序列化接口不就ok了吗,这种实现序列化的模式也不常见,对,一般工作也用不到,但是面试的时候你不得不知道!而且,我给你们截图看这个设计模式的序列化
这个日期类算不算序列化呢,所以好多地方也会用到设计模式的序列化!但是怎么解决呢,到第五步我在细说,我先把概念都说完。
4、单例设计模式的线程安全问题
什么是线程安全?哼哼,这也是一个面试题!想想?好,那就想想!
你先想,我说一个别的东西,你理解单线程和多线程吗?他们的区别我就不说了,如果多个线程同时访问一个方法修改,这个线程修改中但此刻被另外一个线程修改这个方法,最终会错误!所以我们需要加锁或者synchronized,所以线程安全的意思就是同步。 为什么说这个,看第五步!
5、每种单例设计模式的代码实现
5.1 懒汉 真特懒,大学不到期末不抓紧学习,上班不到最后不交工期!
public class LazyPerson { private static LazyPerson lazyPerson = null; //私有构造器 private LazyPerson(){} //提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象 public static LazyPerson getLazyPerson(){ if (null == lazyPerson) { lazyPerson = new LazyPerson(); } return lazyPerson; }}
这个就是线程不安全,我多个线程同时一访问,又会创建多个对象
当然你可以在上面的方法上加锁,synchronized,这就ok了,但是影响效率
5.2 饿汉,饿的我都着急了,先干了!
//饿汉public class HungryPerson { private static HungryPerson instance = new HungryPerson(); private HungryPerson() {} public static HungryPerson getInstance(){ return instance; }}
5.3 双重校验 将synchronized关键字加在了内部,调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升,但也没什么用!
//双重校验public class Two { private static Two instance = null; private Two() { } public static Two getInstance() { if (null == instance) { synchronized (instance) { if (null == instance) { instance = new Two(); } } } return instance; }}
5.4 静态内部类 解决双重校验低性能问题,使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的,这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次, 并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题,同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。
public class StaticSignle { private StaticSignle() { } private static class SingeletonHolder{ public static StaticSignle instance() { return new StaticSignle(); } } public static StaticSignle getInstance(){ return SingeletonHolder.instance(); }}
5.5 枚举的单例设计模式
它在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。
用到三个类Person EnumSignle 和一个main主程序类
public class Person {}
public enum EnumSingle { INSTANCE; private Person person =null; private EnumSingle(){ person= new Person(); } public Person getPerson(){ return person; }}
public class Main { public static void main(String[] args){ Person person1 = EnumSingle.INSTANCE.getPerson(); Person person2 = EnumSingle.INSTANCE.getPerson(); System.out.println(person1==person2); }}
结果是true!
5.6 解决单例设计模式的序列化问题,使用静态内部类
import java.io.ObjectStreamException;import java.io.Serializable;public class MakeSignle implements Serializable { private static class SingletonClassInstance { private static final MakeSignle instance = new MakeSignle(); } // 方法没有同步,调用效率高 public static MakeSignle getInstance() { return SingletonClassInstance.instance; } // 防止反序列化获取多个对象的漏洞。 // 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。 // 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。 // 当你把对象存储到txt文件中,对象转换成字节码存储,当你在获取对象的数据的时候,字节码转换为该对象,但是这个对象已经不是 // 之前的那个对象,所以用 readResolve可以替换成和之前一样的对象 private Object readResolve() throws ObjectStreamException { return SingletonClassInstance.instance; }}
5.7有些安全问题,通过反射会访问单列的私有构造方法,所以我们同5.6一样也是构造一个静态内部类
就加了一个方法,防止反射的
import java.io.ObjectStreamException;import java.io.Serializable;public class MakeSignle implements Serializable { private static class SingletonClassInstance { private static final MakeSignle instance = new MakeSignle(); } // 方法没有同步,调用效率高 public static MakeSignle getInstance() { return SingletonClassInstance.instance; } // 防止反射获取多个对象的漏洞 private MakeSignle() { if (null != SingletonClassInstance.instance) throw new RuntimeException(); } // 防止反序列化获取多个对象的漏洞。 // 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。 // 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。 // 当你把对象存储到txt文件中,对象转换成字节码存储,当你在获取对象的数据的时候,字节码转换为该对象,但是这个对象已经不是 // 之前的那个对象,所以用 readResolve可以替换成和之前一样的对象 private Object readResolve() throws ObjectStreamException { return SingletonClassInstance.instance; }}
测试这个反射,在main函数中代码如下:
/** * 通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞) */ Classclazz = MakeSignle.class; Constructor c = clazz.getDeclaredConstructor(null); c.setAccessible(true); // 跳过权限检查 MakeSignle sc3 = c.newInstance(); MakeSignle sc4 = c.newInstance(); System.out.println(sc3==sc4);}
运行结果:
如果我们把抛异常的注销掉,如图,main执行结果为false,并不是一个对象,所以这种抛异常的方法解决了反射入侵的问题
这基本涵盖了所有,目前我认为静态内部类和枚举这两个单例设计模式不错!