在我们日常的开发工作中,有时候会遇到这样的情况:某个类只需要创建一个实例,这个实例的生命周期贯穿整个程序,共享资源。这时候就需要用到我们今天的主角——单例模式。单例模式不仅可以提高代码的可读性和可维护性,还可以在一定程度上提高系统的性能。那么,接下来让我们一起详细了解一下单例模式吧!
简介
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。单例模式指的是一个类只能实例化一次,并提供全局访问点来访问该实例。在Java编程中,单例模式非常有用,因为它可以确保类只有一个实例,并且该实例可以被其他对象访问,而不需要再创建新的对象。
设计原理
- 拥有静态的、私有的(自身类的)成员变量
- 将构造方法私有化,以避免在类外部创建对象
- 提供静态的public方法用于返回全局唯一的实例
优点
- 节省系统资源。由于单例模式创建的对象只有一个,因此可以避免创建多个实例对象所带来的资源消耗,提高了系统的性能和效率。
- 实现了全局唯一性,避免了多个实例对象之间相互影响的问题。
缺点
- 违反了单一职责原则,因为单例模式把两个职责合并在了一起,即实现了单例模式,同时还要负责自身的业务逻辑。
- 单例模式的单例对象的生命周期往往是整个应用程序的生命周期,如果单例对象需要释放资源,需要格外小心处理,避免影响整个应用程序。
应用场景
- 系统中只需要存在一个实例的对象,例如全局配置信息、数据库连接池、日志管理器等。
- 对象需要被频繁创建和销毁的场景,例如线程池、缓存管理器等。
- 需要控制实例数量的场景,例如计数器、信号量等。
实现方式
1. 懒汉式
延迟加载(懒加载),线程不安全
如要保证线程安全,可在方法上加锁synchronized,但是影响调用效率
public class SingletonDemo1 {
private static SingletonDemo1 instance;
private SingletonDemo2(){
}
public static SingletonDemo1 getInstance(){
if(instance==null){
instance=new SingletonDemo1();
}
return instance;
}
}
如果要验证是否为单例,只需要在main方法中使用equals方法进行比较即可
public static void main(String[] args) {
SingletonDemo1 s1=SingletonDemo1.getInstance();
SingletonDemo1 s2=SingletonDemo1.getInstance();
System.out.println(s1.equals(s2));
}
2. 饿汉式
不能延迟加载,天生线程安全
,但资源利用率低。其所谓的饿汉就是说,在类加载的时候就把实例创建好了,不管用不用,都会一直存在于内存,如果不用就有点浪费资源。
public class SingletonDemo2{
private static SingletonDemo2 instance = new SingletonDemo2();
private SingletonDemo2(){}
public static SingletonDemo2 getInstance(){
return instance;
}
}
3. 静态内部类实现
延迟加载,线程安全
。这种方式可以避免synchronized关键字带来的性能开销,也是一种推荐的使用方式。
延迟加载:静态内部类只有在第一次被使用时才会被加载和初始化,而不是在外部类加载的时候初始化。只有在第一次调用 getInstance() 方法时,才会加载 Inner 类并创建单例对象,所以也是一种懒加载的方式。
类加载的机制保证了线程安全性:虚拟机在加载类的时候会保证线程安全,即多个线程同时调用 getInstance() 方法时,只有一个线程能够获取到类加载锁并加载 Inner 类,其他线程需要等待。在 Inner 类被加载和初始化时,会创建 SingletonDemo3的唯一实例,从而保证了线程安全性。
public class SingletonDemo3 {
private static class Inner {
private static final SingletonDemo3 instance = new SingletonDemo3();
}
private SingletonDemo3(){
}
public static SingletonDemo3 getInstance(){
return Inner.instance;
}
}
4. 双重检查锁(重点)
线程安全,延迟加载
。这种方式采用双重检查锁机制,保证线程安全情况下能保持高性能。“双重检查锁”:即两次非空检查,一个代码块锁
重点说明
1,为什么要进行双重检查
?
第一个非空检查是为了保证效率,如果对象已经实例化,那么直接返回就行了,没必要再进入同步代码块。
第二个非空检查放在了同步代码块里面,也是必须存在的,避免多个线程同时创建对象的情况发生。如在多线程环境下,两个线程可能同时通过第一层非空检查,然后排队进入同步代码块,其中一个线程先获得锁,在创建对象后释放锁,另一个线程获得锁后,如果不检验,会再次创建一个对象。
2,为什么要使用volatile
关键字?
volatile关键字此处的作用:在创建对象时
禁止指令重排序
,避免多线程环境下发生错误访问。原理:
instance = new Singleton()创建实例的操作,并不是一个原子操作,它由三条指令组成,非顺序执行,这三条指令在jvm中会进行指令重排序。
- 分配对象内存
- 调用构造器方法,执行初始化
- 将对象引用赋值给变量。
当一个线程进入到同步代码块中,正在进行实例化操作,此时分为三步执行,如果先执行的是1、3步,此时另一个线程乱入,在第一层非空检查时会得到对象已存在这个结果,但此时对象实际并未被实例化,如果访问对象就会引发错误。所以需要使用volatile关键字禁止指令重排。
代码实现
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
5. 枚举
不能延迟加载,线程安全
这种方式是《Effective Java》作者提倡的方式。最简洁的一种实现方式,提供了序列化机制,保证线程安全,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候,只是在工作中比较少见。
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("hello world");
}
}
public class Test {
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
总结
在本文中,我们详细介绍了Java单例模式的设计原理、几种典型的实现方式以及优缺点分析。单例模式是一种非常实用且常用的设计模式,它可以帮助我们提高代码的可读性和可维护性,节省系统资源,提高性能。
在实际的开发过程中,我们应该根据具体的场景选择合适的单例模式实现方式。同时,我们要时刻关注单例模式的优缺点,确保在使用的过程中避免潜在的问题。
希望这篇文章能够帮助你更好地理解和掌握Java单例模式,为你的编程之旅带来启示。如果你觉得本文对你有所帮助,欢迎分享给更多的小伙伴们!记得关注我的博客,我们下次再见!