单例模式
(Singleton Pattern):通过单例模式的方法创建的类在当前进程中只有一个实例
(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例),该类负责创建自己的对象
,同时确保只有单个对象
被创建。
注
:
1、单例类只能
有一个实例
。
2、单例类必须自己创建
自己的唯一实例。
3、单例类必须给所有其他对象提供
这一实例。
Java中两种构建方式:
懒汉式
—线程不安全
:最基础的实现方式,线程上下文单例
,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作
。
懒汉式
—线程安全
:加上synchronize
之类保证线程安全的基础上的懒汉模式,相对性能很低
,大部分时间并不需要同步饿汉方式。指全局的单例实例在类装载时构建。
饿汉式
:这种方式比较常用
,但容易产生垃圾
对象。优点:没有加锁,执行效率会提高
。缺点:类加载时就初始化,浪费内存
。基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果
双检锁/双重校验锁(DCL,即 double-checked locking)
:在懒汉式基础上利用synchronize关键字和volatile关键字确保第一次创建时没有线程间竞争而产生多个实例,仅第一次创建时同步,性能相对较高。
登记式/静态内部类
:作为创建类的全局属性存在,创建类被装载时创建。
枚举
:java中枚举类本身也是一种单例模式
应用场景
包括需要控制实例数量的类
,如数据库连接池
、配置管理
、日志记录
等。单例模式的主要优点
包括防止多次实例化
、节省资源
、提供全局访问点
以及减少全局状态的复杂性
。然而,它也有一些缺点
,比如可能导致代码难以测试和维护
,以及在多线程环境中
可能存在线程安全问题
。
单例模式的实现通常包括将构造方法
设置为私有
,以防止外部代码创建实例
。它还可能包括
一个私有的静态实例成员
,以及一个公共的静态方法
用于获取这个实例
。此外,双重检查锁定
(double-checked locking)是一种常用的实现线程安全单例
的方法。
示例
:
//懒汉式,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
//懒汉式 线程安全
public class Singleton {
private static Singleton instance;
public Singleton() {
}
public static synchronized Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
//饿汉式 线程安全
public class Singleton {
private static Singleton instance=new Singleton();
public Singleton() {
}
public static Singleton getInstance(){
return instance;
}
}
//双检锁/双重校验锁(DCL,即 double-checked locking)
public class Singleton {
private volatile static Singleton singleton;
public Singleton() {
}
public static Singleton getSingleton(){
if (singleton==null){
synchronized (Singleton.class){
if (singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
//登记式/静态内部类
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTANCE=new Singleton();
}
public Singleton() {
}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
//枚举
public enum Singleton {
INSTANCE;
public void test(){
System.out.println("hi~");
}
}
//CAS「AtomicReference」(线程安全)
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {
}
public static final Singleton getInstance() {
for (; ; ) {
Singleton instance = INSTANCE.get();
if (null != instance) return instance;
INSTANCE.compareAndSet(null, new Singleton());
return INSTANCE.get();
}
}
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
}
}
注
:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式
增加:CAS「AtomicReference」(线程安全)
java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference。
AtomicReference 可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样的一个特点。
使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。