什么情况才会使用设计模式
设计模式是解决问题的方案,学习现有的设计模式可以做到经验复用
设计模式按照用途,可分为三类:创建型、结构型、行为型
创建型
单例模式
单例模式确保一个类只有一个实例,通过一个私有的构造函数、一个私有的静态属性、一个公有的静态函数实现。
私有构造函数保障了用户不能通过构造函数创建对象实例,只能通过公有的静态方法来返回唯一的私有静态变量
懒汉式
在以下实现中,私有静态属性singleton被延迟实例化,这样的好处是,当没有使用到该类时,就不会实例化,从而节约资源。
但是,这种实现是线程不安全的,如果多个线程进入了if (singleton == null)的空判断,并且此时singleton = null时,那么会有多个线程执行**singleton = new Singleton()**创建多个实例,导致不是单实例。
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
饿汉式
饿汉式与懒汉式的区别是,在进行类加载时就已经实例化了。在线程调用**getInstance()**方法时,由于已经创建了对象实例,所以多个线程获取的实例都是同一个,是线程安全的。但是在类加载时就创建对象,会失去懒加载的好处。
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return instance;
}
}
懒汉式-线程安全
既然懒汉式是线程不安全的,所以可以通过对线程不安全的方法getInstance()加锁。但是这种方法会带来性能问题,当多个线程都试图访问getInstance方法时,只有一个线程能够获得锁并进行访问,而其他线程都会被阻塞,即使instance已经实例化了,直到拥有锁的线程释放锁,不推荐使用该方法
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2() {}
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
双重校验锁
我们想要的是,在类未进行实例化时访问getInstance方法,需要对该方法加锁,如果已经实例化了,那么后续的线程应该都可以进行访问,无需加锁阻塞线程。
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
面试题
- 能不能只使用1个if,而不是两个if?
如果只使用了一个if,当多线程进入了if (instance == null)空值判断,并且此时instance = null,多个线程都能进入到if语句块内,虽然if语句块内有加锁操作,每次只能让一个线程进行访问,但是多个线程都会执行对象实例化操作,创建了多个对象实例;而使用双重校验,第一个if用于对象实例化后不需要再加锁,第二个if用于进入了锁代码块后,只有当前私有静态属性instance为空才能进行实例化,从而避免了多次实例化操作 - 为什么需要对instance 添加volatile关键字修饰?
volatile保证了指令的禁止重排,而执行**instance = new Singleton3()需要执行三个操作:
a.为instance分配内存空间
b.instance进行初始化操作
c.为instance分配的内存地址
JVM具有指令重排的特性,执行顺序可能会变成a->c->b,在单线程语意下不会出现问题,但是在多线程环境下,线程a的执行顺序可能是a->c->b,此时线程b调用getInstance()**方法发现instance不为空,直接返回instance,但是线程a还没有执行初始化,此时返回的是一个没有初始化的对象。使用volatile禁止指令重排,保证在多线程语意下也能正常执行
静态内部类实现*
在进行类加载时,静态内部类SingletonHolder还没有被加载到内存,只有访问**getInstance()**方法时才会进行加载,而且这种方式只会创建一次对象实例,所以这种方式实现懒加载的同时实现了线程安全
public class Singleton4 {
private Singleton4() {}
private static class SingletonHolder {
private static final Singleton4 INSTANCE = new Singleton4();
}
public static Singleton4 getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举实现?
public enum Singleton5 {
INSTANCE;
}
通过枚举实现,可以避免反射攻击,在其他实现中,通过setAccessiable()方法可将私有函数访问级别改成public的,此时单实例模式就会失效。枚举实现由JVM保证只会实例化一次,因此不会出现反射攻击的情况。
在实现序列化和反序列化后,也不会出现多个实例对象。而其他的实现方法,需要额外添加transient关键字,并且要手动实现序列化和反序列化方法
思考
- 枚举实现如何保证线程安全
- 枚举如何防止被反射破坏
具体可参考文章:单例模式竟然有这么多种写法
工厂模式
工厂模式可分为简单工厂、工厂方法、抽象工厂方法
简单工厂
简单工厂提供了一个创建对象的接口,不会向客户暴露内部细节
简单工厂把实例化操作单独放到一个类中,这个类就成为了简单工厂类,让简单工厂类来决定用哪个子类进行实例化。
这样做让客户类和具体子类实现解耦,客户类不再需要知道有哪些子类和子类的实现细节。一旦子类发生变化,如增加一个子类,只需在简单工厂类进行修改即可。
interface Product {
}
class ConcreteProduct implements Product {
}
class ConcreteProduct1 implements Product {
}
class ConcreteProduct2 implements Product {
}
public class SimpleFactory {
public Product createProduct(int type) {
if (type == 1) {
return new ConcreteProduct();
} else if (type == 2) {
return new ConcreteProduct1();
}
return new ConcreteProduct2();
}
}
客户端可直接通过简单工厂类来创建对象
class Client {
public static void main(String[] args) {
SimpleFactory factory = new SimpleFactory();
Product product = factory.createProduct(1);
}
}
工厂方法
定义了一个创建对象的接口,与简单工厂方法不同,由子类去决定实例化哪个对象,把实例化操作推迟到子类。
Factory中有一个doSomething()方法,这个方法需要用到一个产品对象,这个产品对象由一个**factoryMethod()**方法创建,该方法是抽象的,需要子类去实现。
public abstract class Factory {
abstract public Product factoryMethod();
public void doSomething() {
Product product = factoryMethod();
}
}
class ConcreteFactory extends Factory {
@Override
public Product factoryMethod() {
return new ConcreteProduct();
}
}
class ConcreteFactory1 extends Factory {
@Override
public Product factoryMethod() {
return new ConcreteProduct1();
}
}
抽象工厂
提供一个接口,用于创建相关的对象家族。
工厂模式用于创建一个对象,而抽象工厂用于创建对象的家族,创建的多个对象之间是有相关性的。例如创建一台电脑,电脑是通过键盘、鼠标、显示器、主机组成的。而键盘,也是由多家厂商的键盘。但是一般情况下,有些产品是有相关性的,比如苹果的设备和安卓的设备不一定都能进行组合。
public abstract class AbstractFactory {
abstract AbstractProductA createProductA();
abstract AbstractProductB createProductB();
}
class ConcreteFactory11 extends AbstractFactory {
@Override
AbstractProductA createProductA() {
return new ProductA1();
}
@Override
AbstractProductB createProductB() {
return new ProductB1();
}
}
class ConcreteFactory22 extends AbstractFactory {
@Override
AbstractProductA createProductA() {
return new ProductA2();
}
@Override
AbstractProductB createProductB() {
return new ProductB2();
}
}
class client {
public static void main(String[] args) {
AbstractFactory abstractFactory = new ConcreteFactory11();
AbstractProductA productA = abstractFactory.createProductA();
AbstractProductB productB = abstractFactory.createProductB();
}
}
class AbstractProductA {
// 相当于键盘产品
}
class AbstractProductB {
// 相当于显示器产品
}
class ProductA1 extends AbstractProductA {
// 相当于安卓的键盘
}
class ProductA2 extends AbstractProductA {
// 相当于苹果的键盘
}
class ProductB1 extends AbstractProductB {
// 相当于安卓的显示器
}
class ProductB2 extends AbstractProductB {
// 相当于苹果的显示器
}
生成器模式/建造者模式
原型模式
总结
- 单例模式:保证一个类只有一个实例,并提供一个它的全局访问点
- 抽象工厂:提供一个创建一系列相关或相互依赖对象的接口
- 工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使类的实例化延迟到子类
- 建造模式:
- 原型模式: