Java单例模式是一种常用的创建型设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。在Java中,有多种方式可以实现单例模式,下面详细介绍其中的几种常见实现方式。

1什么是创建型设计模式?

处理对象创建的模式,简答的理解就是如何创建对象?很多人都会想到new关键字,一般的项目你到处new是不会有问题的,但是稍微复杂一点的项目,可能需要考虑到对象的单一性、复用性和可扩展性。

 为什么要有单例模式?

单例模式可以提供一种简单而有效的方式来管理和访问全局资源,同时也可以提高系统的性能和可维护性

举一个案例,如果你需要一个全局的缓存(现在好多大数量级的都采用redis),那么这个缓存就只能存在一个实例对象中,理解下。当然总结下来:

  1. 节省资源:由于单例模式只创建一个实例,可以节省系统资源,特别是在需要频繁创建和销毁对象的情况下。

  2. 全局访问点:单例模式提供了一个全局访问点,可以方便地访问实例,避免了在不同的地方重复创建实例的问题。

  3. 数据共享:由于单例模式只有一个实例,可以方便地在不同的对象之间共享数据。

  4. 线程安全:单例模式可以确保在多线程环境下只有一个实例被创建,避免了多线程竞争的问题。

2.常见的单例模式

2.1 饿汉式(Eager Initialization)

在这种方式下,单例实例在类加载时就被创建,因此在多线程环境下也能保证只有一个实例。这种方式的缺点是,如果该实例一直没有被使用,会造成内存浪费。

如果看不懂下面代码,就复习下static和final关键字的作用,下面的get方法可有可无,有是因为满足java的封装性,实例变量都是私有静态的,对外提供public的get访问方法。

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

    private Singleton() {}

    //下面相当于是一个get方法,符合封装性
    public static Singleton getInstance() {
        return instance;
    }
}
 

我们复习下static和final关键字:

static关键字用于修饰类的成员(字段、方法、代码块和内部类),表示它们属于类本身,而不是类的实例。所以static不可以用在类方法论里面。下面四条要记住:

  1. 静态字段(static fields):静态字段是属于类的字段,而不是属于类的实例。它们在类加载时被初始化,并且在整个程序运行期间保持不变。可以通过类名直接访问静态字段,而不需要创建类的实例
  2. 静态方法(static methods):静态方法是属于类的方法,而不是属于类的实例。它们可以直接通过类名调用,而不需要创建类的实例。静态方法不能访问非静态的成员变量和方法,只能访问静态的成员变量和方法(这个可以这么理解:静态方法不需要创建对象就可以访问,属于类,非静态方法属于类的实例对象,所以静态方法不能直接访问非静态的,因为对象是类的具体化,静态是类的固有属性)
  3. 静态代码块(static blocks):静态代码块是在类加载时执行的一段代码。它们用于初始化静态字段或执行其他静态操作。静态代码块只会执行一次,并且在类加载时按照它们在代码中的顺序执行。
  4. 静态内部类(static inner classes):静态内部类是定义在类内部的类,但是它是静态的,不依赖于外部类的实例。静态内部类可以直接通过外部类名访问,而不需要创建外部类的实例。

使用static关键字的一些常见用途包括:

  • 创建工具类:将一些通用的方法和字段定义为静态的,可以直接通过类名调用,而不需要创建实例。
  • 计数器:使用静态字段来记录某个类的实例数量。
  • 常量定义:将一些常量定义为静态字段,可以在整个程序中共享使用。
  • 单例模式:使用静态方法和静态字段来实现单例模式,确保只有一个实例被创建。

需要注意的是,静态成员属于类本身,而不是类的实例。因此,它们在内存中只有一份拷贝,并且可以被所有实例共享。同时,静态成员的生命周期与类的生命周期相同,即在类加载时初始化,在程序结束时销毁。

关键字final用于修饰类、方法和变量,具有不同的含义和用法。

  1. final修饰类:当一个类被声明为final时,表示该类不能被继承。这意味着其他类不能扩展或继承该final类。
  2. final修饰方法:当一个方法被声明为final时,表示该方法不能被子类重写或覆盖。final方法在父类中已经实现了最终的功能,子类不能对其进行修改。
  3. final修饰变量:当一个变量被声明为final时,表示该变量的值不能被修改。final变量必须在声明时进行初始化,并且一旦初始化后,其值就不能再被修改。

final变量可以是基本数据类型,也可以是引用类型。对于基本数据类型的final变量,其值不能被修改。对于引用类型的final变量,其引用不能被修改,但是引用所指向的对象的内容可以被修改。

在Java中,final关键字用于声明一个常量,它可以应用于基本数据类型和引用类型。

对于引用类型的final变量,它的引用不能被修改,意味着一旦引用被赋值后,就不能再指向其他对象。这是因为final变量的引用在内存中是不可变的。

然而,虽然引用不能被修改,但是引用所指向的对象的内容可以被修改。这意味着通过final变量的引用,我们可以修改对象的属性或调用对象的方法,只要这些属性或方法不是被声明为final的。

例如,考虑以下代码片段:

final StringBuilder sb = new StringBuilder("Hello");

sb.append(" World");

sb是一个final变量,它的引用不能被修改。但是,我们可以通过sb引用来修改StringBuilder对象的内容,将" World"追加到字符串中。

总结起来,对于引用类型的final变量,它的引用不能被修改,但是引用所指向的对象的内容可以被修改。这样的设计可以保护引用不被误修改,同时允许对对象的内容进行修改。

public static void main(String[] args) {
		final StringBuilder s = new StringBuilder();
		StringBuilder b = new StringBuilder();
		StringBuilder c = new StringBuilder();
		b = c;
		s = b;
		/**
		 * The final local variable s cannot be assigned. It must be blank 
         *  and not using a compound assignment
		 */

	}

final变量在多线程环境下具有线程安全的特性,因为它的值不会被修改。

总结:

  • final修饰类:表示该类不能被继承。
  • final修饰方法:表示该方法不能被重写。
  • final修饰变量:表示该变量的值不能被修改。

2.2懒汉式(Lazy Initialization)

在这种方式下,单例实例在第一次使用时才被创建,以实现延迟加载。这种方式的缺点是,在多线程环境下可能会创建多个实例,需要通过加锁来解决。

下面有的同学就有看不懂了,我来复习下:

java声明一个对象变量(private static Singleton instance;)只是为该变量分配了内存空间,但没有实际创建对象。要创建对象实例,需要使用"new"关键字来调用类的构造方法。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

synchronized

synchronized关键字用于修饰方法或代码块,其作用是实现线程的同步,保证多个线程在访问共享资源时的互斥性和可见性。

当一个方法被synchronized修饰时,同一时刻只能有一个线程执行该方法,其他线程需要等待。这样可以避免多个线程同时访问共享资源导致的数据不一致或错误的问题。

具体来说,synchronized修饰方法的作用有以下几点:

  1. 互斥性:当一个线程进入synchronized修饰的方法时,其他线程需要等待,直到该线程执行完毕释放锁。这样可以保证同一时刻只有一个线程执行该方法,避免了多个线程同时修改共享资源导致的数据不一致问题。
  2. 可见性:synchronized修饰的方法会将修改共享资源的操作立即刷新到主内存中,其他线程可以立即看到最新的值。这样可以保证多个线程之间对共享资源的操作是可见的,避免了数据的脏读、写入和丢失等问题。

需要注意的是,synchronized修饰方法时,锁的粒度是整个方法,即一个线程获取了该方法的锁,其他线程需要等待该线程执行完整个方法才能获取锁。如果方法中只有一小部分代码需要同步,可以考虑使用synchronized代码块来实现更细粒度的锁定。

2.3 双重检查锁(Double-Checked Locking)

这种方式结合了饿汉式和懒汉式的优点,既实现了延迟加载,又保证了线程安全。在第一次创建实例时使用同步锁,以避免多线程环境下创建多个实例。

这种方式其实有点偏底层了,我们知道怎么写就可以。

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile

volatile关键字用于修饰变量,用来保证变量的可见性和禁止指令重排序。

  1. 可见性:当一个变量被volatile修饰时,任何对该变量的修改都会立即被其他线程可见,即保证了线程之间的通信。这是因为volatile修饰的变量会被存储在主内存中,每次使用前都会从主内存中读取最新的值。
  2. 禁止指令重排序:在多线程环境下,为了提高性能,编译器和处理器可能会对指令进行重排序。然而,有些指令重排序可能会导致多线程程序出现问题。使用volatile关键字修饰的变量,可以禁止指令重排序,保证了程序的正确性。

需要注意的是,volatile关键字只能保证可见性和禁止指令重排序,并不能保证原子性。如果需要保证原子性,可以使用synchronized关键字或者使用原子类(如AtomicInteger)。

总结起来,volatile关键字在多线程编程中起到了重要的作用,可以保证变量的可见性和禁止指令重排序,从而保证了线程之间的正确通信。

2.4 静态内部类(Static Inner Class)

这种方式利用了Java类加载机制的特性,当第一次访问内部类时才会加载并初始化单例实例。这种方式既实现了延迟加载,又保证了线程安全。其实不建议写内部类,看似很装逼,实际很繁琐。

这种模式比较常见。

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

3.应用场景

单例模式适用于以下场景:

  1. 当一个类只需要一个实例时,可以使用单例模式。例如,数据库连接池、线程池等资源管理类只需要一个实例来管理资源。
  2. 当需要控制某个资源的访问权限时,可以使用单例模式。例如,配置文件读取类只需要一个实例来读取配置文件,并且避免多个实例同时修改配置文件。
  3. 当需要节省系统资源时,可以使用单例模式。例如,某个类的创建和销毁需要消耗大量资源,使用单例模式可以避免频繁创建和销毁实例,提高系统性能。
  4. 当需要全局访问某个对象时,可以使用单例模式。例如,日志记录类、缓存管理类等需要在整个系统中被访问的对象可以使用单例模式。

需要注意的是,单例模式可能会引起全局状态的共享和修改,因此在使用时需要谨慎考虑其对系统的影响。