目录
单例模式
1、饿汉式单例模式
2、懒汉式单例模式
3、双重检查锁单例模式
4、ThreadLocal单例模式
5、枚举类单例模式
Java中的设计模式主要分为三种类型:创建型模式、结构型模式和行为型模式。
创建型模式关注如何创建对象,旨在解决对象的创建问题,包括单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式等。今天我们详细说说单例模式(Singleton Pattern)。
Java单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。单例模式通常用于需要全局唯一的对象,如线程池、数据库连接池等场景。
在Java单例模式中,通常会使用一个私有的静态变量来存储该类的唯一实例,同时定义一个私有的构造函数来防止外界实例化该类。为了保证线程安全,还可以通过使用synchronized关键字对方法进行同步处理。最后,还需要提供一个公共的静态方法,该方法可以返回类的唯一实例。
Java单例模式的主要作用是限制一个类的实例化次数,并且提供一个全局唯一的访问点,使得多个线程在访问这个类时能够获得同一个实例。它的优点包括减少对系统资源的占用,降低系统开销,提高代码的可重用性和可维护性等。
Java单例模式的主要应用场景包括:
- 数据库连接池
- 日志记录器
- 配置文件管理器
- 线程池
- 缓存管理器
- 打印机池
- 窗口管理器
总之,在需要单例的场景中,Java单例模式可以帮助我们避免因为重复创建对象而导致的资源浪费和性能问题,同时它还可以提高程序的可维护性和可扩展性。
饿汉式单例模式指的是在单例类被加载时就创建了一个单例对象,以保证程序中任何地方都可以访问到这个单例对象。它的核心思想是通过静态变量来持有唯一的单例实例,然后提供一个静态方法来获取该实例,并且将构造函数设置为私有,以防止外部通过new操作符来创建多个实例。
下面是一个简单的Java饿汉式单例模式的示例代码:
public class Singleton {
//私有静态变量,存储唯一实例
private static final Singleton INSTANCE = new Singleton();
// 私有构造函数,防止外界实例化该类
private Singleton() {}
// 公共静态方法,返回唯一实例
public static Singleton getInstance() {
return INSTANCE;
}
}
我们通过使用final关键字定义了一个类型为Singleton的静态常量INSTANCE,在类被加载时即被初始化。由于它是静态常量,因此所有的实例均共享同一个引用,从而保证了整个应用程序中只有一个Singleton对象实例。
需要注意的是,饿汉式单例模式虽然很简单,但也有一些潜在的问题。首先,它会在类初始化时就创建对象实例,无论这个实例是否被用到,这样一来会影响程序的启动速度;其次,如果单例对象初始化需要耗费大量的时间或者资源,那么这种立即创建的方式就可能导致系统资源的浪费。因此,在选择单例模式实现方式时,我们需要根据具体的业务需求和应用场景进行权衡和选择。
饿汉式单例模式的优点和缺点如下:
优点:
- 简单易理解:饿汉式单例模式的实现非常简单,只需要声明私有构造方法、静态变量和静态方法即可。代码易于理解和维护。
- 线程安全:饿汉式单例模式在类加载时就创建了对象实例,因此天然具有线程安全性,不需要进行任何额外的同步操作。
- 调用方便:由于饿汉式单例模式在类加载时就创建了对象实例,并且该实例在整个应用程序中都是唯一的,因此调用方非常方便,不需要考虑参数传递和对象初始化等问题。
缺点:
- 系统资源消耗:由于饿汉式单例模式在程序启动时就创建了对象实例,并且在整个程序生命周期内都处于存活状态,因此会占用一定的系统资源,如果该实例占用的系统资源较多,会影响系统性能和稳定性。
- 懒加载无效:饿汉式单例模式无法实现懒加载(Lazy Loading),即等需要使用时才去创建对象实例的功能,这意味着如果单例对象很少用到,或者占用的系统资源很大,那么就会浪费系统资源。
- 不支持动态扩展:由于饿汉式单例模式在类加载时就创建了对象实例,并且该实例在整个程序生命周期内都处于存活状态,因此不支持动态扩展。
Java懒汉式单例模式是一种延迟创建对象实例的单例模式,只有在需要使用对象时才会创建对象实例。
懒汉式单例模式通常包括一个私有构造方法、一个私有静态变量和一个公有静态方法。私有构造方法用于禁止外部直接创建对象实例,私有静态变量用于保存单例对象实例,公有静态方法则负责返回单例对象实例。
懒汉式单例模式的关键点在于在静态方法中对单例对象进行创建,需要判断单例对象是否已经存在。如果单例对象不存在,则创建单例对象实例并返回,否则直接返回单例对象实例。
为了保证线程安全,懒汉式单例模式通常使用synchronized关键字对静态方法进行同步,防止多个线程同时访问静态方法导致对象被创建多次的问题。但是这种同步方式会影响性能,并且在多线程高并发环境下可能导致死锁问题。
以下是一个懒汉式单例模式的代码实例:
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
上述实现中,内部静态类LazyHolder
持有了单例实例INSTANCE
。当第一次调用getInstance()
方法时,会触发LazyHolder
的初始化,此时会创建单例实例并赋值给INSTANCE
变量,以后每次调用getInstance()
方法时直接返回INSTANCE
变量即可。
需要注意的是,该实现方式存在一定的反射攻击漏洞,可以在控制台输入以下代码来创建多个单例实例:
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
Singleton instance2 = constructor.newInstance();
为了防止反射攻击,可以在私有构造方法中添加判断,当已经存在单例实例时抛出异常,这样一旦创建过单例对象后,就不能再通过反射重新创建单例实例。
private Singleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Already initialized.");
}
}
懒汉式单例模式是一种非常常用且重要的设计模式,能够大大提高系统的性能和效率,同时也需要注意线程安全和反射攻击等问题。 这种单例模式适用于某些开销较大的对象的创建,例如数据库连接池、线程池等。
懒汉式单例模式的优点和缺点如下:
优点:
- 延迟加载。只有在需要使用实例对象时才创建单例对象,避免了一开始就初始化开销较大的对象。
- 线程安全。采用静态内部类等方式实现单例模式,保证了线程安全。
- 代码简洁。相对于饿汉式单例模式,懒汉式单例模式代码较为简洁。
- 节省内存。由于只有在需要使用实例对象时才创建单例对象,避免了不必要的内存开销。
缺点 :
线程不安全的实现方式可能导致多个线程同时进入判断条件的分支,从而创建多个单例对象。
需要考虑单例对象的反序列化问题,否则可能会创建多个实例对象。
对于高并发场景,加锁影响性能,双重检查锁定等技术可能会出现死锁等问题。
结合了懒汉式和饿汉式的优点,兼顾了线程安全和延迟加载。是一种多线程环境下延迟加载的单例模式,适用于需要频繁访问、创建开销较大的对象。其作用是确保在多线程环境下,只有一个实例对象被创建,并且能够避免由于加锁带来的性能损失。
对于双重检查锁单例模式的用户而言,无需关心对象的创建细节,只需要调用getSingleton()方法即可获得单例对象。该单例对象可以被所有模块共用,避免了各个模块各自创建单例对象的情况,从而降低了内存消耗。
以下是双重检查锁单例模式的代码实现:
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getSingleton() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上述实现采用了双重检查锁定技术,第一次检查保证只有一个实例对象,第二次检查在实例对象为null时创建实例,避免了多次加锁和实例化的问题。由于JVM本身的重排序机制可能会影响双重检查锁定的正确性,需要在instance变量上加上volatile关键字,确保多线程间的可见性。
需要注意的是,虽然Java双重检查锁单例模式避免了一般加锁导致的线程阻塞问题,但也存在反射攻击和序列化问题。因此,在实际应用中,需要注意对单例模式的安全防御及其它方面的考虑。
双重检查锁单例模式中,需要注意以下几点:
volatile关键字的使用。在实例变量上加上volatile关键字可以确保多线程间的可见性,避免了因为指令重排序而导致的单例对象创建不正确的问题。
synchronized关键字的使用。在第一次检查时,对于instance为null的情况,需要进行加锁操作,以保证只有一个线程能进入synchronized块来创建单例对象。
反射攻击漏洞的防御。为了防止反射攻击,可以在构造方法中添加判断,当已经存在单例对象时,抛出异常或者直接返回单例对象。
序列化问题的解决。需要在单例类中增加一个readResolve()方法,用于控制被序列化的对象,在被反序列化时是否为同一个对象。
双重检查锁单例模式的优缺点:
优点:
适用于多线程环境下使用频繁又开销较大的对象,可以避免多次实例化对象带来的内存消耗。
采用双重检查锁机制,保证了线程安全性,同时也避免了由于使用synchronized关键字带来的性能损失。
延迟加载机制可以避免一开始就初始化对象,节省了内存资源。
缺点:
实现较为复杂,需要考虑多线程环境下的并发等问题。
反射攻击漏洞与序列化问题需要额外做处理。
在某些场景下,由于系统加锁及其他因素的影响,也可能会导致性能问题。
ThreadLocal单例模式是一种多线程环境下实现单例的方式,它的作用是确保在每个线程中都只有一个单例对象被创建和使用。ThreadLocal类提供了一种解决多线程数据并发访问的方案,它可以为每个线程提供独立的变量副本,从而避免了线程之间的数据共享问题。
ThreadLocal单例模式的用途包括:
数据库连接池、配置信息等资源的共享。在多线程环境下,需要频繁访问这些共享资源,为了避免多次创建和释放资源带来的性能损失,可以使用ThreadLocal单例模式,在每个线程中都创建一个单例对象,实现资源的共享和优化。
线程管理相关的逻辑。在多线程环境下,线程之间应用逻辑相对独立,为了避免各个线程之间的干扰和可能导致的错误,可以使用ThreadLocal单例模式,在每个线程中分别保存线程相关的信息,如上下文、请求信息等。
并发控制的实现。在多线程环境下,为了避免由于竞争条件和互斥锁导致的性能问题,可以使用ThreadLocal单例模式,通过为每个线程提供独立的副本,实现并发控制和优化。
以下是一个简单的 Java ThreadLocal单例模式实现样例:
public class Singleton {
private static final ThreadLocal INSTANCE = new ThreadLocal() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE.get();
}
}
INSTANCE变量用于保存ThreadLocal对象,通过重写initialValue()方法,可以为每个线程创建一个独立的单例对象。而getInstance()方法则返回当前线程对应的单例对象。这样,即使多个线程同时调用getInstance()方法,也可以保证它们各自获得独立的单例对象。
由于ThreadLocal对象本身也是一个静态变量,所以在使用时也需要考虑内存泄漏和资源释放等问题。一般情况下,可以通过调用remove()方法来清除当前线程的ThreadLocal副本。另外,也可以通过使用WeakReference等方式来避免由于强引用导致的问题。
ThreadLocal单例模式的优缺点:
优点:
线程安全性。ThreadLocal为每个线程提供独立的存储空间,避免了多线程共享变量的问题,从而确保了线程安全。
高效性。ThreadLocal适用于频繁读写和访问的场景,可以避免过多的锁竞争带来的性能开销,提高系统的执行效率。
可扩展性。ThreadLocal可以按需创建线程局部变量,具有很好的可扩展性,可以方便地应对不同的需求和场景。
缺点:
内存消耗。由于ThreadLocal需要为每个线程创建独立的变量副本,会占用额外的内存资源,因此需要谨慎使用。
粒度控制不易。由于ThreadLocal是基于线程级别的封装,因此粒度较粗,对于某些粒度较细、需要跨线程访问的场景,可能不太适用。
代码复杂度。ThreadLocal需要引入额外的代码实现,包括ThreadLocal对象的声明和初始化、initialValue()方法的实现等,会增加代码的复杂度和维护成本。
枚举类单例模式是一种基于枚举类型实现的单例模式,它在网络环境、多线程和反序列化中都能保持单例的唯一性。枚举用于创建恰好只有一个实例的对象类型,并且该实例在整个应用程序中可用。Java 枚举类单例模式适用于需要保证单例对象唯一性的场景,如配置信息、数据库连接池、缓存等。
枚举类单例模式的代码实现:
public enum Singleton {
INSTANCE; // 唯一的枚举项,表示单例对象
// 单例对象的属性和方法
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
在这个示例中,我们定义了一个枚举类型 Singleton
,该类型只有一个枚举项 INSTANCE
,表示单例对象。由于枚举类型天生就是单例的,所以这里不需要再额外定义 getInstance()
方法。
在 Singleton
类型中,我们使用 private int value
定义了一个私有的属性,用于描述单例对象的状态。同时,我们还提供了 getValue()
和 setValue()
方法,可以访问和修改单例对象的状态。
在应用程序中,如果要获取单例对象,只需要调用 Singleton.INSTANCE
就可以了,因为 INSTANCE
是唯一的枚举项,表示单例对象。例如:
Singleton singleton = Singleton.INSTANCE;
singleton.setValue(10);
int value = singleton.getValue(); // 10
这段代码创建了一个 Singleton
类型的变量 singleton
,并将该变量赋值为 Singleton.INSTANCE
,即单例对象。然后,我们使用 setValue()
方法设置单例对象的状态为 10,并使用 getValue()
方法获取单例对象的状态值。
枚举类单例模式的优点包括:
简单明了。枚举类单例模式的代码简洁易懂,可以方便地创建单例对象,并在整个应用程序中使用。
高效安全。枚举类单例模式是线程安全的,支持并发访问,不需要任何额外的同步机制,可以提高系统的执行效率。
序列化支持。由于枚举类天生就是Serializable的,因此可以无缝地支持序列化和反序列化,可以避免由于序列化和反序列化带来的单例对象失效问题。
枚举类单例模式的用途包括:
配置信息的管理。在配置信息等场景下,只需要创建一个唯一的实例对象即可满足应用程序的需要。
数据库连接池的管理。在数据库连接池等场景下,需要频繁地访问共享资源,为了避免打开和关闭连接的性能开销,可以使用单例模式,保证只有一个实例对象被创建和使用。
缓存的管理。在缓存等场景下,需要维护特定的缓存策略,可以使用单例模式,将缓存对象作为单例对象进行管理,提高缓存的效率和稳定性。
好了单例模式就介绍到这里,欢迎评论留言谈论。