因为最近在准备春招嘛,并且在简历上写了熟悉常见的23种设计模式,单例模式~,你懂得,那我就要搞一下这个我们程序员都听过的大名鼎鼎的单例模式!
23种设计模式我就不多赘述了,我认为单例模式(Singleton Pattern)是java最简单的设计模式之一。设计模式一共分为三种,这种类型的设计模式属于创建型模式,设计模式一共分为三种,它为我们提供了一种创建对象的最佳方式。
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
里氏代换原则(Liskov Substitution Principle))
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
依赖倒置原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:只对接口编程,依赖于抽象而不依赖于具体。
接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
迪米特法则(最少知道原则)(Demeter Principle)
一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。也就是说一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。
单一职责原则(Single-Responsibility-Principle)
核心:一个类只负责一个功能领域中相应的职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
思想:如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
不多bibi上代码!
public class SinglePattern {
//创建 SingleObject 的一个对象
private static SinglePattern instance;
//单例模式的核心就是构造函数私有
private SinglePattern(){
}
//通过静态方法获取唯一可用的对象
public static SinglePattern getInstance() {
if (instance == null) {
instance = new SinglePattern();
}
return instance;
}
}
我们平时谁会没事写一个类,然后把构造函数设置成私有?仔细想一想,构造函数私有了,我们同包或者其他包下的类在创建该类的实例的时候是不是只能通过SinglePattern.SinglePattern()的方式来返回一个我们需要的类的实例对象,没错这就是最基本的也是最简陋的单例模式了,但是这样写是不是有个问题,线程安全吗?我们是不是应该时时刻刻想着并发情况下我们的代码是不是还能安全的运行?
上面的这种单例模式的是实现就是懒汉式
懒汉式小总结
**1、**是否 Lazy 初始化:是(因为只有我们需要用到这个类的实例的时候才会被创建)
**2、**是否多线程安全:否(很简单没有加锁)
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
下面是几种支持多线程的单例模式的实现:
2、懒汉式,线程安全
**1、**是否 Lazy 初始化:是
**2、**是否多线程安全:是
描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
代码如下:
public class SinglePattern {
private static SinglePattern instance;
private SinglePattern(){
}
public static synchronized SinglePattern getInstance() {
if (instance == null) {
instance = new SinglePattern();
}
return instance;
}
}
3、饿汉式
**1、**是否 Lazy 初始化:否
**2、**是否多线程安全:是
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
代码实例:
public class SinglePattern {
private int[] a=new int[100_000];
private int[] b=new int[100_000];
private int[] c=new int[100_000];
private int[] d=new int[100_000];
private static SinglePattern instance=new SinglePattern();
private SinglePattern(){
}
public static synchronized SinglePattern getInstance() {
return instance;
}
}
那我为什么要定义了四个变量呢?就是为了证明饿汉式确实会造成内存浪费的,因为静态变量的初始化以及赋值操作是在类初始化之前完成的,加载一个类分为五步->**1、加载2、链接3、初始化4、使用5、卸载(重点)**具体可以参考jvm类加载具体过程,以后我会出关于jvm的文章。所以这里很明显一上来就会初始化,就会导致那四个变量都初始化了,会占用很大的内存。这就是饿汉式最坑爹的地方。
4、双检锁/双重校验锁(DCL,即 double-checked locking)
**1、**是否 Lazy 初始化:是
**2、**是否多线程安全:是
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
代码:
public class SinglePattern {
private static SinglePattern instance;
private SinglePattern(){
}
public static synchronized SinglePattern getInstance() {
if (instance == null) {
synchronized (SinglePattern.class) {
if (instance == null) {
instance = new SinglePattern();
}
}
}
return instance;
}
}
第一步先判断对象是否为空,为空先锁住这个类,然后再判断是否为空,因为可能两个线程同时进行第一个if判断然后一个先进入,那么第二个线程获得锁之后判断不为空了就会返回,但是应该给变量加一个volatile关键字,想一下我们的程序在运行的时候是有可能进行指令重排的,并且这个instance = new SinglePattern();也不是个原子操作,第一步:先给对象分配内存空间 第二步:执行构造方法初始化对象 第三步:把这个对象指向这个空间。 这个时候如果进行了指令重排不是按123执行,而是132,也就是先指向了分配的内存空间,那么这个时候第二线程判断也不是空,就会导致错误发生了,而volatile关键字能有效的防止指令重排。
下面玩个比较好玩的东西,反射!!!!
单例模式不是防止我们来创建多个对象吗?你不是构造器私吗?好~那我就上最霸道的反射
public class Reflex {
public static void main(String[] args) throws Exception {
SinglePattern instance0 = SinglePattern.getInstance();
//通过反射拿到类的无参构造
Constructor declaredConstructors = SinglePattern.class.getDeclaredConstructor(null);
//通过这个就可以破解类的私有权限
declaredConstructors.setAccessible(true);
//通过反射来创建类的实例
Object instance1 = declaredConstructors.newInstance();
//打印两个对象的hashcode来判断是否是同一个对象
System.out.println(instance0.hashCode());
System.out.println(instance1.hashCode());
}
}
我写了一个测试类,轻松破解单例模式,具体的实现都在代码里展示的淋漓尽致了!
那我们就没有办法来防止反射了吗?当然有
public class SinglePattern {
private static volatile SinglePattern instance;
private SinglePattern(){
synchronized (SinglePattern.class){
if (instance!=null){
throw new RuntimeException("你想通过反射破环单例?");
}
}
}
我们再通过反射来创建对象的时候就会抛出异常,代码在运行,这里建议大家一起跟着敲一下
这就完了吗?要是世界这么简单就好了,请看我的操作。
public class Reflex {
public static void main(String[] args) throws Exception {
//通过反射拿到类的无参构造
Constructor declaredConstructors = SinglePattern.class.getDeclaredConstructor(null);
//通过这个就可以破解类的私有权限
declaredConstructors.setAccessible(true);
//通过反射来创建类的实例
Object instance1 = declaredConstructors.newInstance();
Object instance0= declaredConstructors.newInstance();
//打印两个对象的hashcode来判断是否是同一个对象
System.out.println(instance0.hashCode());
System.out.println(instance1.hashCode());
}
这里我的两个对象都是通过反射来创建,运行结果:
是不是又被我们给破坏了,真的是道高一尺魔高一丈,还有个办法,红绿灯放法,就是设置一个静态变量,进入构造方法的时候通过这个静态变量来判断是否对象被创建过,我们来看代码:
运行结果:
这次我们很好的防止了反射,这就完了?我们知道通过反射是可以获得类的任何权限的任何方法以及字段,当我们通过反编译或者其他手段知道了这个变量,我们在反射的时候是不是又可以给这个变量赋值了呢?没错,这里留个任务大家自己完成吧~
那我们没办法防止反射了嘛?有!枚举类自带防止反射,这是人家规定的,我们来看一下通过反射构造器创建对象的newInstance()方法源码:
现在终于为什么枚举类可以防止的反射了吧~
啊~好累呀,整理了差不多一个小时,希望大家可以认认真真读完,23种设计模式不会给我们升职加薪,我们抱着学习的态度,我们学习的是思想,就像文章开头,谁没事定义一个私有构造器?学完单例模式发现,人家确实是那么玩的,又通过反射来来回回破坏预防,最终知道了为了防止反射我们要用枚举,引文反射真的太霸道了。好了今天就到这吧,真的有些累_-_