创建型模式
创建型模式包括:单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。
学习目的:创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。
1、单例模式
定义:
1:单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝只有一个实例,并提供一个全局访问点。
2:隐藏其所有的构造方法:
3:属于创建型模式
1-1 、应用场景
在实际业务应用中,Java中的单例模式可以应用于以下场景:
- 资源池管理:在需要对资源进行集中管理的场景下,比如
连接池、线程池、对象池
等,可以使用单例模式来确保资源的唯一性和统一管理。这样可以提高资源的利用效率,避免资源泄露和浪费。- 全局配置管理:当应用程序有全局配置信息需要被共享和使用时,可以使用单例模式来管理这些配置信息。比如
系统属性配置、数据库连接配置、缓存配置等
,通过单例模式可以保证全局的配置信息只有一个实例,方便访问和修改。- 日志记录器:在应用程序中需要
记录日志
信息时,可以使用单例模式来管理日志记录器。这样可以确保日志记录的一致性和唯一性,避免多个日志记录器同时记录导致信息混乱。- 计数器和统计信息:在需要对某个业务操作的
计数或统计信息
进行管理时,可以使用单例模式。比如网站访问量统计、订单数量计数等,通过单例模式可以保证计数器或统计信息的一致性和准确性。框架中常见的单例 ServletContext、ServletConfig、ApplicationContext.........
需要注意的是,单例模式应用于业务场景时需要谨慎,避免过度使用导致代码的复杂性和耦合度增加。在使用单例模式时,应根据具体业务需求进行评估,确保单例模式的适用性和合理性。
1-2、饿汉式单例
package com.tansun.goods.pack;
/**
* 优点:执行效率高,性能高,没有任何的锁
* 缺点:某些情况下,可能会造成内存浪费
* @author Administrator
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
1-3、懒汉式单例
package com.tansun.goods.pack;
/**
* @author Administrator
* 优点:节省内存
* 缺点:线程不安全
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
1-3-1、 测试类:
package com.tansun.goods.pack;
import lombok.extern.slf4j.Slf4j;
/**
* @author Administrator
*/
@Slf4j
public class ExecutorThread implements Runnable{
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
log.debug("The thread name is : {}, hashcode is {}" ,Thread.currentThread().getName(),instance.hashCode());
}
}
1-3-2、 RUN
package com.tansun.goods.pack;
/**
* @author Administrator
*/
public class LazySingletonTest {
public static void main(String[] args) {
new Thread(new ExecutorThread()).start();
new Thread(new ExecutorThread()).start();
}
}
1-3-3、 控制台结果
已连接到目标 VM, 地址: ''127.0.0.1:60386',传输: '套接字''
18:36:55.323 [Thread-0] DEBUG com.tansun.goods.pack.ExecutorThread - The thread name is : Thread-0, hashcode is 1995014693
18:36:55.323 [Thread-1] DEBUG com.tansun.goods.pack.ExecutorThread - The thread name is : Thread-1, hashcode is 597340363
与目标 VM 断开连接, 地址为: ''127.0.0.1:60386',传输: '套接字''
进程已结束,退出代码0
1-3-4、 改进 加锁 synchronized
package com.tansun.goods.pack;
/**
* @author Administrator
* 优点:节省内存,线程也安全了
* 缺点:性能瓶颈,需要等待
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
1-3-5、 控制台输出
已连接到目标 VM, 地址: ''127.0.0.1:60540',传输: '套接字''
18:43:31.807 [Thread-0] DEBUG com.tansun.goods.pack.ExecutorThread - The thread name is : Thread-0, hashcode is 761088712
18:43:31.807 [Thread-1] DEBUG com.tansun.goods.pack.ExecutorThread - The thread name is : Thread-1, hashcode is 761088712
与目标 VM 断开连接, 地址为: ''127.0.0.1:60540',传输: '套接字''
进程已结束,退出代码0
通过以上加锁的代码优化解决了出现不同实例的问题,但是新问题随之而来。虽然解决了出现不同实例的现象,随之而来的是性能问题,等待线程队伍太长,性能自然而然下降。
1-3-6、 再改进,使用双检锁,懒汉式双重检查锁定(Lazy initialization with double-checked locking)
package com.tansun.goods.pack;
/**
* @author Administrator
*/
public class DoubleCheckLazySingleton {
//使用 volatile 关键字修饰的静态变量,确保在多线程环境下的可见性和禁止指令重排序。
private static volatile DoubleCheckLazySingleton doubleCheckLazySingleton = null;
private DoubleCheckLazySingleton() {//私有构造方法,防止其他类直接实例化对象
}
public static synchronized DoubleCheckLazySingleton getInstance() {
//第一次检查,避免不必要的同步
if (doubleCheckLazySingleton == null) {
//加锁,确保只有一个线程进入同步块
synchronized (DoubleCheckLazySingleton.class) {
//第二次检查,确保只有一个线程创建实例
if (doubleCheckLazySingleton == null) {
//创建实例
doubleCheckLazySingleton = new DoubleCheckLazySingleton();
}
}
}
return doubleCheckLazySingleton;
}
}
1-3-7、 控制台输出
已连接到目标 VM, 地址: ''127.0.0.1:61181',传输: '套接字''
19:19:09.148 [Thread-1] DEBUG com.tansun.goods.pack.ExecutorThread - The thread name is : Thread-1, hashcode is 632604309
19:19:17.649 [Thread-0] DEBUG com.tansun.goods.pack.ExecutorThread - The thread name is : Thread-0, hashcode is 632604309
与目标 VM 断开连接, 地址为: ''127.0.0.1:61181',传输: '套接字''
进程已结束,退出代码0
延伸两个问题:
1:问题好像完全解决了,线程安全了、性能也去了。但是代码是不是有点臃肿
?
2:当然这里还使用了关键字volitatle
关键字,为什么使用呢?
1-3-7 为什么使用volitatle关键字
以更简单的方式解释一下为什么要在双重检查锁定模式中使用 volatile
修饰变量。
在多线程环境下,每个线程都有自己的工作内存,这是为了提高运行效率而引入的一种缓存机制。当一个线程修改了某个变量的值时,它会先在自己的工作内存中修改,并不会立即写回主内存,因此其他线程无法立即看到这个修改。
而在双重检查锁定模式中,如果没有使用 volatile
修饰变量,那么可能会出现以下情况:
- 线程 A 进入了第一个
if
判断,此时instance
为空,准备进入同步块。 - 线程 B 也进入了第一个
if
判断,由于线程 A 还未创建实例,同样认为instance
为空,也准备进入同步块。 - 线程 A 获得锁,进入同步块,创建实例,并将其赋值给
instance
。 - 线程 A 释放锁。
- 线程 B 获得锁,进入同步块,此时它没有意识到线程 A 已经创建了实例,继续创建一个新的实例,并将其赋值给
instance
。 - 线程 B 释放锁。
结果是,instance
变量被重复创建了多次,违背了单例模式的初衷。
通过使用 volatile
修饰变量,可以解决上述问题。使用 volatile
关键字修饰的变量具有以下两个特性:
- 可见性:当一个线程修改了被
volatile
修饰的变量的值时,其他线程可以立即看到这个变化。 - 禁止指令重排序:在编译器和处理器层面,禁止对
volatile
变量的赋值语句进行指令重排序,保证了在对象实例化完成之前,不会将未初始化的对象引用赋值给instance
。
因此,通过在双重检查锁定模式中使用 volatile
修饰变量,可以保证多线程环境下对变量的可见性和正确性,避免重复创建实例的问题。希望这样能够更清晰地解释为什么使用 volatile
关键字。
非常抱歉,上面有的童鞋不太懂这里我再尝试解释一下。
在多线程环境中,由于线程之间的并发执行,可能会导致以下问题
:
-
可见性问题:当一个线程修改了某个变量的值时,其他线程可能无法立即看到这个修改,而是使用的是该变量的旧值。这是因为每个线程都有自己的工作内存,将变量从主内存加载到工作内存后,对该变量的操作都在工作内存中进行,然后才会将结果写回主内存。
-
指令重排序问题:为了优化指令执行的效率,编译器和处理器可能会对指令执行的顺序进行调整,这在不影响单线程执行结果的前提下是允许的。然而,对于多线程环境中的共享变量,指令重排序可能会导致不正确的结果。
在懒汉式双重检查锁定模式中,我们希望通过双重检查来减少加锁的次数,提高性能。但在没有适当的同步机制的情况下,可能会导致实例被重复创建。这是因为即使一个线程通过了第一个检查,但在创建实例之前,另一个线程也可能通过了第一个检查并创建了一个实例
。
为了解决这个问题,我们可以使用 volatile
关键字修饰 instance
变量:
在双重检查锁定模式中,使用 volatile 关键字修饰的静态变量 instance 的作用是确保在多线程环境下的可见性和禁止指令重排序。
- 可见性:当
一个线程修改了被 volatile 修饰的变量
的值时,在其他线程中能够立即看
到这个修改,而不会使用过期的缓存值
。这是因为volatile 会告诉编译器
不要将该变量的访问和修改操作缓存到寄存器或者 CPU缓存中,而是直接读写主存中的值
。这样可以保证不同线程之间对该变量的操作是可见的
。 - 禁止指令重排序:指令重排序是指编译器和处理器为了优化指令执行效率,可能会对指令执行的顺序进行调整。在没有 volatile 修饰的情况下,编译器和处理器可能会对 instance 的赋值操作进行重排序,导致其他线程在判断 instance 不为 null时,实际上 instance 还没有完成初始化。而使用 volatile 关键字修饰 instance可以禁止指令重排序,保证在构造函数完成后才会将 instance 的引用赋值给变量。
因此,通过使用 volatile 修饰变量,可以确保多线程环境下的安全访问和正确执行双重检查锁定模式。