【Java】初学 Java 设计模式(六):实战单例模式

初学 Java 设计模式(六):实战单例模式

易羽fxst发布于 15 分钟前

一、单例模式介绍

1. 解决的问题

  • 保证一个类只有一个实例。 最常见的是控制某些共享资源 (例如数据库或文件) 的访问权限。

    运作方式是这样的: 如果创建了一个对象,同时过一会儿后决定再创建一个新对象,此时会获得之前已创建的对象, 而不是一个新对象。

    注意, 普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。

  • 为该实例提供一个全局访问节点。 还记得用过的那些存储重要对象的全局变量吗?它们在使用上十分方便,但同时也非常不安全,因为任何代码都有可能覆盖掉那些变量的内容,从而引发程序崩溃。

    和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。同时可以保护该实例不被其他代码覆盖。

2. 定义

单例模式是一种创建型设计模式,能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

3. 应用场景

  • 程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
  • 要更加严格地控制全局变量,可以使用单例模式。

注意:可以随时调整限制并设定生成单例实例的数量,只需修改 获取实例 方法,即 getInstance 中的代码即可实现。

4. 与其他设计模式的关系

  • 抽象工厂模式、生成器模式和原型模式都可以用单例来实现。

  • 外观模式类通常可以转换为单例模式类,因为在大部分情况下一个外观模式就足够了。

  • 如果能将对象的所有共享状态简化为一个享元对象,那么享元模式就和单例模式类似。但这个两个设计模式有两个根本性的不同。

    1. 只会有一个单例实体,但享元类可以有多个实体,各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。享元对象是不可变的。

二、单例模式优缺点

1. 优点

  • 可以保证一个类只有一个实例。
  • 获得了一个指向该实例的全局访问节点。
  • 仅在首次请求单例对象时对其进行初始化。

2. 缺点

  • 违反了单一职责原则。
  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
  • 该模式在多线程环境下需要进行特殊处理,避免多个线程多次出创建单例对象。
  • 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以需要仔细考虑模拟单例的方法。

三、7 种单例模式实现

静态类使用

/**

* 静态类单例实现

*/

public class StaticSingleton {

public static Map<String,String> hashMap = new ConcurrentHashMap<String, String>();

}

  • 这种方式在我们的业务开发场景中非常常见,这样的方式可以在第一次运行的时候直接初始化 Map 类。
  • 在不需要维持任何状态,不需要延迟加载,仅仅用于全局访问时,这种方式更便于使用。
  • 但在需要被继承、需要维持一些特定状态时,单例模式更适合。

单例模式的实现方式比较多,主要是实现上是否支持懒汉模式、是否线程安全。

接下来通过不同的单例模式实现方式来解析单例模式。

1. 懒汉单例模式(线程不安全)

/**

* 懒汉单例模式(线程不安全)

*/

public class SlobThreadUnsafeSingleton {

private static SlobThreadUnsafeSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private SlobThreadUnsafeSingleton() {

}

public static SlobThreadUnsafeSingleton getInstance() {

if (ObjectUtils.isEmpty(instance)) {

instance = new SlobThreadUnsafeSingleton();

}

return instance;

}

}

这种懒汉单例模式满足了懒加载,但当多个访问者同时获取对象实例时(多进程),会导致多个实例并存,没有达到单例模式的需求。

2. 懒汉单例模式(线程安全)

/**

* 懒汉单例模式(线程安全)

*/

public class SlobThreadSafeSingleton {

private static SlobThreadSafeSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private SlobThreadSafeSingleton() {

}

public static synchronized SlobThreadSafeSingleton getInstance() {

if (ObjectUtils.isEmpty(instance)) {

instance = new SlobThreadSafeSingleton();

}

return instance;

}

}

这种懒汉单例模式是线程安全的,但由于锁在方法上,所有的访问都需要锁占用,会导致资源的浪费。非特殊情况,不建议使用此种方式来实现单例模式。

3. 饿汉单例模式(线程安全)

/**

* 饿汉单例模式(线程安全)

*/

public class EagerSingleton {

private static EagerSingleton instance = new EagerSingleton();

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private EagerSingleton() {

}

private static EagerSingleton getInstance() {

return instance;

}

}

饿汉单例模式并不是懒加载,简单来说就是无论是否用到该类都会在程序启动之初创建。这种方式会导致的问题类似一打开某个软件,手机直接卡死(开启了过多无用功能,导致内存不足)。

4. 双重校验锁单例模式(线程安全)

/**

* 双重校验锁单例模式(线程安全)

*/

public class DoubleCheckingLockingSingleton {

private static volatile DoubleCheckingLockingSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private DoubleCheckingLockingSingleton() {

}

public static DoubleCheckingLockingSingleton getInstance() {

if (ObjectUtils.isNotEmpty(instance)) {

return instance;

}

synchronized (DoubleCheckingLockingSingleton.class) {

if (ObjectUtils.isEmpty(instance)) {

instance = new DoubleCheckingLockingSingleton();

}

}

return instance;

}

}

双重校验锁方式实现的单例模式是方法级锁的优化,减少了部分获取实例的耗时,同时也满足了懒加载。

5. 静态内部类单例模式(线程安全)

/**

* 静态内部类单例模式(线程安全)

*/

public class StaticInnerSingleton {

private static class SingletonHolder {

private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();

}

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private StaticInnerSingleton(){

}

public static final StaticInnerSingleton getInstance() {

return SingletonHolder.INSTANCE;

}

}

静态内部类实现的单例模式,既保证了线程安全,也保证了懒加载,同时不会因为加锁的方式导致性能开销过大。

这主要是因为 JVM 虚拟机可以保证多进程并发访问的正确性,即一个类的构造方法在多线程环境下,可以被正确加载。

因此,静态类内部类的实现方式非常推荐使用。

6. CAS「AtomicReference」单例模式(线程安全)

/**

* CAS「AtomicReference」单例模式(线程安全)

*/

public class CompareAndSwapSingleton {

private static final AtomicReference<CompareAndSwapSingleton> INSTANCE = new AtomicReference<CompareAndSwapSingleton>();

private static CompareAndSwapSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private CompareAndSwapSingleton() {

}

public static final CompareAndSwapSingleton getInstance() {

for (; ; ) {

CompareAndSwapSingleton instance = INSTANCE.get();

if (ObjectUtils.isNotEmpty(instance)) {

return instance;

}

INSTANCE.compareAndSet(null, new CompareAndSwapSingleton());

return INSTANCE.get();

}

}

}

Java 并发库提供了很多原子类来支持并发访问的数据安全性:AtomicIntegerAtomicBoolean AtomicLongAtomicReference

AtomicReference 可以封装引用一个实例,支持并发访问,该单例方式就是使用该特点实现。

采用 CAS 方式的优点就是不需要传统的加锁方式来保证线程安全,而是依赖 CAS 的忙等算法,依赖底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞,也就没有了额外开销,因此可以支持较大的并发。

当然,CAS 方式也存在缺点,忙等即如果一直没有获取到将会处于死循环中。

7. 枚举单例模式(线程安全)

/**

* 枚举单例(线程安全)

*/

public enum EnumSingleton {

INSTANCE;

public void whateverMethod() {

System.out.println("enum singleton method");

}

}

《Effective Java》作者约书亚·布洛克推荐使用枚举的方式解决单例模式,这种方式应该是平时最少用到的。

这种方式解决的单例模式的主要问题:线程安全、自由串行化、单一实例。

调用方式

@Test

public void testEnumSingleton()

{

EnumSingleton.INSTANCE.whateverMethod();

}

这种写法在功能上与共有域⽅法相近,但是它更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这种方式还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现单例 的最佳⽅法。

但这种⽅式在存在继承场景下是不可⽤的。

四、单例模式结构

【Java】初学 Java 设计模式(六):实战单例模式

  1. 单例(Singleton)类声明了一个名为 getInstance 获取实例的静态方法来返回其所属类的一个相同实例。

  2. 单例的构造模式必须对客户端(Client)代码隐藏。调用 获取实例 方法必须是获取单例对象的唯一方式。

设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。

java设计模式单例多线程单例模式

阅读 19发布于 15 分钟前

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


学习笔记

个人学习笔记

avatar

易羽fxst

147 声望

2 粉丝

0 条评论

得票时间

avatar

易羽fxst

147 声望

2 粉丝

宣传栏

一、单例模式介绍

1. 解决的问题

  • 保证一个类只有一个实例。 最常见的是控制某些共享资源 (例如数据库或文件) 的访问权限。

    运作方式是这样的: 如果创建了一个对象,同时过一会儿后决定再创建一个新对象,此时会获得之前已创建的对象, 而不是一个新对象。

    注意, 普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。

  • 为该实例提供一个全局访问节点。 还记得用过的那些存储重要对象的全局变量吗?它们在使用上十分方便,但同时也非常不安全,因为任何代码都有可能覆盖掉那些变量的内容,从而引发程序崩溃。

    和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。同时可以保护该实例不被其他代码覆盖。

2. 定义

单例模式是一种创建型设计模式,能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

3. 应用场景

  • 程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
  • 要更加严格地控制全局变量,可以使用单例模式。

注意:可以随时调整限制并设定生成单例实例的数量,只需修改 获取实例 方法,即 getInstance 中的代码即可实现。

4. 与其他设计模式的关系

  • 抽象工厂模式、生成器模式和原型模式都可以用单例来实现。

  • 外观模式类通常可以转换为单例模式类,因为在大部分情况下一个外观模式就足够了。

  • 如果能将对象的所有共享状态简化为一个享元对象,那么享元模式就和单例模式类似。但这个两个设计模式有两个根本性的不同。

    1. 只会有一个单例实体,但享元类可以有多个实体,各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。享元对象是不可变的。

二、单例模式优缺点

1. 优点

  • 可以保证一个类只有一个实例。
  • 获得了一个指向该实例的全局访问节点。
  • 仅在首次请求单例对象时对其进行初始化。

2. 缺点

  • 违反了单一职责原则。
  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
  • 该模式在多线程环境下需要进行特殊处理,避免多个线程多次出创建单例对象。
  • 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以需要仔细考虑模拟单例的方法。

三、7 种单例模式实现

静态类使用

/**

* 静态类单例实现

*/

public class StaticSingleton {

public static Map<String,String> hashMap = new ConcurrentHashMap<String, String>();

}

  • 这种方式在我们的业务开发场景中非常常见,这样的方式可以在第一次运行的时候直接初始化 Map 类。
  • 在不需要维持任何状态,不需要延迟加载,仅仅用于全局访问时,这种方式更便于使用。
  • 但在需要被继承、需要维持一些特定状态时,单例模式更适合。

单例模式的实现方式比较多,主要是实现上是否支持懒汉模式、是否线程安全。

接下来通过不同的单例模式实现方式来解析单例模式。

1. 懒汉单例模式(线程不安全)

/**

* 懒汉单例模式(线程不安全)

*/

public class SlobThreadUnsafeSingleton {

private static SlobThreadUnsafeSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private SlobThreadUnsafeSingleton() {

}

public static SlobThreadUnsafeSingleton getInstance() {

if (ObjectUtils.isEmpty(instance)) {

instance = new SlobThreadUnsafeSingleton();

}

return instance;

}

}

这种懒汉单例模式满足了懒加载,但当多个访问者同时获取对象实例时(多进程),会导致多个实例并存,没有达到单例模式的需求。

2. 懒汉单例模式(线程安全)

/**

* 懒汉单例模式(线程安全)

*/

public class SlobThreadSafeSingleton {

private static SlobThreadSafeSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private SlobThreadSafeSingleton() {

}

public static synchronized SlobThreadSafeSingleton getInstance() {

if (ObjectUtils.isEmpty(instance)) {

instance = new SlobThreadSafeSingleton();

}

return instance;

}

}

这种懒汉单例模式是线程安全的,但由于锁在方法上,所有的访问都需要锁占用,会导致资源的浪费。非特殊情况,不建议使用此种方式来实现单例模式。

3. 饿汉单例模式(线程安全)

/**

* 饿汉单例模式(线程安全)

*/

public class EagerSingleton {

private static EagerSingleton instance = new EagerSingleton();

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private EagerSingleton() {

}

private static EagerSingleton getInstance() {

return instance;

}

}

饿汉单例模式并不是懒加载,简单来说就是无论是否用到该类都会在程序启动之初创建。这种方式会导致的问题类似一打开某个软件,手机直接卡死(开启了过多无用功能,导致内存不足)。

4. 双重校验锁单例模式(线程安全)

/**

* 双重校验锁单例模式(线程安全)

*/

public class DoubleCheckingLockingSingleton {

private static volatile DoubleCheckingLockingSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private DoubleCheckingLockingSingleton() {

}

public static DoubleCheckingLockingSingleton getInstance() {

if (ObjectUtils.isNotEmpty(instance)) {

return instance;

}

synchronized (DoubleCheckingLockingSingleton.class) {

if (ObjectUtils.isEmpty(instance)) {

instance = new DoubleCheckingLockingSingleton();

}

}

return instance;

}

}

双重校验锁方式实现的单例模式是方法级锁的优化,减少了部分获取实例的耗时,同时也满足了懒加载。

5. 静态内部类单例模式(线程安全)

/**

* 静态内部类单例模式(线程安全)

*/

public class StaticInnerSingleton {

private static class SingletonHolder {

private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();

}

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private StaticInnerSingleton(){

}

public static final StaticInnerSingleton getInstance() {

return SingletonHolder.INSTANCE;

}

}

静态内部类实现的单例模式,既保证了线程安全,也保证了懒加载,同时不会因为加锁的方式导致性能开销过大。

这主要是因为 JVM 虚拟机可以保证多进程并发访问的正确性,即一个类的构造方法在多线程环境下,可以被正确加载。

因此,静态类内部类的实现方式非常推荐使用。

6. CAS「AtomicReference」单例模式(线程安全)

/**

* CAS「AtomicReference」单例模式(线程安全)

*/

public class CompareAndSwapSingleton {

private static final AtomicReference<CompareAndSwapSingleton> INSTANCE = new AtomicReference<CompareAndSwapSingleton>();

private static CompareAndSwapSingleton instance;

/**

* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法

*/

private CompareAndSwapSingleton() {

}

public static final CompareAndSwapSingleton getInstance() {

for (; ; ) {

CompareAndSwapSingleton instance = INSTANCE.get();

if (ObjectUtils.isNotEmpty(instance)) {

return instance;

}

INSTANCE.compareAndSet(null, new CompareAndSwapSingleton());

return INSTANCE.get();

}

}

}

Java 并发库提供了很多原子类来支持并发访问的数据安全性:AtomicIntegerAtomicBoolean AtomicLongAtomicReference

AtomicReference 可以封装引用一个实例,支持并发访问,该单例方式就是使用该特点实现。

采用 CAS 方式的优点就是不需要传统的加锁方式来保证线程安全,而是依赖 CAS 的忙等算法,依赖底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞,也就没有了额外开销,因此可以支持较大的并发。

当然,CAS 方式也存在缺点,忙等即如果一直没有获取到将会处于死循环中。

7. 枚举单例模式(线程安全)

/**

* 枚举单例(线程安全)

*/

public enum EnumSingleton {

INSTANCE;

public void whateverMethod() {

System.out.println("enum singleton method");

}

}

《Effective Java》作者约书亚·布洛克推荐使用枚举的方式解决单例模式,这种方式应该是平时最少用到的。

这种方式解决的单例模式的主要问题:线程安全、自由串行化、单一实例。

调用方式

@Test

public void testEnumSingleton()

{

EnumSingleton.INSTANCE.whateverMethod();

}

这种写法在功能上与共有域⽅法相近,但是它更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这种方式还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现单例 的最佳⽅法。

但这种⽅式在存在继承场景下是不可⽤的。

四、单例模式结构

【Java】初学 Java 设计模式(六):实战单例模式

  1. 单例(Singleton)类声明了一个名为 getInstance 获取实例的静态方法来返回其所属类的一个相同实例。

  2. 单例的构造模式必须对客户端(Client)代码隐藏。调用 获取实例 方法必须是获取单例对象的唯一方式。

设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。

以上是 【Java】初学 Java 设计模式(六):实战单例模式 的全部内容, 来源链接: www.h5w3.com/115165.html

回到顶部