单例模式
单例模式(Singleton Pattern)是设计模式中的一种,它确保一个类仅有一个实例,并提供一个全局访问点来访问该实例。这种设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
单例模式的应用场景十分广泛,主要涉及需要频繁使用某个对象而又不想重复创建的情况:
常见应用场景:
- Windows的任务管理器和回收站:这两个都是只能打开一个实例的应用程序,符合单例模式的特点。
- 网站的计数器:由于需要同步访问,使用单例模式可以确保计数的准确性。
- 应用程序的日志应用:一般系统共用一个日志,因此采用单例模式可以方便地对日志进行管理和操作。
- 数据库连接池:数据库连接是一种昂贵的资源,频繁地创建和关闭数据库连接会带来巨大的性能损耗。采用单例模式,可以维护一个数据库连接的实例,从而避免这种损耗。
- 多线程的线程池:线程池需要方便地对池中的线程进行控制,使用单例模式可以确保线程池的唯一性,从而方便管理。
单例模式的优点
- 节省系统资源:由于单例模式限制了一个类只能有一个实例,这避免了因重复创建对象而浪费的内存和其他系统资源。特别是在处理大型对象或需要消耗大量资源的对象时,单例模式可以显著减少资源消耗。
- 提高系统性能:由于无需反复创建对象,单例模式减少了在对象创建和销毁过程中的系统开销。在频繁创建和销毁对象会导致性能问题的场景下,单例模式能有效提升性能。
- 简化管理:单例模式提供了一个全局访问点,使得开发者可以方便地访问和操作该类的唯一实例。这简化了对对象的管理和维护,降低了代码的复杂性。
- 保证数据一致性:在某些情况下,如配置信息、线程池、数据库连接等,需要确保在整个系统中只有一个实例存在,以保证数据的一致性。单例模式可以确保这些资源在全局范围内只有一个访问点,从而避免了数据不一致的问题。
- 易于扩展:虽然单例模式限制了类的实例数量,但它并不限制类的功能扩展。开发者可以在保持单例特性的同时,为类添加新的方法和属性,以满足不断变化的需求。
常见五种单例模式实现方式
1. 饿汉式
实现方式SingletonDemo.java
package demo1;
/**
* 单例模式:饿汉式
* 1 提供一个私有静态的SingletonDemo类型属性
* 2 构造器私有化
* 3 提供一个public静态的初始化方法getInstance()方法
*
* @author Anna.
* @date 2024/4/5 20:34
*/
public class SingletonDemo {
// 静态的SingletonDemo类型属性,初始化时,立即加载这个对象
// 由于JVM类加载规则,加载class时进行初始化,保证了线程安全,但是没有延时加载的优势
// 因此可能会造成内存浪费,因为无论是否使用到该实例,它都会在类加载时就被创建
private static SingletonDemo instance = new SingletonDemo();
// 构造器私有化
private SingletonDemo() {
}
// 提供一个public的初始化方法 getInstance()方法
public static SingletonDemo getInstance(){
return instance;
}
}
单线程测试:
package demo1;
public class SingleThreadedTestClient {
public static void main(String[] args) {
// 单线程模式
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
}
}
执行结果:
多线程测试:
package demo1;
public class MultithreadingTestClient {
public static void main(String[] args) {
// 多线程模式
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo instance3 = SingletonDemo.getInstance();
SingletonDemo instance4 = SingletonDemo.getInstance();
if (instance3.hashCode() != instance4.hashCode()) {
System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-线程名称%s:%s%n",
Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
}
}, "线程" + i).start();
}
}
}
执行结果:
结论:
优点:没有线程安全问题,实现简单。
缺点:可能会造成内存浪费,因为无论是否使用到该实例,它都会在类加载时就被创建。
2. 懒汉式:
实现方式SingletonDemo.java
package demo2;
/**
* 单例模式:懒汉式
* 1 提供一个私有的静态SingletonDemo属性
* 2 构造函数私有化
* 3 提供一个public静态的初始化方法getInstance()方法
*
* @author Anna.
* @date 2024/4/5 20:59
*/
public class SingletonDemo {
// 1 提供一个私有的静态SingletonDemo属性
// 这里在加载时,不进行初始化,调用getInstance()方法时进行初始化。以达到延时加载的目的
private static SingletonDemo instance;
// 2 构造函数私有化
private SingletonDemo(){}
// 3 提供一个public静态的初始化方法getInstance()方法
public static SingletonDemo getInstance(){
// 判断属性是否为空
if(instance == null){
// 延时加载,初始化对象
instance = new SingletonDemo();
}
// 返回对象
return instance;
}
}
单线程测试:
package demo2;
public class SingleThreadedTestClient {
public static void main(String[] args) throws InterruptedException {
// 单线程模式
System.out.println("========== 单线程测试-懒汉式单例模式是否线程安全 ==========");
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
}
}
执行结果:
多线程测试:
package demo2;
public class MultithreadingTestClient {
public static void main(String[] args) {
// 多线程模式
System.out.println("========== 多线程测试-懒汉式单例模式是否线程安全 ==========");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SingletonDemo instance3 = SingletonDemo.getInstance();
SingletonDemo instance4 = SingletonDemo.getInstance();
if (instance3.hashCode() != instance4.hashCode()) {
System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
}
}, "线程" + i).start();
}
}
}
执行结果:多线程模式下,多执行几次我们会发现,有时懒汉式单例模式,拿到的并不是同一个对象
优点:延时加载,节省内存。
缺点:在多线程环境下存在线程安全问题,需要通过加锁来保证实例的唯一性,这可能会影响并发性能。
// 加锁
public static synchronized SingletonDemo getInstance(){
// 判断属性是否为空
if(instance == null){
// 延时加载,初始化对象
instance = new SingletonDemo();
}
// 返回对象
return instance;
}
3. 双重检锁方式:
实现方式SingletonDemo.java
package demo3;
/**
* 单例模式:双重检锁方式
* 1 提供一个私有的静态SingletonDemo属性
* 2 构造函数私有化
* 3 提供一个public静态的初始化方法getInstance()方法
*
* @author Anna.
* @date 2024/4/5 20:59
*/
public class SingletonDemo {
// 1 提供一个私有的静态SingletonDemo属性
// 这里在加载时,不进行初始化,调用getInstance()方法时进行初始化。以达到延时加载的目的
// 使用volatile修饰instance,保证多线程环境下instance的可见性
private volatile static SingletonDemo instance;
// 2 构造函数私有化
private SingletonDemo() {
}
// 3 提供一个public静态的初始化方法getInstance()方法
public static SingletonDemo getInstance() {
// 判断属性是否为空
if (instance == null) {
// 使用 synchronized 代码块 进行加锁
synchronized (SingletonDemo.class) {
if (instance == null) {
// 延时加载,初始化对象
instance = new SingletonDemo();
}
}
}
// 返回对象
return instance;
}
}
单线程测试:
package demo3;
public class SingleThreadedTestClient {
public static void main(String[] args) throws InterruptedException {
// 单线程模式·
System.out.println("========== 单线程测试-双重检锁方式单例模式是否线程安全 ==========");
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
}
}
执行结果:
多线程测试:
package demo3;
public class MultithreadingTestClient {
public static void main(String[] args) {
// 多线程模式
System.out.println("========== 多线程测试-双重检锁方式单例模式是否线程安全 ==========");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SingletonDemo instance3 = SingletonDemo.getInstance();
SingletonDemo instance4 = SingletonDemo.getInstance();
if (instance3.hashCode() != instance4.hashCode()) {
System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
}
}, "线程" + i).start();
}
}
}
执行结果:
优点:结合了饿汉式和懒汉式的优点,既实现了懒加载,又解决了线程安全问题。
缺点:双重检锁的实现虽然巧妙,但也比较复杂,容易出现错误。同时,从Java 5开始,JVM内部对synchronized的实现做了优化,使得这种方式的性能优势不再明显。因此不建议使用。
4. 静态内部类式:
实现方式SingletonDemo.java
package demo4;
/**
* 单例模式:静态内部类方式
*
* 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
* 2 构造函数私有化
* 3 提供一个public静态的初始化方法getInstance()方法
*
* @author Anna.
* @date 2024/4/5 21:31
*/
public class SingletonDemo {
// 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
static class SingletonDemoInstance{
// 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
private static final SingletonDemo instance = new SingletonDemo();
}
// 2 构造函数私有化
private SingletonDemo() {
}
// 3 提供一个public静态的初始化方法getInstance()方法
public static SingletonDemo getInstance() {
return SingletonDemoInstance.instance;
}
}
单线程测试:
package demo4;
public class SingleThreadedTestClient {
public static void main(String[] args) throws InterruptedException {
// 单线程模式·
System.out.println("========== 单线程测试-静态内部类方式单例模式是否线程安全 ==========");
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
}
}
执行结果:
多线程测试:
package demo4;
public class MultithreadingTestClient {
public static void main(String[] args) {
// 多线程模式
System.out.println("========== 多线程测试-静态内部类方式单例模式是否线程安全 ==========");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SingletonDemo instance3 = SingletonDemo.getInstance();
SingletonDemo instance4 = SingletonDemo.getInstance();
if (instance3.hashCode() != instance4.hashCode()) {
System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
}
}, "线程" + i).start();
}
}
}
执行结果:
优点:线程安全,实现简单,且实现了懒加载,延时加载。
缺点:相较于双重检查锁定式,其实现方式稍微复杂一些。
5. 枚举式:
实现方式SingletonDemo.java
package demo5;
/**
* 单例模式:枚举
*
* 1 定义一个枚举的元素
* 2 枚举本省就是单例模式。有JVM从根本上提供保证。避免通过反射和反序列化的漏洞
* 3 如需使用提供相应public 方法即可
*
* @author Anna.
* @date 2024/4/5 21:31
*/
public enum SingletonDemo {
/** 定义一个枚举元素,它就代表了单例的一个实例*/
INSTANCD;
public void test(){System.out.println("枚举单例内部方法调用了");}
}
单线程测试:
package demo5;
public class SingleThreadedTestClient {
public static void main(String[] args) throws InterruptedException {
// 单线程模式·
System.out.println("========== 单线程测试枚举单例模式是否线程安全 ==========");
SingletonDemo instance1 = SingletonDemo.INSTANCD;
SingletonDemo instance2 = SingletonDemo.INSTANCD;
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
// 调用方式
SingletonDemo.INSTANCD.test();
}
}
执行结果:
多线程测试:
package demo5;
public class MultithreadingTestClient {
public static void main(String[] args) {
// 多线程模式
System.out.println("========== 多线程测试枚举单例模式是否线程安全 ==========");
for (int i = 0; i < 100; i++) {
new Thread(() -> {
SingletonDemo instance3 = SingletonDemo.INSTANCD;
SingletonDemo instance4 = SingletonDemo.INSTANCD;
if (instance3.hashCode() != instance4.hashCode()) {
System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
}
}, "线程" + i).start();
}
}
}
执行结果:
优点:线程安全,实现简单,防止反射和反序列化漏洞。
缺点:没有实现懒加载,类加载时就创建了对象实例。
当然通过Spring,也可以创建单例模式(后面介绍)
多线程模式下效率测试
package demo6;
import demo1.SingletonDemo;
import java.util.concurrent.CountDownLatch;
/**
* 测试5中单例模式 多线程下的执行效率
*
* @author Anna.
* @date 2024/4/5 21:53
*/
public class TestClient {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
int threadNum = 10;
CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000000; j++) {
SingletonDemo instance = SingletonDemo.getInstance();
}
countDownLatch1.countDown();
}).start();
}
countDownLatch1.await();
long end = System.currentTimeMillis();
System.out.printf("饿汉式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);
CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000000; j++) {
demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
}
countDownLatch2.countDown();
}).start();
}
countDownLatch2.await();
end = System.currentTimeMillis();
System.out.printf("懒汉式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);
CountDownLatch countDownLatch3 = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000000; j++) {
demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
}
countDownLatch3.countDown();
}).start();
}
countDownLatch3.await();
end = System.currentTimeMillis();
System.out.printf("双重检锁方式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);
CountDownLatch countDownLatch4 = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000000; j++) {
demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
}
countDownLatch4.countDown();
}).start();
}
countDownLatch4.await();
end = System.currentTimeMillis();
System.out.printf("静态内部类方式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);
CountDownLatch countDownLatch5 = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
for (int j = 0; j < 1000000000; j++) {
demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
}
countDownLatch5.countDown();
}).start();
}
countDownLatch5.await();
end = System.currentTimeMillis();
System.out.printf("枚举单例-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);
}
}
执行结果
如何选用
- 单例对象占用资源少,不需要延时加载:枚举式 好于 饿汉式
- 单例对象暂用资源大,需要延时加载:静态内部类 好于 懒汉式
通过反射方式破坏单例模式
验证
package demo7;
import demo4.SingletonDemo;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 通过反射破坏单例模式
*
* @author Anna.
* @date 2024/4/5 22:07
*/
public class TestClient {
public static void main(String[] args) throws Exception {
// 静态内部内实现单例模式
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
// 反射获取静态内部内实现单例模式
Class<SingletonDemo> clazz = (Class<SingletonDemo>) Class.forName("demo4.SingletonDemo");
// 获取构造器
Constructor<SingletonDemo> declaredConstructor = clazz.getDeclaredConstructor(null);
// 设置跳过安全检查
declaredConstructor.setAccessible(true);
// 初始化对象
SingletonDemo singletonDemo1 = declaredConstructor.newInstance();
SingletonDemo singletonDemo2 = declaredConstructor.newInstance();
System.out.printf("反射方式,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", singletonDemo1.hashCode() == singletonDemo2.hashCode());
}
}
执行结果:
优化方案
SingletonDemo.java
package demo7;
public class SingletonDemo {
// 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
static class SingletonDemoInstance{
// 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
private static final SingletonDemo instance = new SingletonDemo();
}
// 2 构造函数私有化
private SingletonDemo() {
// 反射通过调用SingletonDemo()进行初始化时,判断属性 SingletonDemoInstance.instance 是否为空,如果为空则直接抛出异常
if(SingletonDemoInstance.instance != null){
throw new RuntimeException();
}
}
// 3 提供一个public静态的初始化方法getInstance()方法
public static SingletonDemo getInstance() {
return SingletonDemoInstance.instance;
}
}
TestClient2.java
package demo7;
import java.lang.reflect.Constructor;
public class TestClient2 {
public static void main(String[] args) throws Exception {
// 静态内部内实现单例模式
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
// 反射获取静态内部内实现单例模式
Class<SingletonDemo> clazz = (Class<SingletonDemo>) Class.forName("demo7.SingletonDemo");
// 获取构造器
Constructor<SingletonDemo> declaredConstructor = clazz.getDeclaredConstructor(null);
// 设置跳过安全检查
declaredConstructor.setAccessible(true);
// 初始化对象
SingletonDemo singletonDemo1 = declaredConstructor.newInstance();
SingletonDemo singletonDemo2 = declaredConstructor.newInstance();
System.out.printf("反射方式,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", singletonDemo1.hashCode() == singletonDemo2.hashCode());
}
}
执行结果:
通过反序列化方式破坏单例模式
序列化时java,必须实现Serializable接口,因此修改单例
SingletonDemo.java
package demo8;
import java.io.Serializable;
// 实现Serializable接口
public class SingletonDemo implements Serializable {
// 序列化ID,用于版本控制
private static final long serialVersionUID = 1L;
// 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
static class SingletonDemoInstance{
// 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
private static final SingletonDemo instance = new SingletonDemo();
}
// 2 构造函数私有化
private SingletonDemo() {
}
// 3 提供一个public静态的初始化方法getInstance()方法
public static SingletonDemo getInstance() {
return SingletonDemoInstance.instance;
}
}
TestClient.java
package demo8;
import demo8.SingletonDemo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* 通过反序列化单例模式
*
* @author Anna.
* @date 2024/4/5 22:07
*/
public class TestClient {
public static void main(String[] args) throws Exception {
// 静态内部内实现单例模式
SingletonDemo instance1 = SingletonDemo.getInstance();
SingletonDemo instance2 = SingletonDemo.getInstance();
System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
// 序列化instance1到本地文件中 序列化时必须实现序列化接口
try(FileOutputStream fos = new FileOutputStream("D:/temp/a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos);){
oos.writeObject(instance1);
}
// 反序列化获取instance1对象
try(FileInputStream fis = new FileInputStream("D:/temp/a.txt"); ObjectInputStream ois = new ObjectInputStream(fis);){
SingletonDemo instance3 = (SingletonDemo) ois.readObject();
System.out.printf("反序列化方式,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance3.hashCode());
}
}
}
执行结果:
优化方案
为了避免当通过反序列化一个单例类的实例时,可能会创建该类的另一个实例,从而破坏单例模式的唯一性保证。
可以通过在单例类中实现readResolve方法来确保反序列化时返回的是单例的原始实例。
readResolve方法是java.io.Serializable接口中的一个特殊方法, 当反序列化对象时,如果对象所属的类定义了readResolve方法, JVM会调用这个方法,并返回其返回的对象,而不是新创建的对象。
因此,我们可以在单例类的readResolve方法中返回单例的原始实例,以确保反序列化不会创建新的实例。
package demo8;
import java.io.ObjectStreamException;
import java.io.Serializable;
// 实现Serializable接口
public class SingletonDemo implements Serializable {
// 序列化ID,用于版本控制
private static final long serialVersionUID = 1L;
// 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
static class SingletonDemoInstance{
// 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
private static final SingletonDemo instance = new SingletonDemo();
}
// 2 构造函数私有化
private SingletonDemo() {
}
// 3 提供一个public静态的初始化方法getInstance()方法
public static SingletonDemo getInstance() {
return SingletonDemoInstance.instance;
}
// readResolve方法是java.io.Serializable接口中的一个特殊方法,
// 当反序列化对象时,如果对象所属的类定义了readResolve方法,
// JVM会调用这个方法,并返回其返回的对象,而不是新创建的对象。
// 因此,我们可以在单例类的readResolve方法中返回单例的原始实例,以确保反序列化不会创建新的实例。
protected Object readResolve() throws ObjectStreamException {
return getInstance();
}
}
执行结果:
gitee源码
git clone https://gitee.com/dchh/JavaStudyWorkSpaces.git