文章目录
- 饿汉模式
- 懒汉模式
- 线程不安全
- 懒汉式(线程安全)
- “双重检查锁定”(Double-Checked Locking, DCL)实现单例(线程安全)
- 例子 1:两个线程几乎同时请求单例实例
- 例子 2:多个线程在不同时间点请求单例实例
- 例子 3:线程在单例实例创建过程中被阻塞
- 例子 4:线程在实例已创建后进入同步块
- 例子 5:高并发下的单例获取
- 例子 6:异常处理中的单例获取
- 例子 7:延迟初始化的效果
- 静态内部类实现单例(线程安全)
- 类定义
- 构造方法
- 获取单例的方法
- 静态内部类
- 线程安全性
- 例子1:直接通过`getInstance`方法获取单例
- 例子2:在并发环境中验证单例的唯一性
多线程(Multithreading)
“多线程”指的是同时执行多个线程以完成不同的任务。在操作系统中,线程是进程中的一个执行单元,它负责程序的执行流。多线程允许程序在同一时间内执行多个操作,从而提高了程序的总体执行效率和响应能力。在多线程环境中,需要注意线程同步和数据一致性的问题,以避免竞态条件和数据损坏。
并发(Concurrency)
“并发”是指在同一时间段内,多个任务或操作被交替执行,从用户的角度来看,这些任务似乎是同时进行的。并发并不意味着这些任务在同一时刻物理上并行执行,而是在一个时间段内通过时间分片技术交替执行。并发可以通过多线程、多进程、异步I/O等方式实现。
饿汉模式
饿汉式的单例实现比较简单,其在类加载的时候,静态实例instance 就已创建并初始化好了。
public class Singleton {private static final Singleton instance = new Singleton();private Singleton () {}public static Singleton getInstance() {return instance;}
}
饿汉式单例优缺点:
优点:
单例对象的创建是线程安全的;
获取单例对象时不需要加锁。
缺点:单例对象的创建,不是延时加载。
积极加载的问题在于:如果这个类除了加载一次,但其他场景并未调用,那么这个对象就白白被创建,浪费了内存资源.一般认为延时加载可以节省内存资源。但是延时加载是不是真正的好,要看实际的应用场景,而不一定所有的应用场景都需要延时加载。
懒汉模式
线程不安全
与饿汉式对应的是懒汉式,懒汉式为了支持延时加载,将对象的创建延迟到了获取对象的时候
private static Singleton instance = null;public static Singleton getInstance() {if (null == instance){instance = new Singleton();}return instance;
}
这是懒汉式中最简单的一种写法,只有在方法第一次被访问时才会实例化,达到了懒加载的效果。但是这种写法有个致命的问题,就是多线程的安全问题。假设对象还没被实例化,然后有两个线程同时访问,那么就可能出现多次实例化的结果,所以这种写法不可采用。
懒汉式(线程安全)
但为了线程安全,不得不为获取对象的操作加锁,这就导致了低性能。
并且这把锁只有在第一次创建对象时有用,而之后每次获取对象,这把锁都是一个累赘(双重检测对此进行了改进)。
private static Singleton instance = null;public static synchronized Singleton getInstance() {if (null == instance){instance = new Singleton();}return instance;}
}
懒汉式单例优缺点:
优点:
对象的创建是线程安全的。
支持延时加载。
缺点:获取对象的操作被加上了锁,影响了并发度。
如果单例对象需要频繁使用,那这个缺点就是无法接受的。
如果单例对象不需要频繁使用,那这个缺点也无伤大雅。在Java中,当我们谈论“同步开销”时,我们指的是使用
synchronized
关键字来同步代码块或方法时所引入的性能成本。同步是为了防止多个线程同时访问和修改共享资源,从而导致数据不一致或其他线程安全问题。但是,同步也会带来一些性能上的损失,因为线程之间需要竞争锁,而且在等待锁的过程中可能会被阻塞,这些都是需要消耗计算资源的。“同步代码块”是指使用
synchronized
关键字包裹的一段代码,它确保在同一时间只有一个线程可以执行这段代码。同步代码块通常用于保护对共享资源的访问,以避免并发问题。
“双重检查锁定”(Double-Checked Locking, DCL)实现单例(线程安全)
第一次 null
检查是在同步块外部进行的,它能够快速过滤掉那些不需要进入同步块的线程(即当单例实例已经被创建时)。而第二次 null
检查则是在确保线程安全的前提下,进一步确认实例是否已经被创建,从而避免不必要的实例创建过程。
双重检查锁定的主要目的是确保单例实例只被创建一次,并且在多线程环境下保持线程安全,同时尽量减少同步带来的性能开销。
private static Singleton instance = null;//用于存储单例的实例public static Singleton getInstance() {//因为本身"锁"资源就是一个比较昂贵的资源,为了避免跑得慢的线程去抢这个把锁,所以此处也要进行非空判断.if (null == instance){//先进来的部分线程在此处排队//当多个线程同时尝试获取单例实例时,由于 synchronized 关键字的存在,只有一个线程能够进入同步块,其他线程将被阻塞,等待锁被释放。这就形成了一种“排队”的效果。synchronized (Singleton.class){//此时判断的是已经抢到锁资源,进行排队的线程是否"来晚了"//看他有没有获得实例引用//防止二次创建//在同步块内部的第二次 null 检查。这是因为在多线程环境下,有可能出现两个线程同时通过了第一次 null 检查,但只有一个线程能够获得锁并进入同步块。如果没有内部的第二次 null 检查,那么第二个获得锁的线程可能会再次创建单例实例,从而导致单例模式失效。内部的第二次 null 检查确保了即使在这种情况下,单例实例也只会被创建一次。if (null == instance){instance = new Singleton();}}}return instance;}
这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第三种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。
这种实现方式在 Java 1.4 及更早的版本中有些问题,就是指令重排序,可能会导致 Singleton 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化,就被另一个线程使用了。
要解决这个问题,需要给 instance 成员变量加上 volatile 关键字,从而禁止指令重排序。
而高版本的 Java 已在 JDK 内部解决了这个问题,所以高版本的 Java 不需要关注这个问题。
双重检测单例优点:
对象的创建是线程安全的。
支持延时加载。
获取对象时不需要加锁。
例子 1:两个线程几乎同时请求单例实例
线程A和线程B几乎同时调用getInstance()方法。
两者都发现instance是null,因此都尝试进入synchronized块。
假设线程A先获得了锁,它将创建一个Singleton实例并将其赋值给instance变量。
当线程A释放锁后,线程B将获得锁并进入synchronized块。但是,由于已经有了内部的第二次null检查,线程B将发现instance已经不再是null,因此它不会创建新的Singleton实例。
线程B将返回已经创建的Singleton实例。
例子 2:多个线程在不同时间点请求单例实例
线程A首先调用getInstance()方法,发现instance是null,于是它创建一个Singleton实例并返回。
稍后,线程B调用getInstance()方法。此时,由于instance已经非空,线程B将直接返回已经创建的Singleton实例,而不会进入synchronized块。
更多的线程在不同时间点调用getInstance(),但它们都将直接返回同一个Singleton实例,而不会创建新的实例。
例子 3:线程在单例实例创建过程中被阻塞
线程A进入synchronized块并开始创建Singleton实例。
在线程A还没有完成实例创建时,线程B尝试调用getInstance()方法。
线程B将被阻塞,直到线程A完成实例的创建并释放锁。
一旦线程A释放锁,线程B将获得锁并进入synchronized块。但是,由于内部的null检查,它将发现instance已经不再是null,并且不会尝试重新创建Singleton实例。
线程B(以及任何后续线程)将返回同一个已经创建的Singleton实例。
例子 4:线程在实例已创建后进入同步块
线程A首先调用getInstance()方法,并成功创建了Singleton实例。
稍后,线程B进入getInstance()方法,并通过了第一次null检查(尽管此时实例已经被创建)。
线程B进入synchronized块,但由于内部的第二次null检查,它发现instance已经非空,因此不会重新创建实例。
线程B直接返回已创建的Singleton实例。
这个例子展示了即使在实例已经存在的情况下,线程仍然可能会进入同步块,但由于内部的第二次检查,它不会重复创建实例。
例子 5:高并发下的单例获取
在高并发场景下,多个线程(线程A、B、C、D等)几乎同时调用getInstance()方法。
由于多线程的竞态条件,这些线程可能都会通过第一次的null检查。
这些线程将尝试获取Singleton.class的锁以进入同步块。
只有一个线程(例如线程A)将获得锁,并创建Singleton实例。
其他线程(B、C、D等)将被阻塞,直到线程A释放锁。
当线程A释放锁后,其他线程将依次获得锁,但由于内部的null检查,它们将不会重新创建实例。
所有线程最终都将返回同一个Singleton实例。
这个例子强调了在高并发环境下,DCL模式如何确保只有一个实例被创建,并且所有线程最终都获得相同的实例。
例子 6:异常处理中的单例获取
线程A进入getInstance()方法并开始创建Singleton实例。
在创建过程中,线程A遇到了一个异常(例如,内存不足),导致实例创建失败。
异常被捕获并处理,但没有重新尝试创建实例。
稍后,线程B调用getInstance()方法,并发现instance仍为null。
线程B成功创建Singleton实例并返回。
这个例子展示了在异常处理中,如果实例创建失败,DCL模式能够允许其他线程在后续尝试中成功创建实例。
例子 7:延迟初始化的效果
在程序启动初期,没有线程调用getInstance()方法,因此Singleton实例尚未被创建。
当程序运行一段时间后,某个功能需要Singleton实例时,线程A首次调用getInstance()方法。
线程A发现instance为null,并进入同步块创建实例。
后续对getInstance()的调用都将返回此先前创建的实例。
这个例子展示了DCL模式如何实现单例的延迟初始化,即只在首次需要时才创建实例。
静态内部类实现单例(线程安全)
静态内部类也称作Singleton Holder, 也就是单持有者模式, 是线程安全的, 也是懒惰模式的变形.
JVM加载类的时候, 有这么几个步骤:
①加载 -> ②验证 -> ③准备 -> ④解析 -> ⑤初始化
需要注意的是:
JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类(SingletonHolder)的属性/方法被调用时才会被加载, 并初始化其静态属性(instance).
/*** 静态内部类模式, 也称作Singleton Holder(单持有者)模式: 线程安全, 懒惰模式的一种, 用到时再加载*/
final class StaticInnerSingleton {/** 禁用构造方法 */private StaticInnerSingleton() { }/*** 通过静态内部类获取单例对象, 没有加锁, 线程安全, 并发性能高* @return SingletonHolder.instance 内部类的实例*/public static StaticInnerSingleton getInstance() {return SingletonHolder.instance;}/** 静态内部类创建单例对象 */private static class SingletonHolder {private static StaticInnerSingleton instance = new StaticInnerSingleton();}
}
这个StaticInnerSingleton
类是一个实现了单例设计模式的Java类。单例设计模式确保一个类只有一个实例,并提供一个全局访问点来获取这个唯一实例。这个特定的实现方式被称为“静态内部类”或“Singleton Holder”模式,它结合了线程安全和延迟初始化的优点。
类定义
final class StaticInnerSingleton
: 这个类被声明为final
,意味着它不能被继承。这是为了确保单例的唯一性和稳定性。
构造方法
private StaticInnerSingleton() { }
: 私有构造方法确保外部代码不能直接通过new
关键字创建StaticInnerSingleton
的实例。这是实现单例模式的关键步骤之一,因为它防止了外部代码的非法实例化
获取单例的方法
public static StaticInnerSingleton getInstance()
: 这是一个公开的静态方法,用于获取单例对象。由于它是静态的,你可以直接通过类名调用它,而不需要创建类的实例。
静态内部类
private static class SingletonHolder
: 这是一个私有的静态内部类。由于它是私有的,外部代码无法直接访问它。静态内部类在这里起到了关键作用,因为它允许延迟初始化单例对象。
private static StaticInnerSingleton instance = new StaticInnerSingleton();
: 在这个静态内部类中,我们创建了StaticInnerSingleton
的一个静态实例。由于这个内部类是私有的,并且这个实例是静态的,它只会在第一次被引用时才会被初始化(即,当getInstance()
方法首次被调用时)。这种延迟初始化的特性使得这个单例模式实现既是线程安全的,又是懒惰初始化的。
线程安全性
这种实现方式的线程安全性是由Java的类加载机制保证的。在Java中,类的加载和初始化是线程安全的,也就是说,当多个线程同时尝试加载和初始化一个类时,Java虚拟机(JVM)会确保这个类只被加载和初始化一次。因此,即使多个线程同时调用getInstance()
方法,SingletonHolder
类(以及其中的instance
字段)也只会被初始化一次,从而保证了单例的唯一性。
“静态内部类”或“Singleton Holder”模式是一种高效且线程安全的单例实现方式。它结合了懒惰初始化和线程安全的优点,而且由于其简洁和高效的特性,在实际开发中经常被使用
多线程环境下创建单例对象需要确保线程安全,即无论多少个线程同时尝试获取单例,都应该得到同一个实例,且该实例只被创建一次。StaticInnerSingleton
类使用了静态内部类(也称为嵌套类)来实现这一点,这是一种既线程安全又延迟初始化的单例实现方式。
下面,我将结合StaticInnerSingleton
类,给出几个多线程创建单例的例子。
例子1:直接通过getInstance
方法获取单例
多个线程可以同时调用getInstance
方法,但是由于静态内部类的特性,SingletonHolder.instance
只会在首次被引用时初始化,因此是线程安全的。
public class SingletonDemo { public static void main(String[] args) { // 模拟多线程环境 Runnable task = () -> { StaticInnerSingleton instance = StaticInnerSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ": " + instance); }; // 创建并启动多个线程来获取单例对象 Thread t1 = new Thread(task, "Thread-1"); Thread t2 = new Thread(task, "Thread-2"); Thread t3 = new Thread(task, "Thread-3"); t1.start(); t2.start(); t3.start(); }
}
在这个例子中,我们创建了三个线程,每个线程都尝试获取StaticInnerSingleton
的实例。由于使用了静态内部类实现单例,因此无论多少个线程同时尝试获取,都只会得到一个相同的实例。
例子2:在并发环境中验证单例的唯一性
我们可以进一步验证在多线程环境中单例的唯一性。
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; public class SingletonConcurrencyTest { public static void main(String[] args) throws InterruptedException { // 创建一个固定大小的线程池 ExecutorService executor = Executors.newFixedThreadPool(10); Set<StaticInnerSingleton> instances = new HashSet<>(); // 提交多个任务来验证单例的唯一性 for (int i = 0; i < 100; i++) { executor.submit(() -> { StaticInnerSingleton instance = StaticInnerSingleton.getInstance(); synchronized (instances) { instances.add(instance); } }); } // 关闭线程池并等待所有任务完成 executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); // 验证是否所有实例都是相同的(即集合中只有一个元素) System.out.println("Number of unique instances: " + instances.size()); // 应该输出 1 }
}
在这个例子中,我们创建了一个包含100个任务的线程池,每个任务都尝试获取StaticInnerSingleton
的实例并将其添加到一个同步集合中。最后,我们验证集合中实例的数量,以确认是否所有线程都获取到了相同的单例对象。如果单例实现是正确的,那么集合中应该只有一个唯一的实例。
这些例子展示了如何在多线程环境中安全地创建和使用单例对象,同时验证了StaticInnerSingleton
类的线程安全性和单例保证。