创建型-单例模式

创建单例

关键点:
  • 构造函数必须是private访问权限,避免外部直接new一个对象;
  • 需要考虑创建时的线程安全问题;
  • 获取单例对象时的性能是否高(是否加锁);
  • 是否支持延迟加载;
实现方式:
饿汉式:

饿汉式是在类加载时就创建了对象,所以创建对象的过程是线程安全的。这种实现方式有这些特点:

  • 不支持延迟加载,初始状态就加载了对象;

  • 没有线程安全问题;

  • 不需要加锁;

饿汉式的示例代码如下:

public class Logger {
    private static final Logger instance = new Logger();
  
    private Logger() {};

    public static Logger getInstance() {
        return intance;
    }
}
懒汉式:

懒汉式是给单例类加锁,支持延迟加载,但是在频繁使用、并发高的场景下会出现性能瓶颈,它有这些特点:

  • 支持延迟加载;
  • 没有线程安全问题;
  • 将锁加在了对象上,导致每次只能串行获取对象,性能不高;

懒汉模式的样例代码如下:

public class Logger {
    private static Logger instance;
  
    private Logger() {};

    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return intance;
    }
}
双重检测:

双重检查是一种既支持延迟加载、又支持高并发的单例实现方式,它的特点有:

  • 支持延迟加载,没有线程安全问题;
  • 需要加锁,将锁加在getInstance()方法内部;

示例代码如下:

public class Logger {
    private static Logger instance;
  
    private Logger() {};

    public static Logger getInstance() {
        if (instance == null) {
            synchronized(Logger.class) { // 双重检测
                if (instance == null) {
                    instance = new Logger();
                }
            }
        }
        return intance;
    }
}
静态内部类:

通过静态内部类来创建单例是一种相对安全的实现方式。在外部类被加载时、并不会立即加载静态内部类,当通过getInstance()方法初次加载静态内部类时,JVM保证只有一个线程对这个类进行初始化,因此这种方式既能实现懒加载、又能保证线程安全。它有这些特点:

  • 支持延迟加载;
  • 没有线程安全问题;
  • 不需要加锁;

示例代码如下:

public class Logger {
    private Logger() {};

    // 静态内部类
    private static class SingletonHolder {
        private static final Logger instance = new Logger();
    }
    
    public static Logger getInstance() {
        return SingletonHolder.instance;
    }
}
枚举类:

通过Java枚举类本身的特性、即JVM保证枚举对象的唯一性,来保证实例创建的线程安全性和实例的唯一性。它具有以下特性:

  • 不支持延迟加载;

  • 没有线程安全问题;

  • 不需要加锁;

枚举类实现方式的示例如下:

public enum Logger {
    INSTANCE;
}

使用单例

单例使用场景

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似 IdGenerator.getInstance().getId() 这样的方法来调用就可以了。

单例模式存在的问题
  • 单例对OOP特性的支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数(可以实现,但是很麻烦)
单例模式的使用范围

单例即唯一,但是这个唯一是相对的,我们通常说的单例,是指在Java进程中唯一,但是在部分场景下,需要实现类唯一、线程唯一、集群唯一等相对的唯一。

进程唯一

进程唯一是我们通常所说的单例。我们编写的代码,通过编译、链接、组织在一起,构成了一个操作系统可执行的文件;在进程启动后,会开辟独立的内存空间,用于保存程序+数据。线程间是共享进程里的内存地址空间,因此单例类在进程中只能生成一个对象、单例对象在线程间也是唯一的。

线程唯一

线程唯一是指单例对象在线程内是唯一的,但是在进程内(线程间)不唯一。在实现的时候需要用hash表保存已经初始化了的单例对象;Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例

public class IdGenerator{
    private AtomicLong id = new AtomicLong(0);
    
    private static final ConcurrentHashMap<Long, Logger> instances = new ConcurrentHashMap<>();
    
    private IdGenerator() {}
    
    public static IdGenerator getInstance() {
        Long currentThreadId = Thread.currentThread().getId();
        instances.putIfAbsent(currentThreadId, new IdGenerator());
        return instances.get(currentThreadId);
    }
    
    public long getId() {
        return id.incrementAndGet();
    }
}
集群唯一

我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。

多例模式

多例”指的是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象、或者每个指定的类里只能创建一个单例对象(Logger对象)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值