文章目录
前言
本篇是关于设计模式中单例模式(8种,包含线程安全,非安全的实现)、工厂模式(3种)、以及原型模式(深拷贝、浅拷贝)的笔记。
一、单例模式
单例模式
的核心目的是确保某个类只有一个实例,并且提供一个全局访问点来获取该实例。这种模式通常用于需要全局共享资源或者全局配置的场景,通常可用在日志管理器(只有一个日志实例用于输出日志)、线程池(避免重复创建)等全局资源只需要初始化一次的场景。
如果需要实现单例模式,通常需要满足以下三大要素:
- 私有化构造函数:防止外部直接创建实例。
- 静态变量:保存唯一的实例。
- 公共的静态方法:提供对外的访问方式,返回该唯一实例。
1.1、饿汉式静态常量单例
最常见的一种饿汉式单例,Singleton1
的实例是在外部调用Singleton1
的getInstance
方法时创建的,并且Java 的类加载懒加载
的,也就是说只有当类第一次被引用时,才会加载这个类。在加载过程中,JVM 会保证静态变量的初始化是线程安全的。
JVM是如何保证静态变量初始化的线程安全?
类加载过程是
串行的,每个类在被加载时会有一个单独的类加载过程,加载过程中的所有操作是线程安全的。JVM 会确保对类的初始化只有一个线程可以执行。其他线程会被阻塞,直到类初始化完成。对于静态变量,JVM 在类的初始化过程中会执行
双检模式
,当线程第一次访问类时,如果该类尚未初始化,JVM 会进行初始化,并且只有一个线程会执行这个初始化过程。其他线程在初始化过程中会被阻塞,直到第一个线程完成初始化,类初始化过程保证只会被执行一次,避免了多个线程同时初始化实例的问题。
在案例中,是在同一个线程中获取了两次INSTANCE实例,为何都是同一个?因为静态变量,是属于类而不是属于某个方法的,静态变量是类的所有实例共享的,当
Singleton1
类被加载并初始化时,JVM 会创建INSTANCE
变量并赋值。这一过程只会发生一次。
java
public class HungryMan1 {
public static void main(String[] args) {
Singleton1 s1 = Singleton1.getInstance();
Singleton1 s2 = Singleton1.getInstance();
System.out.println(s1 == s2);
}
}
/**
* 类加载时就创建单例对象
* 由JVM保证线程安全性
*/
class Singleton1{
private Singleton1(){
}
private final static Singleton1 INSTANCE = new Singleton1();
public static Singleton1 getInstance(){
return INSTANCE;
}
}
1.2、饿汉式静态代码块单例
相比较于第一种实现,区别在于本实现是在静态代码块中完成单例对象初始化的。当调用Singleton2.getInstance()
,这触发 Singleton2
类的加载,类加载过程中,JVM 会初始化静态成员变量和静态代码块。
java
public class HungryMan2 {
public static void main(String[] args) {
Singleton2 s1 = Singleton2.getInstance();
Singleton2 s2 = Singleton2.getInstance();
System.out.println(s1 == s2);
}
}
class Singleton2{
private Singleton2(){
}
private final static Singleton2 INSTANCE;
/**
* 在静态代码块中完成初始化
*/
static {
INSTANCE = new Singleton2();
}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
1.3、懒汉式单例(线程不安全)
该种单例的设计思想是,在调用getInstance()
时才会主动去创建单例实例。但是下面的实现是存在线程安全问题的,如果两个线程同时到达了if块,都判断为空,就会创建两个不同的实例。
java
public class LazyMan1 {
public static void main(String[] args) throws InterruptedException {
Singletion3 s1 = Singletion3.getInstance();
Singletion3 s2 = Singletion3.getInstance();
System.out.println(s1 == s2);
}
}
class Singletion3{
private Singletion3(){
}
private static Singletion3 instance;
/**
* 多线程下存在并发问题
* @return
*/
public static Singletion3 getInstance(){
if (instance == null){
instance = new Singletion3();
}
return instance;
}
}
1.4、懒汉式单例(线程安全,同步代码块)
java
public class LazyMan2 {
public static void main(String[] args) {
Singletion4 s1 = Singletion4.getInstance();
Singletion4 s2 = Singletion4.getInstance();
System.out.println(s1 == s2);
}
}
class Singletion4{
private Singletion4(){
}
private static Singletion4 instance;
/**
* 解决线程安全问题,但是synchronized是重量级锁,效率低
* @return
*/
public static synchronized Singletion4 getInstance(){
if (instance == null){
instance = new Singletion4();
}
return instance;
}
}
1.5、懒汉式单例(线程不安全,同步代码块)
1.4的案例,使用重量级锁保证线程安全,弊端在于锁的粒度过大。如果缩小锁的范围?本案例的写法依旧会存在线程安全问题:
java
public class LazyMan3 {
public static void main(String[] args) {
Singletion5 s1 = Singletion5.getInstance();
Singletion5 s2 = Singletion5.getInstance();
System.out.println(s1 == s2);
}
}
class Singletion5 {
private Singletion5() {
}
private static Singletion5 instance;
/**
* 降低锁的粒度,依旧会存在线程安全问题
* 比如AB两个线程在IF处判断,都为空,都进入了IF块
* 虽然只有一个线程能争抢到锁,但是在释放锁之后,另一个线程也能再次进入同步代码块创建一个新的对象
* @return
*/
public static Singletion5 getInstance() {
if (instance == null) {
synchronized (Singletion5.class) {
instance = new Singletion5();
}
}
return instance;
}
}
1.6、懒汉式单例(线程安全,双检锁模式)
JUC并发编程,java内存模型,volatile关键字,双检锁单例
java
public class LazyMan4 {
public static void main(String[] args) {
}
}
class Singletion6 {
private Singletion6() {
}
/**
* 这里的volatile 一定要加 第一是避免指令重排序问题,第二是将对于instance的更改立刻同步到主存,防止缓存问题=-
*
*/
private static volatile Singletion6 instance;
/**
* 降低锁的粒度,并且进行双重检查
* @return
*/
public static Singletion6 getInstance() {
if (instance == null) {
synchronized (Singletion6.class) {
if (instance == null) {
instance = new Singletion6();
}
}
}
return instance;
}
}
1.7、静态内部类单例
保证线程安全的方式,和饿汉式的类似。
java
public class StaticInner {
public static void main(String[] args) {
Singleton7 s1 = Singleton7.getInstance();
Singleton7 s2 = Singleton7.getInstance();
System.out.println(s1 == s2);
}
}
class Singleton7{
private Singleton7(){
}
/**
* 使用静态内部类的方式,静态内部类会在其中方法/变量被调用时初始化
*/
public static class inner{
private static final Singleton7 INSTANCE = new Singleton7();
}
/**
* 外部调用Singleton7的getInstance静态方法时,初始化inner内部类
* JVM在加载类时是线程安全的,通过静态内部类只加载一次,保证只创建一次外部类的实例
* @return
*/
public static Singleton7 getInstance(){
return inner.INSTANCE;
}
}
1.8、枚举单例
最后一种是枚举单例,也是推荐使用的一种方式。上面所有的单例,即使是线程安全的,也会有可能因为使用反射而被破坏。
枚举单例为什么能保证线程安全?
在类加载时,JVM 会创建枚举实例并将其保存在内存中。在整个应用生命周期内,枚举的实例是唯一的,JVM 会在类加载时保证它的线程安全和单例性。
当 Singleton.INSTANCE 被访问时,枚举实例已经由 JVM 在类加载时创建好并且只会创建一次。
java
public class EnumSingleton {
public static void main(String[] args) {
Singleton8 s1 = Singleton8.SINGLETON;
Singleton8 s2 = Singleton8.SINGLETON;
System.out.println(s1 == s2);
}
}
/**
* 枚举单例 没有线程安全问题,也不会导致通过暴力反射破坏单例
* 前面的方式,虽然将构造私有化,但是都是可以通过反射破解的
*/
enum Singleton8{
SINGLETON;
}
二、工厂模式
工厂模式的核心思想在于,将对象的实例化过程封装起来,使得代码不需要直接调用构造方法来创建对象,而是通过一个工厂方法来获取实例客户端代码不需要关心如何创建对象,只需要关心如何使用对象。(七大原则中的迪米特原则,依赖倒置原则)
即,将对象的创建过程和使用过程分离,客户端可以获取到对象,而不需要知道对象的具体创建细节。
2.1、简单工厂模式
假设现在要模拟一个制作披萨的过程,披萨的种类有Cheess
和Greek
两种,制作的过程有prepare准备
,bake烘烤
,cut切割
,box打包
。由于不同的披萨准备原材料的方式是不一样的,可以这样设计:
java
public abstract class Pizza {
private String name;
public void setName(String name) {
this.name = name;
}
/**
* 每种披萨的准备过程是不一样的,留给子类去实现
*/
public void prepare() {}
public void bake() {
System.out.println("烘烤 " + name);
}
public void cut() {
System.out.println("切割 " + name);
}
public void box() {
System.out.println("打包 " + name);
}
}
java
public class GreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println("制作希腊披萨 准备材料");
}
}
java
public class CheesePizza extends Pizza {
@Override
public void prepare() {
System.out.println("制作奶酪披萨 准备材料");
}
}
再用一个类模拟订购披萨的过程:
java
public class OrderPizza {
public OrderPizza() {
Pizza pizza = null;
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("***********请输入需要制作的pizza***********");
String next = sc.next();
//调用工厂的方法,创建具体的实例
PizzaFactory pizzaFactory = new PizzaFactory();
pizza = pizzaFactory.createPizza(next);
if (pizza == null){
break;
}
pizza.bake();
pizza.cut();
pizza.box();
}
sc.close();
}
}
将制作具体披萨的代码放置到了工厂类中,这样有什么好处?如果有多个订购披萨的类,并且我现在要加一个披萨的种类,如果制作具体披萨的代码还是放置在每个订购披萨的类中,那么所有的类都需要进行修改,扩展性
很差。
java
/**
* 创建Pizza的工厂类
*/
public class PizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("Cheese")){
pizza = new CheesePizza();
pizza.setName("奶酪披萨");
}else if (type.equals("Greek")){
pizza = new GreekPizza();
pizza.setName("希腊披萨");
}else {
return null;
}
return pizza;
}
}
java
/**
* Pizza店
*/
public class PizzaStore {
public static void main(String[] args) {
new OrderPizza();
}
}
2.2、工厂方法模式
工厂方法模式
是对简单工厂模式
的一种改进。假设需求发生变更,除了披萨有不同的种类,还有不同地区供应披萨,比如北京的Cheess披萨,伦敦的Greek披萨...等,简单工厂模式
不适合这种复杂的需求,我们可以用工厂方法
模式将一个大的工厂,拆分成多个子工厂实现:
java
public abstract class Pizza {
private String name;
public void setName(String name) {
this.name = name;
}
/**
* 每种披萨的准备过程是不一样的,留给子类去实现
*/
public void prepare() {}
public void bake() {
System.out.println("烘烤 " + name);
}
public void cut() {
System.out.println("切割 " + name);
}
public void box() {
System.out.println("打包 " + name);
}
}
class LDGreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println("制作伦敦希腊披萨 准备材料");
}
}
class LDCheesePizza extends Pizza{
@Override
public void prepare() {
System.out.println("制作伦敦奶酪披萨 准备材料");
}
}
class BJGreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println("制作北京希腊披萨 准备材料");
}
}
class BJCheesePizza extends Pizza {
@Override
public void prepare() {
System.out.println("制作北京奶酪披萨 准备材料");
}
}
对工厂进行拆分:
java
/**
* 订购pizza
*/
public abstract class OrderPizza {
/**
* 该方法留给具体的 伦敦 北京 披萨的类去实现,做自己类型的pizza
* @param type
* @return
*/
public abstract Pizza createPizza(String type);
public OrderPizza() {
Pizza pizza = null;
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("***********请输入需要制作的pizza***********");
String next = sc.next();
//制作pizza
pizza = createPizza(next);
if (pizza == null){
break;
}
pizza.bake();
pizza.cut();
pizza.box();
}
sc.close();
}
}
class LDOrderPizza extends OrderPizza{
public LDOrderPizza() {
super();
}
@Override
public Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("Cheese")){
pizza = new CheesePizza();
pizza.setName("伦敦奶酪披萨");
}else if (type.equals("Greek")){
pizza = new GreekPizza();
pizza.setName("伦敦希腊披萨");
}else {
return null;
}
return pizza;
}
}
class BJOrderPizza extends OrderPizza {
public BJOrderPizza() {
super();
}
@Override
public Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("Cheese")){
pizza = new CheesePizza();
pizza.setName("北京奶酪披萨");
}else if (type.equals("Greek")){
pizza = new GreekPizza();
pizza.setName("北京希腊披萨");
}else {
return null;
}
return pizza;
}
}
在模拟订购披萨时,只需要创建具体实现类的对象即可:
java
/**
* Pizza店
*/
public class PizzaStore {
public static void main(String[] args) {
//创建具体的实现类
new BJOrderPizza();
}
}
2.3、抽象工厂模式
抽象工厂模式
是工厂方法模式
的进一步扩展,将工厂抽象成两层,接口层
:抽象工厂,实现类
:具体负责生产各自产品的工厂:
java
/**
* 抽象工厂模式
* 侧重于将工厂分为了多层
*/
public interface AbsFactory {
Pizza createPizza(String type);
}
class BJPizzaFactory implements AbsFactory {
@Override
public Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("Cheese")){
pizza = new CheesePizza();
pizza.setName("北京奶酪披萨");
}else if (type.equals("Greek")){
pizza = new GreekPizza();
pizza.setName("北京希腊披萨");
}else {
return null;
}
return pizza;
}
}
class LDPizzaFactory implements AbsFactory {
@Override
public Pizza createPizza(String type) {
Pizza pizza;
if (type.equals("Cheese")){
pizza = new CheesePizza();
pizza.setName("伦敦奶酪披萨");
}else if (type.equals("Greek")){
pizza = new GreekPizza();
pizza.setName("伦敦希腊披萨");
}else {
return null;
}
return pizza;
}
}
只需要实例化对应的工厂类实现,即可获得相应的对象。
java
/**
* Pizza店
*/
public class PizzaStore {
public static void main(String[] args) {
new OrderPizza(new BJPizzaFactory());
}
}
class OrderPizza {
public OrderPizza(AbsFactory factory) {
setFactory(factory);
}
private void setFactory(AbsFactory factory) {
Pizza pizza = null;
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("***********请输入需要制作的pizza***********");
String type = sc.next();
//制作pizza 由传入的AbsFactory子类类型 去路由到不同的子类
pizza = factory.createPizza(type);
if (pizza == null){
break;
}
pizza.bake();
pizza.cut();
pizza.box();
}
sc.close();
}
}
小结
简单工厂模式
可以理解成仅仅是把具有共性的创建对象的代码抽取到了一个类中,无法应对复杂的业务。而工厂方法模式
是对简单工厂模式
的一种改进,将产品的创建过程委托给子类来解决简单工厂模式的缺点。会将子类进行分类 。每个子类负责创建一个特定类型的产品,客户端只需要通过工厂方法来获取对象。抽象工厂模式
可以看做是简单工厂模式
和工厂方法模式
的结合,既包含了抽象工厂和实现工厂,也包含了抽象产品和实现产品。
简单工厂模式
适合产品种类较少的情况。工厂方法模式
适合产品种类较多且需要通过继承进行扩展的情况。抽象工厂模式
适合需要创建一系列相关产品的情况。
三、原型模式
原型模式
的核心思想在于,通过复制现有的对象 来创建新对象,而不是通过使用构造函数直接创建。这种模式通过克隆现有对象来生成新实例,从而避免了重复创建对象的复杂性和性能开销,特别适合在需要频繁创建类似对象的场景中。
原型模式的结构通常包括以下几个角色:
Prototype(原型接口)
:定义了一个抽象的克隆方法,通常是 clone() 方法,供具体类实现。
java
public interface Prototype {
Prototype clone(); // 克隆方法
}
ConcretePrototype(具体原型类)
:实现了原型接口的具体类,具体定义克隆方法,返回一个自己对象的副本。
java
public class ConcretePrototype implements Prototype {
private String name;
public ConcretePrototype(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public Prototype clone() {
// 通过构造函数创建新的对象
return new ConcretePrototype(this.name);
}
}
Client(客户端)
:使用原型对象并通过克隆方法来创建新的对象。
java
public class Client {
public static void main(String[] args) {
// 创建一个原型对象
ConcretePrototype prototype = new ConcretePrototype("Prototype1");
// 通过克隆方法创建一个新对象
ConcretePrototype clonePrototype = (ConcretePrototype) prototype.clone();
// 输出原型对象的名称
System.out.println("Original: " + prototype.getName());
System.out.println("Clone: " + clonePrototype.getName());
System.out.println(prototype == clonePrototype);
}
}
同时JDK自带的clone
方法也是原型模式的体现,默认是浅拷贝,需要实现Cloneable
接口。
什么是浅拷贝和深拷贝?
浅拷贝
对于基本数据类型,拷贝的是值。对于引用类型,拷贝的是引用(即地址),源对象和目标对象的引用类型成员指向同一个内存位置。如果源对象或目标对象对引用类型的成员进行修改,另一对象的对应成员也会发生变化。
深拷贝
对于基本数据类型,拷贝的是值。对于引用类型,会递归地拷贝引用类型所指向的对象。源对象和目标对象不会共享任何引用类型成员,修改一个对象的引用类型成员不会影响另一个对象。也是不可变类
设计的一个要素。
JUC并发编程,不可变类设计
3.1、如何在原型模式中实现浅拷贝和深拷贝?
浅拷贝通常通过调用 clone() 方法来实现:
java
public class ConcretePrototype implements Cloneable {
private String name;
private Address address;
public ConcretePrototype(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public ConcretePrototype clone() {
try {
return (ConcretePrototype) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
class Address {
private String street;
public Address() {
}
public Address(String street) {
this.street = street;
}
/**
* 获取
* @return street
*/
public String getStreet() {
return street;
}
/**
* 设置
* @param street
*/
public void setStreet(String street) {
this.street = street;
}
public String toString() {
return "Address{street = " + street + "}";
}
}
java
public class ShallowCopy {
public static void main(String[] args) {
Address address = new Address();
address.setStreet("xx街道");
ConcretePrototype original = new ConcretePrototype("测试", address);
ConcretePrototype copy = original.clone();
System.out.println("原件=" + original.toString());
System.out.println("复印件=" + copy.toString());
System.out.println("修改复印件的Address中的Street字段==================");
copy.getAddress().setStreet("yy街道");
System.out.println("原件=" + original.toString());
System.out.println("复印件=" + copy.toString());
}
}
最终的结果是,复印件引用的Address中的字段值发生改变,原件的同步更新
原件=ConcretePrototype{name='测试', address=Address{street = xx街道}}
复印件=ConcretePrototype{name='测试', address=Address{street = xx街道}}
修改复印件的Address中的Street字段==================
原件=ConcretePrototype{name='测试', address=Address{street = yy街道}}
复印件=ConcretePrototype{name='测试', address=Address{street = yy街道}}
深拷贝的区别在于,在调用clone()方法进行浅克隆后,还需要对其中的引用类型创建新的实例,赋值给复制品的引用类型字段,这样原件和复印件的引用类型字段的引用,指向的地址就不相同:
java
public class ConcretePrototype implements Cloneable {
//......
@Override
public ConcretePrototype clone() {
try {
// 深拷贝:创建新的 Address 对象
ConcretePrototype clone = (ConcretePrototype) super.clone();
// 手动创建新的引用,使原件和复印件的地址不相同
clone.address = new Address(this.address.getStreet());
return clone;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
@Override
public String toString() {
return "ConcretePrototype{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
原件=ConcretePrototype{name='测试', address=Address{street = xx街道}}
复印件=ConcretePrototype{name='测试', address=Address{street = xx街道}}
修改复印件的Address中的Street字段==================
原件=ConcretePrototype{name='测试', address=Address{street = xx街道}}
复印件=ConcretePrototype{name='测试', address=Address{street = yy街道}}
小结
原型模式的适用场景:
- 创建对象的代价较大:当对象的创建成本高(如涉及到数据库操作、网络调用等),而且创建出来的对象大部分都相似时,原型模式可以通过复制现有对象来降低性能开销。(和
单例模式
的区别在于,因为业务需求,该对象必须要创建多份)。 - 需要大量相似对象:如果需要创建很多类似的对象,通过克隆现有对象可以避免反复执行相同的构造逻辑。
- 避免重复代码:当多个对象具有相似的构建过程时,原型模式能够避免重复编写对象创建的代码。