以前文章已经说过了设计模式的七大原则,即接口屏蔽原则,开闭原则,依赖倒转原则,迪米特原则,里氏替换原则,单一职责原则,合成复用原则,不明白的,能够移至XXXX(代写)。从今天开始咱们就要学习一些常见的设计模式,方便咱们之后看源码使用,固然,也能够指导咱们日常的编码任务。
设计模式
咱们常见的设计模式主要有23种,分为3种类型,咱也不全说,只写重要的几个把。缓存
建立型:单例模式,工厂模式,原型模式安全
结构型:适配器模式,装饰模式,代理模式bash
行为型:模板模式,观察者模式,状态模式,责任链模式markdown
系统中只须要一个全局的实例,好比一些工具类,Converter,SqlSession等。多线程
咱们须要将构造方法私有化,而默认不写的话,是公有的构造方法,外部能够显式的调用来建立对象,咱们的目的是让外部不能建立对象。框架
对外只提供一个公有的的方法,用来获取实例,而这个实例是不是惟一的,单例的,由方法决定,外部无需关心。jvm
public class A { //私有的构造方法 private A(){} //私有的静态变量 private final static A a=new A(); //对外的公有方法 public static A getInstance(){ return a; } }复制代码
public class test { public static void main(String[] args){ A a1=A.getInstance(); System.out.println(a1.hashCode()); A a2=A.getInstance(); System.out.println(a2.hashCode()); } }复制代码
该方法采用的静态常量的方法来生成对应的实例,其只在类加载的时候就生成了,后续并不会再生成,因此其为单例的。函数
在类加载的时候,就完成实例化,避免线程同步问题。工具
没有达到懒加载的效果,若是从始到终都没有用到这个实例,可能会致使内存的浪费。
public class A { //私有的构造方法 private A(){} //私有的静态变量 private final static A a; //静态代码块 static{ a=new A(); } //对外的公有方法 public static A getInstance(){ return a; } }复制代码
public class test { public static void main(String[] args){ A a1=A.getInstance(); System.out.println(a1.hashCode()); A a2=A.getInstance(); System.out.println(a2.hashCode()); } }复制代码
该静态代码块的饿汉式单例模式与静态变量的饿汉式模式大同小异,只是将初始化过程移到了静态代码块中。
与静态变量饿汉式的优缺点相似。
public class A { //私有的构造方法 private A(){} //私有的静态变量 private static A a; //对外的公有方法 public static A getInstance(){ if(a==null){ a=new A(); } return a; } }复制代码
同上。
该方法的确作到了用到即加载,也就是当调用getInstance的时候,才判断是否有该对象,若是不为空,则直接放回,若是为空,则新建一个对象并返回,达到了懒加载的效果。
当多线程的时候,可能会产生多个实例。好比我有两个线程,同时调用getInstance方法,并都到了if语句,他们都新建了对象,那这里就不是单例的啦。
public class A { //私有的构造方法 private A(){} //私有的静态变量 private static A a; //对外的公有方法 public synchronized static A getInstance(){ if(a==null){ a=new A(); } return a; } }复制代码
同上。
经过synchronize关键字,解决了线程不安全的问题。若是两个线程同时调用getInstance方法时,那就先执行一个线程,另外一个等待,等第一个线程运行结束了,另外一个等待的开始执行。
这种方法是解决了线程不安全的问题,却给性能带来了很大的问题,效率过低了,getInstance常常发生,每一次都要同步这个方法。
咱们想着既然是方法同步致使了性能的问题,咱们核心的代码就是新建对象的过程,也就是new A();
的过程,咱们能不能只对部分代码进行同步呢?
那就是方法5啦。
public class A { //私有的构造方法 private A(){} //私有的静态变量 private static A a; public static A getInstance(){ if(a==null){ synchronized (A.class){ a=new A(); } } return a; } } 复制代码
如上。
懒汉式的通用优势,用到才建立,达到懒加载的效果。
这个没有意义,并无解决多线程的问题。咱们能够看到若是两个线程同时调用getInstance方法,而且都已经进入了if语句,即synchronized的位置,即使同步了,第一个线程先执行,进入synchronized同步的代码块,建立了对象,另外一个进入等待状态,等第一个线程执行结束,第二个线程仍是会进入synchronized同步的代码块,建立对象。这个时候咱们能够发现,对这代码块加了synchronized没有任何意义,仍是建立了多个对象,并不符合单例。
public class A { //私有的构造方法 private A() { } //私有的静态变量 private volatile static A a; //对外的公有方法 public static A getInstance() { if (a == null) { synchronized (A.class) { if (a == null) { a = new A(); } } } return a; } } 复制代码
同上。
强烈推荐使用,这种写法既避免了在多线程中出现线程不安全的状况,也能提升性能。
咱具体来讲,若是两个线程同时调用了getInstance方法,而且都已到达了if语句以后,synchronized语句以前,此时第一个线程进入synchronized之中,先判断是否为空,很显然第一次确定为空,那么则新建了对象。等到第二个线程进入synchronized之中,先判断是否为空,显然第一个已经建立了,因此即不新建对象。下次,不论是一个线程或者多个线程,在第一个if语句那就判断出有对象了,便直接返回啦,根本进不了里面的代码。
就是这么完美,没有缺点,哈哈哈。
咱先来看一个概念,重排序
,也就是语句的执行顺序会被从新安排。其主要分为三种:
1.编译器优化的重排序:能够从新安排语句的执行顺序。
2.指令级并行的重排序:现代处理器采用指令级并行技术,将多条指令重叠执行。
3.内存系统的重排序:因为处理器使用缓存和读写缓冲区,因此看上去多是乱序的。
上面代码中的a = new A();可能被被JVM分解成以下代码:
// 能够分解为如下三个步骤
1 memory=allocate();// 分配内存 至关于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址复制代码
// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 至关于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象 复制代码
一旦假设发生了这样的重排序,好比线程A在执行了步骤1和步骤3,可是步骤2尚未执行完。这个时候线程B有进入了第一个if语句,它会判断a不为空,即直接返回了a。其实这是一个未初始化完成的a,即会出现问题。
因此咱们会将入volatile关键字,来禁止这样的重排序,便可正常运行。
public class A { //私有构造函数 private A() { } //私有的静态内部类 private static class B { //私有的静态变量 private static A a = new A(); } //对外的公有方法 public static A getInstance() { return B.a; } }复制代码
B在A装载的时候并不会装载,而是会在调用getInstance的时候装载,这利用了JVM的装载机制。这样一来,优势有两点,其一就是没有A加载的时候,就装载了a对象,而是在调用的时候才装载,避免了资源的浪费。其二是多线程状态下,没有线程安全性的问题。
没有缺点,太完美啦。
若是不明白反射,能够查看我以前的文章,传送门,万字总结之反射(框架之魂)。
若是咱们的对象是经过反射方法invoke出来,这样新建的对象与经过调用getInstance新建的对象是不同的,具体咱来看代码。
public class test { public static void main(String[] args) throws Exception { A a=A.getInstance(); A b=A.getInstance(); System.out.println("a的hash:"+a.hashCode()+",b的hash:"+b.hashCode()); Constructor<A> constructor=A.class.getDeclaredConstructor(); constructor.setAccessible(true); A c=constructor.newInstance(); System.out.println("a的hash:"+a.hashCode()+",c的hash:"+c.hashCode()); } }复制代码
咱们来看下运行结果:
咱们能够看到c的hashcode是和a,b不同,由于c是经过构造器反射出来的,由此能够证实私有构造器所组成的单例模式并非十分安全的。
咱们先将A类实现一个Serializable接口,具体代码以下,跟以前的双重if检查同样,只是多了个接口。
public class A implements Serializable { //私有的构造方法 private A() { } //私有的静态变量 private volatile static A a; //对外的公有方法 public static A getInstance() { if (a == null) { synchronized (A.class) { if (a == null) { a = new A(); } } } return a; } } 复制代码
测试类:
public class test { public static void main(String[] args) throws Exception { A s = A.getInstance(); //写 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("学习Java的小姐姐")); oos.writeObject(s); oos.flush(); oos.close(); //读 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("学习Java的小姐姐")); A s1 = (A)ois.readObject(); ois.close(); System.out.println(s+"\n"+s1); System.out.println("序列化先后两个是否同一个:"+(s==s1)); } }复制代码
咱们来看下运行结果,很显然序列化先后两个对象并不相等。为何会出现这种问题呢?这个讲起来,又能够写一篇文章了。简单来讲,任何一个readObject方法,不论是显式的仍是默认的,它都会返回一个新建的实例,这个新建的实例不一样于该类初始化时建立的实例。
public enum A { a; public A getInstance(){ return a; } } 复制代码
看着代码量不多,咱们将其编译下,代码以下:
public final class A extends Enum< A> { public static final A a; public static A[] values(); public static AvalueOf(String s); static {};
}复制代码
public class test { public static void main(String[] args) throws Exception { A a1 = A.a; A a2 = A.a; System.out.println("正常状况下,实例化两个实例是否相同:" + (a1 == a2)); Constructor<A> constructor = null; constructor = A.class.getDeclaredConstructor(); constructor.setAccessible(true); A a3 = null; a3 = constructor.newInstance(); System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode()); System.out.println("经过反射攻击单例模式状况下,实例化两个实例是否相同:" + (a1 == a3)); } }复制代码
运行结果:
咱们看到报错了,是在寻找构造函数的时候报错的,即没有无参的构造方法,那咱们看下他继承的父类ENUM有没有构造函数,看下源码,发现有个两个参数String和int类型的构造方法,咱们再看下是否是构造方法的问题。
咱们再用父类的有参构造方法试下,代码以下:
public class test { public static void main(String[] args) throws Exception { A a1 = A.a; A a2 = A.a; System.out.println("正常状况下,实例化两个实例是否相同:" + (a1 == a2)); Constructor<A> constructor = null; constructor = A.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器 constructor.setAccessible(true); A a3 = null; a3 = constructor.newInstance("学习Java的小姐姐",1); System.out.println("a1的hash:" + a1.hashCode() + ",a2的hash:" + a2.hashCode() + ",a3的hash:" + a3.hashCode()); System.out.println("经过反射攻击单例模式状况下,实例化两个实例是否相同:" + (a1 == a3)); } }复制代码
运行结果以下:
咱们发现报错信息的位置已经换了,如今是已经有构造方法,而是在newInstance方法的时候报错了,咱们跟下源码发现,人家已经明确写明了若是是枚举类型,直接抛出异常,代码以下,因此是没法使用反射来操做枚举类型的数据的。
public class test { public static void main(String[] args) throws Exception { A s = A.a; //写 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("学习Java的小姐姐")); oos.writeObject(s); oos.flush(); oos.close(); //读 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("学习Java的小姐姐")); A s1 = (A)ois.readObject(); ois.close(); System.out.println(s+"\n"+s1); System.out.println("序列化先后两个是否同一个:"+(s==s1)); } }复制代码
运行结果;
避免了反射带来的对象不一致问题和反序列问题,简单来讲,就是简单高效没问题。
看到这里的都是真爱的,在这里先谢谢各位大佬啦。
单例模式是最简单的一种设计模式,主要包括八种形式,分别是饿汉式静态变量,饿汉式静态代码块,懒汉式线程不安全,懒汉式线程安全,懒汉式线程不安全(没啥意义),懒汉式双重否认线程安全,内部静态类,枚举类型。
这几种最优的是枚举类型和内部静态类,其次是懒汉式双重否认,剩下的都差很少啦。
若是有说的不对的地方,还请各位指正,我好继续学习去。
小姐姐陪你学习,陪你走心。