软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
1.创建型模式
用于描述“怎样创建对象”,它的主要特点是“==将对象的创建与使用分离==”。GoF(四人组)书中提供了==单例、原型、工厂方法、抽象工厂、建造者==等 5 种创建型模式。
2.结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了==代理、适配器、桥接、装饰、外观、享元、组合==等 7 种结构型模式。
3.行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了==模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器==等 11 种行为型模式。
1.单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
2.单例模式的创建方式
第一种:饿汉式--->不管是否使用该对象,该类会直接创建
第二种:懒汉式--->当使用到该对象再创建
//饿汉式 创建一个单例类
public class Singleton {
//构造方法私有化
private Singleton(){}
//创建一个本类的对象---static:随着类的加载而加载到内存而且只会加载一次
private static Singleton singleton = new Singleton();
//静态方法,返回本类创建的类对象
public static Singleton getInstance(){
return singleton;
}
}
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
//懒汉式双重校验
public class Singleton03 {
private Singleton03(){}
private static Singleton03 instance ;
public static Singleton03 getInstance(){
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if (instance==null){
synchronized (Singleton03.class){
//抢到锁之后再次判断是否为null
if (instance==null){
instance = new Singleton03();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
1.添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。还有,这里的private static volatile Singleton singleton = null;中的volatile也必不可少,volatile关键字可以防止jvm指令重排优化,
在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
1.第一步是给 singleton 分配内存空间;
2.第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
3.第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错。
public class Test01 {
public static void main(String[] args) {
//饿汉式
/*Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);*/
//懒汉式
/*Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
Singleton01 s3 = Singleton01.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);*/
//双重校验
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0;i < 5;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Singleton03.getInstance());
}
});
}
}
}
在 JDK1.2 之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的Java内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为 volatile,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
优点:单例类只有一个实例,节省了内存资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;单例模式可以在系统设置全局的访问点,优化和共享数据,例如前面说的Web应用的页面计数器就可以用单例模式实现计数值的保存。
缺点:单例模式一般没有接口,扩展的话除了修改代码基本上没有其他途径。