文章目录
23种设计模式基本介绍
经典设计模式大方向分为三大类,创建型模式、结构型模式、行为型模式。创建型模式里面包含5个,结构型模式包含7个,行为型模式包含11个。
创建型模式[Creational Pattern]
- 单例模式
- 抽象工厂模式
- 工厂方法模式
- 建造者模式
- 原型模式
结构型模式[Structural Pattern]
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
行为型模式[Behavioral Pattern]
- 策略模式
- 模板方法模式
- 观察者模式
- 迭代子模式
- 责任链模式
- 命令模式
- 备忘录模式
- 状态模式
- 访问者模式
- 中介者模式
- 解释器模式
设计模式三大类是按照特性什么划分的?
**创建型模式:**主要用于处理对象的创建,实例化对象。但是,这可能会限制在系统内创建对象的类型或数目。
**结构型模式:**处理类或对象间的组合。它将以不同的方式影响着程序,允许在补充写代码或自定义代码的情况下创建系统,而且具有重复使用性和应用性能。
**行为型模式:**描述类或对象怎样进行交互和职责分配。影响系统的状态、行为流,简化、优化并且提高应用程序的可维护性。
如何合理的选择该用什么样的设计模式?
单例模式(Singleton Pattern): 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
工厂方法模式(Factory Method Pattern): 定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延伸到其子类。
抽象工厂模式(Abstract Factory Pattern): 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
建造者模式(Builder Pattern): 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
原型模式(Prototype Pattern): 用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
适配器模式(Adapter Pattern): 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
桥接模式(Bridge Pattern): 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
组合模式(Composite Pattern): 将对象组合成树形结构以表示“部分-整体”的层次结构。它使得客户对单个对象和复合对象的使用具有一致性。
装饰模式(Decorator Pattern): 动态地给一个对象添加一些额外的职责。就扩展功能而言,它比生成子类方式更为灵活。
外观模式(Facade Pattern): 为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
享元模式(Flyweight Pattern): 运用共享技术有效地支持大量细粒度的对象。
代理模式(Proxy Pattern): 为其他对象提供一个代理以控制对这个对象的访问。
责任链模式(Chain of Responsibility Pattern): 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
命令模式(Command Pattern): 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
解释器模式(Interpreter Pattern): 给定一个语言,定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
迭代器模式(Iterator Pattern): 提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
中介者模式(Mediator Pattern): 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
备忘录模式(Memento Pattern): 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
观察者模式(Observer Pattern): 定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
状态模式(State Pattern): 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
策略模式(Strategy Pattern): 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。
模板方法模式(Template Method Pattern): 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
访问者模式(Visitor Pattern): 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
以上就是经典设计模式的23种的简单介绍了!!!
设计模式的六大原则:
原则 | 解释 |
---|---|
单一原则 (SRP) | 一个类只做一件事 |
开放-封闭原则(OCP) | 软件实体(类、模块、函数)可以拓展,但是不可修改 |
依赖倒转原则(DIP) | A.高层模块不应该依赖底层,两个都应该依赖抽。 B.抽象不应该依赖细节,细节依赖抽象 |
里氏代换原则(LSP) | 子类型必须能够替换掉它们的父类型 |
迪米特法则(LoD) | 如果两个类不必直接通信,那么这两个类不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可通过第三者发起这个调用 |
合成/聚合复用原则(CARP) | 尽量使用合成/聚合,尽量不要使用类继承 |
单例的几种实现方式
使用单例需要注意的关键点
1、将构造函数访问修饰符设置为private
2、通过一个静态方法或者枚举返回单例类对象
3、确保单例类的对象有且只有一个,特别是在多线程环境下
4、确保单例类对象在反序列化时不会重新构建对象
单例模式的几种写法
1.饿汉式(静态常量)
class Singleton{
//1.构造器私有化,外部不能new
private Singleton(){
}
//2.本类内部创建对象实例
private final static Singleton instance = new Singleton();
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return instance;
}
}
2、饿汉式(静态代码块)
class Singleton {
//1.构造器私有化,外部不能new
private Singleton() {
}
//2.本类内部创建对象实例
private static Singleton instance;
static { //在静态代码块中,创建单例对象
instance = new Singleton();
}
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
3、懒汉式(线程不安全)
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,当使用到方法时,才去创建instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4、懒汉式(线程安全,同步方法)
class Singleton {
private static Singleton instance;
private Singleton() {
}
//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
5、懒汉式(双重校验,线程安全,效率较高,推荐使用)
class Singleton {
private static volatile Singleton instance; //volatile保证线程间的可见性
private Singleton() {
}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题
//同时保证了效率,推荐使用
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
6、静态内部类完成,推荐使用
class Singleton {
private Singleton() {
}
//写一个静态内部类,该类中有一个静态属性Singleton
//在调用getInstance()方法时,静态内部类才会被装载,保证了懒加载;同时类加载是线程安全的
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
7、使用枚举,推荐使用
enum Singleton{
INSTANCE; //属性
public void doSomething(){
System.out.println("do something");
}
}
一、饿汉式
1、
缺点:会造成内存资源的浪费。
package com.java.se.demo.singleton;
/**
* 单例:饿汉式:1、在类初始化的时候,直接创建实例对象,不管你需不需要。(很饿,一上来就创建对象)
* (1)、构造器私有,不允许外部创建实例对象
* (2)、自行创建,并且用静态变量保存
* (3)、向外提供获取该对象实例的方式:
* (1)、直接暴露(public)
* (2)、用静态变量的get方法,这里用的是(1)
* (4)强调这是一个单例,可以用final关键字修饰
* (5)饿汉式不存在线程不安全问题
*/
public class SingleTon1 {
public static final SingleTon1 INSTANCE = new SingleTon1();
private SingleTon1(){
}
}
测试
package com.java.test;
import com.java.se.demo.singleton.SingleTon1;
/**
* 测试
*/
public class TestSingleTon1 {
public static void main(String[] args) {
SingleTon1 s = SingleTon1.INSTANCE;
System.out.println(s);
}
}
枚举
优点:
- 可以避免反射破坏单例模式
- 防止反序列化重新创建新的对象
2、
package com.java.se.demo.singleton
/**
* 单例:饿汉式:2、枚举类型(最简洁): 表示该类型的对象是有限的几个
* 我们可以限定为一个,就成为了单例
* 所有的构造器都私有了
*/
enum class SingleTon2 {
INSTANCE
}
测试
package com.java.test;
import com.java.se.demo.singleton.SingleTon2;
public class TestSingleTon2 {
public static void main(String[] args) {
SingleTon2 s = SingleTon2.INSTANCE;
System.out.println(s);
}
}
这种方式利用了枚举类的特性,代码极其简洁,实际的单例对象就是Singleton7.INSTANCE,而且可以避免反射破坏单例模式,推荐!
静态代码块
3、
package com.java.se.demo.singleton;
import java.io.IOException;
import java.util.Properties;
/**
* 单例:饿汉式:3、静态代码块(复杂的实例->>需要加载一堆配置文件)
*/
public class SingleTon3 {
public static final SingleTon3 INSTANCE;
private String info;
static {
try {
Properties pro = new Properties();
pro.load(SingleTon3.class.getClassLoader().getResourceAsStream("single.properties"));//这里的single.properties配置文件必须要放在src目录下
INSTANCE = new SingleTon3(pro.getProperty("info"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private SingleTon3(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "SingleTon3{" +
"info='" + info + '\'' +
'}';
}
}
测试
package com.java.test;
import com.java.se.demo.singleton.SingleTon3;
public class TestSingleTon3 {
public static void main(String[] args) {
SingleTon3 s = SingleTon3.INSTANCE;
System.out.println(s);
}
}
二、懒汉式
2.1、单线程下(线程不安全)
这种方式在单线程情况下没有问题,属于懒汉式,但是多线程的时候会导致可能创建多个对象,也就破坏了单例模式,不推荐!
package com.java.se.demo.singleton;
/**
* 单例:懒汉式:1、单线程下(线程不安全)
* (1)、构造器私有,不允许外部创例建实对象
* (2)、中自行创建,并且用静态变量保,在静态方法中new
* (3)、向外提供获取该对象实例的方式:
* (1)、直接暴露(public)
* (2)、用静态变量的get方法,这里用的是(2)
* (4)强调这是一个单例,可以用final关键字修饰
*/
public class SingleTon4 {
private static SingleTon4 instance;//这里不要用final修饰
private SingleTon4() {
}
public static SingleTon4 getInstance() {
try {
Thread.sleep(1000);//为了观察明显,让其睡1000ms
} catch (InterruptedException e) {
e.printStackTrace();
}return instance;
}
}
测试
package com.java.test;
import com.java.se.demo.singleton.SingleTon4;
import java.util.concurrent.*;
public class TestSingleTon4 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//单线程情况下,线程安全
/*SingleTon4 s1 = SingleTon4.getInstance();
SingleTon4 s2 = SingleTon4.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);*/
//多线程情况下:存在线程安全问题
Callable<SingleTon4> c = new Callable<SingleTon4>() {
@Override
public SingleTon4 call() throws Exception {
return SingleTon4.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<SingleTon4> f1 = es.submit(c);
Future<SingleTon4> f2 = es.submit(c);
SingleTon4 s1 = f1.get();
SingleTon4 s2 = f2.get();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);
}
}
2.2、单线程下加锁(线程安全)
这种方式在单线程情况下没有问题,属于懒汉式,但是多线程的时候会导致可能创建多个对象,也就破坏了单例模式,不推荐!
/**
* 懒汉式单例模式(线程安全)
*/
public class Singleton3 {
private static Singleton3 uniqueInstance;
private Singleton3() {
}
public static synchronized Singleton3 getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton3();
}
return uniqueInstance;
}
}
这种方式属于线程安全的懒汉式单例模式。懒汉式的含义就是只有在需要生成单例对象的时候才会被调用生成该对象,灵活性更大。但是该模式可能会造成系统资源的浪费,原因是synchronized加在方法体上,每调用一次方法都会进行加锁,正常是在第一次生成单例对象的时候才需要加锁,后续调用再进行加锁是对系统资源的浪费。不推荐!
2.3、多线程下加DCL双端检锁机制(线程安全)
package com.java.se.demo.singleton;
/**
* 单例:懒汉式:2、多线程下加DCL双端检锁机制
* (1)、构造器私有,不允许外部创建实例对象
* (2)、自行创建,并且用静态变量保存
* (3)、向外提供获取该对象实例的方式:
* (1)、直接暴露(public)
* (2)、用静态变量的get方法
* (4)强调这是一个单例,可以用final关键字修饰
* (5)饿汉式不存在线程不安全问题
*/
public class SingleTon5 {
private static volatile SingleTon5 instance=null;//禁止指令重排
private SingleTon5() {
}
public static SingleTon5 getInstance() {
//多线程下加DCL双端检锁机制(存在指令重排)
if (instance == null) {
synchronized (SingleTon5.class) {
if (instance == null) {
try {
Thread.sleep(1000);//为了观察明显,让其睡1000 ms
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingleTon5();
}
}
}
return instance;
}
}
测试
package com.java.test;
import com.java.se.demo.singleton.SingleTon4;
import java.util.concurrent.*;
public class TestSingleTon4 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//单线程情况下,线程安全
/*SingleTon4 s1 = SingleTon4.getInstance();
SingleTon4 s2 = SingleTon4.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);*/
//多线程情况下:存在线程安全问题
Callable<SingleTon4> c = new Callable<SingleTon4>() {
@Override
public SingleTon4 call() throws Exception {
return SingleTon4.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<SingleTon4> f1 = es.submit(c);
Future<SingleTon4> f2 = es.submit(c);
SingleTon4 s1 = f1.get();
SingleTon4 s2 = f2.get();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);
}
}
这种方式是对前一种实现方式的改进,既保证了线程安全,也避免了前一种方式的坑,但是需要注意的是单例对象的引用需要用volatile关键字修饰,防止指令重排序,避免产生未初始化完全的对象。推荐!
三、静态内部类
- 避免造成内存的浪费
- 防止反射破解单例模式
- 防止反序列化重新创建新的对象
package com.java.se.demo.singleton;
/**
* 单例:懒汉式:3、静态内部类(最简洁)
* 1、内部类被加载和初始化时,才去创建INSTANCE实例对象
* 2、静态内部类不会随着外部类的加载和初始化而初始化,他是要单独去加载和初始化的
* 3、因为是在内部类加载和初始化时才创建的,所以是线程安全的
*/
public class SingleTon6 {
private SingleTon6() {
}
private static class Inner{
private static final SingleTon6 INSTANCE = new SingleTon6();
}
public static SingleTon6 getInstance() {
return Inner.INSTANCE;
}
}
这种方式代码结构清晰,推荐!
通过反射破坏单例模式
此处用静态内部类实现单例模式的代码做演示:
/**
* 静态内部类实现单例模式
*/
public class Singleton6 {
private Singleton6() {
}
public void sayHello(){
System.out.println("Hello World");
}
private static class SingletonIns {
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return SingletonIns.INSTANCE;
}
public static void main(String[] args)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton6 s1 = Singleton6.getInstance();
System.out.println(s1);
s1.sayHello();
/**
* 通过反射破坏单例模式
*/
//反射获得单例类的构造函数
Constructor<Singleton6> constructor = Singleton6.class.getDeclaredConstructor();
//指示反射的对象在使用时取消Java语言访问检查,绕过private Singleton6()
constructor.setAccessible(true);
Singleton6 s2 = constructor.newInstance();
System.out.println(s2);
s2.sayHello();
}
}
运行结果如下:
singleton.Singleton6@6e0be858
Hello World
singleton.Singleton6@61bbe9ba
Hello World
Process finished with exit code 0
可以看见通过反射,在已经生成了一个单例对象的情况下,又生成了一个完全不同的对象,破坏了单例模式。
而利用枚举则可以避免被反射破坏:
/**
* 枚举实现单例模式
*/
enum Singleton7 {
INSTANCE;
public void sayHello() {
System.out.println("Hello World");
}
public static void main(String[] args)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton7.INSTANCE.sayHello();
/**
* 尝试使用反射来创建单例类对象
* 在通过反射创建对象时,会检查该类是否时ENUM修饰,如果是则抛出异常,反射失败
*/
Constructor<Singleton7> constructor = Singleton7.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton7 singleton7 = constructor.newInstance();
singleton7.sayHello();
}
}
运行结果抛出了异常:
Hello World
Exception in thread "main" java.lang.NoSuchMethodException: singleton.Singleton7.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at singleton.Singleton7.main(Singleton7.java:23)
Process finished with exit code 1
单例模式的优点
- 由于单例模式在内存中只有一个实例,减少内存开支,特别是一个对象需要频繁地创建销毁时,而且创建或销毁时性能又无法优化,单例模式就非常明显了
- 由于单例模式只生成一个实例,所以,减少系统的性能开销,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
- 单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理
单例模式的缺点
- 不适用于变化的对象
- 由于单例模式没有抽象层,所以扩展困难
- 单例类的职责过重,在一定程度上违背了“单一职责原则”
单一职责原则:一个类,应该只有一个职责
单例模式介绍
单例模式(Singleton Pattern)是 Java 中最简单、最常用的设计模式之一。单例模式提供了一种在多线程环境下保证实例唯一性的解决方案。即:对象一经初始化,后需可以直接访问,不需要再次实例化该类的对象。属于设计模式三大类中的创建型模式。在Java中,一般常用在工具类的实现、对象创建开销较大的情景下。
单例模式具有典型的三个特点:
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。(自我实例化)
单例类必须给所有其他对象提供这一实例。(提供全局访问点)
单例模式虽然比较简单,但实现方式却多种多样,本篇列举了7种实现方式,并从线程安全、高性能、懒加载三个维度对其进行评估,比较其优劣。
一、饿汉式
饿汉模式,顾名思义,就是采用静态初始化的方式在类被加载时就将自身实例化,所以被形象地称之为饿汉式单例模式。
// final不允许被继承
public final class HungrySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
//第一步:私有化构造器,不允许外部new操作
private HungrySingleton(){
}
//第二步:在类初始化时立即实例化该对象,从而保证线程安全
private static HungrySingleton instance = new HungrySingleton();
//第三步:提供一个获取全局访问点的方法
public static HungrySingleton getInstance() {
return instance;
}
//other methods
}
饿汉式把instance作为"类变量"并且直接初始化,当主动使用Singleton 类时会完成instance的创建,包括其中的实例变量都会得到初始化,比如上例中的data数组将被创建并占用1K的空间。如果instance被ClassLoader加载后很长一段时间才被使用,那就意味着instance实例所开辟的堆内存会驻留更久的时间,如果一个类的成员占用的内存资源较多,那么采用饿汉式就有些不妥。
总结
线程安全。
getInstance方法的性能比较高。
无法进行懒加载。
注意:上面代码中有使用final关键字,强制该类不允许被继承。若父类所有的构造器都是私有的(private修饰),那么JVM规定该父类不允许被继承,因为子类的构造器都必须显示或隐式调用父类的构造器。此时可以不使用final关键字声明。
JDK饿汉式单例举例:
二、懒汉式
懒汉式就是在第一次使用类实例的时候再去创建,和饿汉式在类初始化时就提前创建实例不同,所以就被称为懒汉式单例模式。
//final不允许被继承
public final class LazySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
// 未实例化的类变量
private static LazySingleton instance = null;
// 私有化构造器
private LazySingleton() {
}
// 运行时加载对象
public static LazySingleton getInstance() {
//判断是否已经初始化过(没有同步机制控制,多线程不安全)
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//other methods
}
总结
线程不安全。instance是共享资源,当多个线程对其访问时需要保证共享资源的同步性,因此线程不安全,无法保证单例的唯一性。(更加具体的原因不摊开说明了)
性能和懒加载,就不讨论了,因为这种方法本身就不正确。
三、懒汉式+synchronized同步
上述的懒汉式保证了实例的懒加载,但无法保证实例的唯一性,需要增加对共享资源instance的同步访问机制,可以采用synchronized关键字实现。
//final不允许被继承
public final class LazySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
// 未实例化的类变量
private static LazySingleton instance = null;
// 私有化构造器
private LazySingleton() {
}
// 运行时加载对象(增加了synchronized,每次只能有一个线程能够进入)
public static synchronized LazySingleton getInstance() {
//判断是否已经初始化过(没有同步机制控制,多线程不安全)
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//other methods
}
总结
线程安全,能够保证实例的唯一性;
getInstance方法采用synchronized关键字所以性能较低
懒加载。
四、Double-Check式(注意有坑)
Double-Check的方式是一种更加高效的数据同步策略,只有首次初始化时才加锁,之后多个线程获取实例时都无需同步控制。
// final不允许被继承
public final class LazySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
// 未实例化的类变量
private static LazySingleton instance = null;
// 私有化构造器
private LazySingleton() {
}
public static LazySingleton getSingleton() {
// 若instance不为null,则不用获取锁,提升了效率
if (instance == null) {
//同步加锁是为了线程安全,确保只有一个线程创建实例
synchronized (LazySingleton.class) {
//再次判空是为了保证单例对象的唯一性,只有没被创建才去创建
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
//other methods
}
当两个线程同时发现instance == null成立时,只有一个线程有资格进入synchronized同步代码块完成instance的实例化,随后的线程进入synchronized同步代码块后发现instance == null不成立则无需再次实例化,以后对getSingleton方法的访问也不需要执行synchronized同步代码块,大大提升了性能。满足懒加载、线程安全、高性能这三个标准,一切看起来很完美。但这种方式在多线程环境下可能会导致空指针异常,原因如下。
首先,我们要理解new LazySingleton()做了什么,详细的介绍请查看《java new一个对象的过程中发生了什么》。本篇简单介绍new一个对象需要的4个步骤,如下:
看class对象是否加载,如果没有就先加载class对象。(加载)
为类的静态变量分配内存空间并为其初始化默认值(连接阶段),为静态变量赋予正确的初始值(初始化阶段)。
调用构造函数。(单例比较复杂时,有很多的成员变量需要初始化)
返回地址给引用。
然后,cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配(或是只实例化了部分成员变量),就被使用了,导致空指针。下面举例:
线程A执行到new LazySingleton(),开始初始化实例对象,由于存在指令重排序,先执行步骤4,先把引用instance赋值了,此时还没有执行构造函数(或执行还未完成,只实例化了部分成员变量),这时CPU时间片耗尽,切换到线程B执行,线程B调用new LazySingleton()方法,发现instance == null不成立,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量,报空指针错误。
五、Volatile + Double-Check式(最终版)
Double-Check是一种巧妙的设计,但由于JVM在运行new LazySingleton()时可能对指令重排序,导致空指针异常。而volatile关键字可以防止重排序,因此还需要对Double-Check方式稍加修改。关于volatile关键字的用法,可参考另一篇博文《深入理解volatile关键字》。
// 加volatile 修饰
private static volatile LazySingleton instance = null;
至此,就有了一个线程安全、高性能、懒加载版本的双重检查加锁式单例模式。但这种写法对于初学者很棘手,一下子很难理解。
六、Holder式
直接上代码,然后给出说明。
// 不允许被继承
public final class HolderSingleton {
// 类的成员变量
private byte[] data = new byte[1024];
// 私有化构造器
private HolderSingleton() {
}
// 静态内部类 Holder中持有实例instance
private static class Holder{
private static HolderSingleton instance = new HolderSingleton();
}
// 调用getSingleton,返回Holder的instance类属性
public static HolderSingleton getSingleton() {
return Holder.instance;
}
//other methods
}
这种方式利用了类加载的特点。HolderSingleton 类中没有持有静态的instance实例,而是放在静态内部类Holder中,该方式仍然需要私有化构造器。当Holder被主动引用时(懒加载)会创建HolderSingleton 的实例,JVM保证实例的唯一性,性能高。是目前广泛采用的一种单例设计。
七、枚举式
八、防止反射/反序列化攻击单例类
上面的单例实现方式中,除了枚举类型外,其他的实现方式是可以被JAVA的反射机制攻击的。享有特权的客户端可以借助AccessibleObject.setAccessible方法通过反射机制调用私有构造器,如果需要抵御这种攻击,可以修改构造器,让它的被要求创建第二个实例的时候抛出一个异常。具体方式请参考 《如何防止JAVA反射对单例类的攻击?》 《设计模式——单例模式》