单例模式的特点
- 单一实例: 单例模式确保一个类只有一个实例存在。这意味着无论多少次实例化该类,都只能得到同一个实例对象。
- 全局访问点: 单例模式提供了一个全局的访问点,使得外部代码可以访问到这个单例实例。这样可以避免通过传递实例的方式,简化了对象的访问。
- 延迟实例化(懒加载): 单例模式通常使用延迟实例化的方式来确保在首次需要时才创建单例对象。这样可以节省资源,避免不必要的初始化。
实现
1.懒汉式单例
/**
* 懒汉式单例类.在第一次调用的时候实例化自己
* 线程不安全
*/
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
// 私有化构造函数,防止外部直接实例化
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。
懒汉式的单例模式会出现线程安全的问题
懒汉式单例设计模式在第一次调用获取实例的方法时才会创建单例对象,而不是在类加载的时候就创建。这种延迟实例化的方式存在线程安全性问题,主要是由于以下几个原因:
- 并发调用导致多次创建:如果多个线程同时进入获取单例实例的方法,而此时单例对象还未创建,那么它们可能会同时触发单例对象的创建逻辑,导致创建多个实例,违背了单例模式的初衷。
- 未加锁导致竞争条件:懒汉式单例模式通常没有进行同步操作(例如使用 synchronized),这样在并发情况下,多个线程可以同时进入实例化代码段,进而导致创建多个实例的问题。
解决懒汉式单例模式的线程不安全问题可以采用以下几种方式:
1. 使用 synchronized 关键字
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造函数
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2. 双重检查锁定(Double-Checked Locking)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3. 静态内部类实现
public class Singleton {
private Singleton() {
// 私有构造函数
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
其中,利用加锁的方法能保证线程安全是多线程中安全锁的原理,为什么静态内部类也能保证线程安全呢?
静态内部类的机制
- 类加载机制:
- 静态内部类在外部类被加载的时候并不会被加载,而是在第一次使用时才会被加载。这是因为静态内部类的加载并不依赖于外部类的实例化,而是独立于外部类的。
- 类加载过程由 JVM 的类加载器负责,确保了类的加载是线程安全的。因此,静态内部类的加载过程本身就是线程安全的。
- 实例化方式:
- 静态内部类的实例化是在第一次访问时才进行的,并且在类加载时会通过类加载器来确保只有一个实例被创建。
- 因为静态内部类的静态成员只会被初始化一次,且由类加载器负责,多个线程访问时不会出现竞态条件,从而保证了线程安全性。
2.饿汉式单例
public class SingletonEager {
// 在类加载时就创建实例
private static final SingletonEager instance = new SingletonEager();
// 私有构造方法,防止外部通过 new 创建实例
private SingletonEager() {
}
// 公共静态方法,提供全局访问点
public static SingletonEager getInstance() {
return instance;
}
}
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
在这种方法中,实例在类加载时就被创建,因此线程安全,但可能会造成资源浪费,因为即使没有使用该实例,它也会被创建。
为什么饿汉式单例模式没有线程安全的问题呢?
在 Java 中,类加载器确保一个类只被加载一次。无论多少次加载该类,都只会有一个 Class 对象,因此也只会有一个静态变量的实例。这意味着,即使多次加载类,饿汉式单例模式也不会导致多个实例被创建。
具体来说,类加载过程分为加载、连接和初始化三个阶段。在加载阶段,类加载器会检查是否已经加载过该类,如果没有加载过,则加载该类的字节码并生成对应的 Class 对象。在初始化阶段,静态变量会被初始化,包括饿汉式单例模式中的单例实例。
因此,无论多次加载类的字节码,只要 JVM 中存在对应的 Class 对象,单例实例也只会被创建一次。这保证了饿汉式单例模式在多次加载类时仍能保持单例的特性,不会被多次创建。
比较与选择
- 饿汉式适合实例创建后几乎不会被销毁的情况,或者实例占用资源较少且频繁使用的情况。
- 懒汉式适合资源消耗较大或者只在需要时才创建实例的情况,但需要考虑线程安全问题,可以通过双重检查锁定(Double-Checked Locking)或者静态内部类实现来保证线程安全性。
选择适合的单例模式取决于具体的应用场景和需求,需要权衡资源消耗、线程安全性以及实例创建时机等因素。
应用场景
- 日志类(Logger)
- 配置文件读取类(Configuration)
- 数据库连接池(Connection Pool)
- 线程池(Thread Pool)
- 缓存(Cache)
- 对话框(Dialog Box)
- 应用程序的状态管理器(State Manager)。
举例:获取数据库的连接参数
在我们的项目开发阶段,我们的Oracle,MySQL,redis等数据库配置参数,以及我们调用的各种API时要使用的各种密钥等等,这种信息都是属于敏感型信息,如果涉及到上线,就会显得更加重要,如果要把项目上传到git平台,这些敏感信息是绝对不能上传的。因此,我们最好是不将这些敏感信息与我们的业务代码写在一起,原因:1.安全性 2.便于维护。当我们将这些敏感信息提出来统一写到对应的配置文件中后,我们就可以通过忽略文件,选择性的将携带敏感信息的配置文件不上传git。
而我们一般的做法就是将这些敏感信息统一写到一个配置信息文件中,然后通过配置读取类读取出来使用。我们可以发现,无论我们需要使用多少次配置文件中信息,都是通过配置类的实例获取的,而这个配置类的实例实际上只需要一个就可以了。这样就可以采用我们的单例设计模式实现。
db.properties文件
diverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/bookmanager
uname=root
pwd=a
DbProperties配置信息读取类
/**
* 此DbProperties继承 自 Properties,所以它也是 Map,也是一个键值对
* 但增的功能是 ,此DbProperties必须是单例
*/
public class DbProperties extends Properties {
private static DbProperties instance;
private DbProperties(){
//读取配置文件
InputStream iis= DbProperties.class.getClassLoader().getResourceAsStream("db.properties");
//Properties类的load方法加载
try {
this.load( iis ); // this就是 DbProperties 对象,
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static DbProperties getInstance(){
if( instance==null){
instance=new DbProperties();
}
return instance;
}
}
Java代码
String url= p.getProperty("url");
String uname= p.getProperty("uname");
String pwd= p.getProperty("pwd");
String driverClassName= p.getProperty("diverClassName");
//接下来就是我们的数据库操作代码了