单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
所有的单例模式都具有
1、私有的构造函数,防止在外部被new出。
2、私有的静态成员变量,类型为本类 。
3、公开的一个静态方法,作为新的构造函数,它往往会创建一个对象并保存在静态成员变量里。被称为全局访问点
这是一个单例模式,特点是变量已经初始化,无论是否使用都存在。称之为饿汉式
public class Hungry {
private Hungry(){
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
这是一个单例模式,特点是只有被使用时才会调用getInstance()为变量分配内存空间并初始化。称之为懒汉式
public class LazyMan {
private static LazyMan lazyMan = null;
private LazyMan(){
}
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
getInstance()
getInstance() 是一个在面向对象编程中常见的方法,尤其是在实现单例设计模式时。单例设计模式的目的是控制类的实例化过程,确保在应用程序运行期间某个类只有一个实例存在,并提供一个全局访问点来获取这个实例。
getInstance() 方法通常具有以下特点:
1、静态方法:因为我们需要在不创建类的实例的情况下调用它,所以 getInstance() 定义为类的静态方法。
2、返回类型:该方法的返回类型是它所属的类类型,即返回的是该类的一个实例。
3、确保唯一性:在方法内部,它会检查是否已经存在该类的一个实例。如果是第一次调用,它会创建一个新的实例并记住这个实例(通常通过类的静态变量)。如果之前已经创建过实例,那么它就直接返回之前创建的那个实例,确保了全局只存在一个实例。
static
当static修饰时,其用途都体现了它让成员成为类级别、而非对象级别(使用时不用new而是用类名.的方式)的特性:
1、静态变量
- 当一个变量需要被所有实例共享,即无论创建多少个对象,该变量都只有一份副本时,应使用 static。例如,一个记录对象创建计数的变量。
- 静态变量属于类本身,不是某个对象的属性,可以通过类名直接访问,无需创建类的实例
2、静态方法
- 当一个方法不操作任何实例变量,即它的执行不依赖于类的实例状态时,应使用 static。这类方法通常用于工具方法或者工厂方法。
- 静态方法同样可以直接通过类名调用,无需实例化对象。
- 注意,静态方法内不能直接访问非静态成员变量或非静态方法,因为这些成员属于对象,而静态方法在没有具体对象的上下文中执行。
在上述代码中,结合getInstance()和static,不难看出,在单例模式中我们会把new出对象的这一操作彻底的对外封闭,只在类的内部进行,而且在类内部会保持new出的对象在内存中的唯一性。
在多线程中
看下面的代码
public class LazyMan {
private static LazyMan lazyMan = null;
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"创建LazyMan");
}
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
//多线程测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
结果如下,多个线程都new出了对象,破坏了单例
针对此问题,使用 synchronized (LazyMan.class)
进行加锁,确保线程唯一,称之为双重锁定模式(DCL,Double Check Lock)
public static LazyMan getInstance(){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
return lazyMan;
}
但是
因为lazyMan = new LazyMan()
这一操作并不是原子性的,过程如下
1、分配对象内存空间
2、执行构造方法初始化对象
3、把对象指向这个空间
在多线程环境中,为了优化性能,编译器和处理器可能会对指令进行重排序,这在某些情况下可能导致程序的行为不符合预期。
也就是说当线程A执行完lazyMan = new LazyMan()
后,在处理器中,原本123的步骤被优化为了132
1、分配对象内存空间
3、把对象指向这个空间lazyMan!=null
此时线程B来了,因为lazyMan!=null
,返回lazyMan,但是还没初始化,类内部的属性可能为null,会出现空指针
2、执行构造方法初始化对象
因此,需要禁止指令重排,即使用 volatile 修饰的变量
// 添加 volatile 关键字
private volatile static LazyMan lazyMan=null;
public static LazyMan getInstance(){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan();
}
}
return lazyMan;
}
volatile
在Java中,volatile 关键字主要用于修饰变量,它确保了多线程环境下的可见性和有序性,但不保证原子性。以下是使用 volatile 的典型场景:
1、可见性:当一个线程修改了某个变量的值,其他线程能够立即看到修改后的最新值,而不是各自缓存的旧值。这对于状态标记(比如线程中断标志、是否停止的标志)非常有用,例如:
private volatile boolean stop = false;
public void requestStop() {
stop = true;
}
public void workerThread() {
while (!stop) {
// 执行任务...
}
}
在上面的例子中,volatile 确保了当其他线程调用 requestStop() 方法修改 stop 标记后,正在执行循环的线程能够立即感知并退出循环。
2、禁止指令重排序:如上述
静态内部类
可以看到,多了一个新类InnerClass,类型为私有静态。
当外部类被加载时,内部类不需要被立即加载,因此也不会去new(初始化)故不会占用内存。
只有当第一次调用getInstance()时才会使虚拟机加载InnerClass,顺势的new出OuterClass 。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
public class OuterClass {
private OuterClass(){}
private static class InnerClass{
private static final OuterClass STATIC_INNER = new OuterClass();
}
private static OuterClass getInstance(){
return InnerClass.STATIC_INNER;
}
}
参考:https://blog.csdn.net/mnb65482/article/details/80458571,其详细解释了此方法如何保程安全
枚举
枚举类型:被 enum 关键字修饰
其特性也能实现单例模式
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}