目录
(2) 懒汉式(Lazy Initialization)(线程不安全)
(3) 线程安全的懒汉式(Synchronized Method)
(4) 双重检查锁(Double-Checked Locking)
一.设计模式概述
1.什么是设计模式?
设计模式(Design Pattern)是一套被广泛接受的、经过试验验证的、可反复使用的基于面向对象的软件设计经验总结,它是软件开发人员在软件设计中,对常见问题的解决方案的总结和抽象。设计模式是针对软件开发中常见问题和模式的通用解决方案
大白话:可以重复利用的解决方案(即有个模版可以直接套用)
2.设计模式有哪些?
①.GoF设计模式:《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为四人组(Gang of Four)。
②.架构设计模式(Architectural Pattern):主要用于软件系统的整体架构设计,包括多层架构、MVC架构、微服务架构、REST架构和大数据架构等。
③.企业级设计模式(Enterprise Pattern):主要用于企业级应用程序设计,包括基于服务的架构(SOA)、企业集成模式(EIP)、业务流程建模(BPM)和企业规则引擎(BRE)等。
④.领域驱动设计模式(Domain Driven Design Pattern):主要用于领域建模和开发,包括聚合、实体、值对象、领域事件和领域服务等。
⑤.并发设计模式(Concurrency Pattern):主要用于处理并发性问题,包括互斥、线程池、管道、多线程算法和Actor模型等。
⑥.数据访问模式(Data Access Pattern):主要用于处理数据访问层次结构,包括数据访问对象(DAO)、仓库模式和活动记录模式等。
3.GoF设计模式的分类?
①.创建型:主要解决对象的创建问题
②.结构型:通过设计和构建对象之间的关系,以达到更好的重用性、扩展性和灵活性
③.行为型:主要用于处理对象之间的算法和责任分配
二.单例模式
(单例即一个实例(一个对象)即使用这种模式可以保证只创建一个对象)
单例模式(Singleton Pattern) 是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
单例模式通常用于管理共享资源,如数据库连接、线程池、配置管理等。
1. 单例模式的核心思想
-
唯一实例:确保一个类只有一个实例。
-
全局访问:提供一个全局访问点,方便其他对象访问该实例。
2. 单例模式的实现方式
引入:怎样算单例
Student类
public class Student { }
StudentTest类
public class StudentTest { public static void main(String[] args) { Student s1=new Student(); System.out.println(s1); Student s2=new Student(); System.out.println(s2);//输出结果:lianxi.oop11.Student@4eec7777 //其中@4eec7777为16进制数 // 比较 s1 和 s2 的引用,结果为 false,因为它们是不同的对象 System.out.println(s1==s2);//输出结果:false //所以这不是单例,因为有2个对象 int a=10; int b=10; System.out.println(a==b); //输出结果:true // 比较基本数据类型 a 和 b 的值,结果为 true,因为它们的值相等 } }
输出结果:
解释:
public static void main(String[] args):程序的入口方法。
Student s1 = new Student(); 和 Student s2 = new Student(); :创建了两个 Student 类的对象 s1 和 s2。
System.out.println(s1); 和 System.out.println(s2);:打印 s1 和 s2 的对象引用。输出结果类似于 lianxi.oop11.Student@4eec7777,这里 @ 后面的十六进制数字是对象的哈希码,它是对象的内存地址的一种表示方式,通常由 Java 的 Object 类的 hashCode() 方法生成。由于 s1 和 s2 是两个不同的对象,它们存储在不同的内存位置,所以哈希码不同。
System.out.println(s1 == s2);:使用 == 运算符比较 s1 和 s2。在 Java 中,对于对象,== 比较的是对象的引用(即它们是否指向相同的内存地址),而不是对象的内容。因为 s1 和 s2 是两个不同的对象,所以结果为 false,这表明这里创建的不是单例模式,因为单例模式要求在整个程序中只有一个对象实例。
int a = 10; 和 int b = 10;:定义了两个整型变量 a 和 b,并初始化为 10。
System.out.println(a == b);:对于基本数据类型(如 int),== 比较的是它们的值。由于 a 和 b 的值都是 10,所以结果为 true。
所以这不是单例,因为有2个对象
以下是几种常见的单例模式实现方式:
(1) 饿汉式(Eager Initialization)
单例模式怎么实现? 第一步:构造方法私有化private Singleton(){ }
第二步:对外提供一个公开的静态的方法,用这个方法获取单个实例//实例方法 //(实例方法必须创建对象才能使用) //因为对象都私有化了,所以使用静态 public static Singleton get(){//返回Singleton类型 //因为要通过这个方法获取Singleton对象 return s;
第三步:定义一个静态变量,在类加载的时候,初始化静态变量。(只初始化一次)private static Singleton s=new Singleton();
/* 单例模式怎么实现? 第一步:构造方法私有化 第二步:对外提供一个公开的静态的方法,用这个方法获取单个实例 第三步:定义一个静态变量,在类加载的时候,初始化静态变量。(只初始化一次) * */ public class Singleton { private static Singleton s=new Singleton(); private Singleton(){ } //实例方法 //(实例方法必须创建对象才能使用) //因为对象都私有化了,所以使用静态 public static Singleton get(){//返回Singleton类型 //因为要通过这个方法获取Singleton对象 return s; } }
-
特点:在类加载时创建实例。
-
优点:实现简单,线程安全。
-
缺点:如果实例未被使用,会造成内存浪费。
public class Singleton {
// 在类加载时创建实例
private static final Singleton s = new Singleton();
// 私有构造方法,防止外部创建实例
private Singleton() {}
// 提供全局访问点
public static Singleton get() {
return s;
}
}
(2) 懒汉式(Lazy Initialization)(线程不安全)
-
特点:在第一次调用
getInstance()
时创建实例。 -
优点:延迟加载,节省内存。
-
缺点:线程不安全,需要额外处理多线程问题。
public class Singleton {
private static Singleton instance;//其默认值为null
//相当于 private static Singleton instance=null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
测试:
Singleton类:
/**
* Singleton是单例的。
*
* 懒汉式的单例模式:用到这个对象的时候再创建对象。别在类加载的时候创建对象。
*
* 第一步:构造方法私有化。
* 第二步:对外提供一个静态方法,通过这个方法可以获取到Singleton对象。
* 第三步:提供一个静态变量,但是这个变量值为null。
*/
public class Singleton {
private static Singleton s;
private Singleton(){}
public static Singleton get(){
if (s == null) {
s = new Singleton();
System.out.println("对象创建了");
}
return s;
}
}
SingletonTest类:
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.get();
Singleton s2 = Singleton.get();
Singleton s3 = Singleton.get();
Singleton s4 = Singleton.get();
Singleton s5 = Singleton.get();
Singleton s6 = Singleton.get();
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
System.out.println(s4);
System.out.println(s5);
System.out.println(s6);
System.out.println(s1 == s2);
}
}
运行结果:
可见,此时只有一个对象。但这个方法其实是存在问题的,试想一下,如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。
▼
(3) 线程安全的懒汉式(Synchronized Method)
最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子
-
特点:通过
synchronized
关键字确保线程安全。 -
优点:线程安全,延迟加载。
-
缺点:每次调用
getInstance()
都会加锁,性能较差。
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
// 或者
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
接下来要做的就是优化性能,目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例
所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁
(4) 双重检查锁(Double-Checked Locking)
-
特点:在加锁前后分别检查实例是否为空,减少加锁次数。
-
优点:线程安全,延迟加载,性能较好。
-
缺点:实现稍复杂。
public class Singleton {
// 使用 volatile 关键字确保可见性
private static volatile Singleton singleton;
// 私有构造方法,防止外部创建实例
private Singleton() {}
// 公共静态方法,提供获取单例实例的全局访问点
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
public static Singleton getInstance():
这是获取单例实例的公共静态方法,作为全局访问点。
if (singleton == null) {...}:第一次检查,避免不必要的同步开销,如果 singleton 已经实例化,直接返回实例,不需要进入同步块。
synchronized(Singleton.class) {...}:同步块,确保只有一个线程可以进入该代码块,防止多个线程同时创建实例。
if (singleton == null) {...}:第二次检查,确保只有一个线程可以创建实例,因为可能有多个线程同时通过第一次检查,但只有一个线程能进入同步块并创建实例。
(5) 静态内部类(Static Inner Class)
-
特点:利用类加载机制确保线程安全,同时实现延迟加载。
-
优点:线程安全,延迟加载,实现简单。
-
缺点:无法传递参数。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
(6) 枚举(Enum)
-
特点:利用枚举的特性实现单例。
-
优点:线程安全,防止反射和序列化破坏单例。
-
缺点:不够灵活,无法延迟加载。
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
3. 单例模式的应用场景
-
资源共享:如数据库连接池、线程池、缓存等。
-
配置管理:如全局配置对象。
-
日志记录:如日志管理器。
-
设备驱动:如打印机驱动程序。
4. 单例模式的优缺点
优点
-
唯一实例:确保一个类只有一个实例,避免资源浪费。
-
全局访问:提供全局访问点,方便其他对象访问。
缺点
-
难以扩展:单例类的职责过重,违反了单一职责原则。
-
难以测试:单例类的全局状态可能影响测试结果。
-
线程安全问题:需要额外处理多线程环境下的实例化问题。
5. 总结
-
单例模式 确保一个类只有一个实例,并提供一个全局访问点。
-
实现方式:包括饿汉式、懒汉式、双重检查锁、静态内部类和枚举等。
-
应用场景:适用于资源共享、配置管理、日志记录等场景。