单例模式

单例模式(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 clazz = (Class) Class.forName("demo4.SingletonDemo");

// 获取构造器

Constructor 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 clazz = (Class) Class.forName("demo7.SingletonDemo");

// 获取构造器

Constructor 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

精彩链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。