Java设计模式之创建型模式 (一)单例模式

本文详细探讨了Java中的单例模式,比较了饿汉式和懒汉式的实现方式,以及如何通过双重检查锁定和volatile关键字解决线程安全和性能问题。
摘要由CSDN通过智能技术生成


创建型模式包括:单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式。
学习目的:创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。

1、单例模式

定义:

1:单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝只有一个实例,并提供一个全局访问点。
2:隐藏其所有的构造方法:
3:属于创建型模式

1-1 、应用场景

在实际业务应用中,Java中的单例模式可以应用于以下场景:

  1. 资源池管理:在需要对资源进行集中管理的场景下,比如连接池、线程池、对象池等,可以使用单例模式来确保资源的唯一性和统一管理。这样可以提高资源的利用效率,避免资源泄露和浪费。
  2. 全局配置管理:当应用程序有全局配置信息需要被共享和使用时,可以使用单例模式来管理这些配置信息。比如系统属性配置、数据库连接配置、缓存配置等,通过单例模式可以保证全局的配置信息只有一个实例,方便访问和修改。
  3. 日志记录器:在应用程序中需要记录日志信息时,可以使用单例模式来管理日志记录器。这样可以确保日志记录的一致性和唯一性,避免多个日志记录器同时记录导致信息混乱。
  4. 计数器和统计信息:在需要对某个业务操作的计数或统计信息进行管理时,可以使用单例模式。比如网站访问量统计、订单数量计数等,通过单例模式可以保证计数器或统计信息的一致性和准确性。
  5. 框架中常见的单例 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 修饰变量,那么可能会出现以下情况:

  1. 线程 A 进入了第一个 if 判断,此时 instance 为空,准备进入同步块。
  2. 线程 B 也进入了第一个 if 判断,由于线程 A 还未创建实例,同样认为 instance 为空,也准备进入同步块。
  3. 线程 A 获得锁,进入同步块,创建实例,并将其赋值给 instance
  4. 线程 A 释放锁。
  5. 线程 B 获得锁,进入同步块,此时它没有意识到线程 A 已经创建了实例,继续创建一个新的实例,并将其赋值给 instance
  6. 线程 B 释放锁。

结果是,instance 变量被重复创建了多次,违背了单例模式的初衷。

通过使用 volatile 修饰变量,可以解决上述问题。使用 volatile 关键字修饰的变量具有以下两个特性:

  1. 可见性:当一个线程修改了被 volatile 修饰的变量的值时,其他线程可以立即看到这个变化。
  2. 禁止指令重排序:在编译器和处理器层面,禁止对 volatile 变量的赋值语句进行指令重排序,保证了在对象实例化完成之前,不会将未初始化的对象引用赋值给 instance

因此,通过在双重检查锁定模式中使用 volatile 修饰变量,可以保证多线程环境下对变量的可见性和正确性,避免重复创建实例的问题。希望这样能够更清晰地解释为什么使用 volatile 关键字。


非常抱歉,上面有的童鞋不太懂这里我再尝试解释一下。

在多线程环境中,由于线程之间的并发执行,可能会导致以下问题

  1. 可见性问题:当一个线程修改了某个变量的值时,其他线程可能无法立即看到这个修改,而是使用的是该变量的旧值。这是因为每个线程都有自己的工作内存,将变量从主内存加载到工作内存后,对该变量的操作都在工作内存中进行,然后才会将结果写回主内存。

  2. 指令重排序问题:为了优化指令执行的效率,编译器和处理器可能会对指令执行的顺序进行调整,这在不影响单线程执行结果的前提下是允许的。然而,对于多线程环境中的共享变量,指令重排序可能会导致不正确的结果。

在懒汉式双重检查锁定模式中,我们希望通过双重检查来减少加锁的次数,提高性能。但在没有适当的同步机制的情况下,可能会导致实例被重复创建。这是因为即使一个线程通过了第一个检查,但在创建实例之前,另一个线程也可能通过了第一个检查并创建了一个实例

为了解决这个问题,我们可以使用 volatile 关键字修饰 instance 变量:

在双重检查锁定模式中,使用 volatile 关键字修饰的静态变量 instance 的作用是确保在多线程环境下的可见性和禁止指令重排序。

  • 可见性:当一个线程修改了被 volatile 修饰的变量的值时,在其他线程中能够立即看到这个修改,而不会使用过期的缓存值。这是因为 volatile 会告诉编译器不要将该变量的访问和修改操作缓存到寄存器或者 CPU缓存中,而是直接读写主存中的值。这样可以保证不同线程之间对该变量的操作是可见的
  • 禁止指令重排序:指令重排序是指编译器和处理器为了优化指令执行效率,可能会对指令执行的顺序进行调整。在没有 volatile 修饰的情况下,编译器和处理器可能会对 instance 的赋值操作进行重排序,导致其他线程在判断 instance 不为 null时,实际上 instance 还没有完成初始化。而使用 volatile 关键字修饰 instance可以禁止指令重排序,保证在构造函数完成后才会将 instance 的引用赋值给变量。

因此,通过使用 volatile 修饰变量,可以确保多线程环境下的安全访问和正确执行双重检查锁定模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值