所有分类
  • 所有分类
  • 未分类

单例模式Java实战–6种写法

简介

说明

本文用示例介绍Java的单例模式的写法。有如下六种写法:懒汉式,饿汉式,静态内部类,双重校验锁,枚举,非synchronized的加锁。

本文所述的单例模式都是线程安全的。线程不安全的单例模式,不是合格的单例模式。

在下边的单例模式中,我比较喜欢静态内部类。如果涉及到反序列化创建对象我会使用枚举的方式。我永远不会使用饿汉式,如果有其他特殊的需求,我可能会使用双重校验锁。

什么是单例模式?

单例模式(Singleton Pattern)用于确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。

单例模式适用于以下情况

  1. 一个类只需要一个实例,例如:工具类等。
  2. 控制资源的使用,避免多次创建对象造成资源浪费。
  3. 想把实例作为全局访问点,方便其他地方访问。

在 Java 中,实现单例模式的关键在于:

  1. 将类的构造方法私有化,防止外部直接实例化该类。
  2. 提供一个静态方法来获取该类的唯一实例,并确保在整个应用程序中只有一个实例存在。
  3. 要保证线程安全。

第一种 懒汉

简介

懒汉模式就是,它很懒,直到用到的时候才会去创建对象,而不是一开始就创建对象。

特点

  1. 支持多线程
  2. 支持懒加载
  3. 性能很低
    1. 因为是加锁同步。

实例 

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

第二种 饿汉

简介

饿汉模式就是,它很饿,想快点创建对象,此模式在类加载时就立即创建对象。

特点

  • 支持多线程
    • 这种方式基于classloder机制保证初始化instance时只有一个线程。
  • 不完全支持懒加载
    • instance在类装载时就实例化,大多数都是调用getInstance方法。但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
  • 性能很高(因为使用时不需要加锁同步)。

法1

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

    private Singleton() {
    }

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

法2

表面上看起来差别挺大,其实跟法1差不多,都是在类初始化即实例化instance。

public class Singleton {
    private static Singleton instance = null;

    static {
        instance = new Singleton();
    }

    private Singleton() {
    }

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

第三种 静态内部类

简介

静态内部类的单例写法是:在类内部有一个静态内部类,它持有外部类的实例。

特点

  • 支持多线程
    • 同样基于classloder机制保证初始化instance时只有一个线程。
  • 支持懒加载
    • Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance。
  • 性能很高
    • 因为使用时不需要加锁同步。

实例 

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

    private Singleton() {
    }

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

第四种 双重校验锁

简介

说明

此模式用了两次if判断。

特点

  • 是懒汉模式的升级版。
  • 在JDK1.5之后,双重检查锁定才能够正常达到单例效果。
    • 原因: Java 5 以前的 JMM (Java 内存模型)存在缺陷,即使将变量声明成 volatile 也不能完全避免重排序。

实例 

public class Singleton {
    private static volatile Singleton singleton;

    private Singleton() {
    }

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

为什么加volatile关键字?

volatile作用:保证有序性、可见性。

有序性

Singleton singleton = new Singleton() 这句话可以分为三步:

  1. 为 Singleton 分配内存空间(加载、链接);
  2. 初始化 singleton;
  3. 将 singleton 指向分配的内存空间。

但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。所以,使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。

可见性

把变量声明为 volatile,就指示 JVM,修改的值立即被更新到主存。

普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

为什么有两次校验? 

第一次校验:也就是第一个if(singleton == null)

这个是为了代码提高代码执行效率。由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

第二次校验:也就是第二个if(singleton == null)

这个校验是防止二次创建实例。假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton ==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

所以说:两次校验都必不可少。

第五种 枚举

简介

Effective Java作者Josh Bloch 提倡的方式。

关于破坏单例,见:Java单例模式–破坏单例的方法 – 自学精灵

实例 

public enum Singleton {
    INSTANCE;
}

简单示例

package org.example.a;

enum MyEnum{
    FIRST("第一个"),
    SECOND("第二个");

    private String desc;
    private MyEnum(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    private String lastName;

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}

public class Demo {
    public static void main(String[] args) {
        MyEnum.FIRST.setLastName("Tony");
        System.out.println(MyEnum.FIRST.getLastName());
        System.out.println(MyEnum.FIRST.getDesc());
        MyEnum myEnum = MyEnum.FIRST;
        MyEnum myEnum1 = MyEnum.FIRST;
        System.out.println(myEnum == myEnum1);
    }
}

执行结果

Tony
第一个
true

第六种 非synchronized (不常用)

CAS

package org.example.a;

import java.util.concurrent.atomic.AtomicReference;

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = 
            new AtomicReference<Singleton>();

    private Singleton() {
    }

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

ThreadLocal

package org.example.a;

public class Singleton {
    private static final ThreadLocal<Singleton> singleton =
            new ThreadLocal<Singleton>() {
                @Override
                protected Singleton initialValue() {
                    return new Singleton();
                }
            };

    public static Singleton getInstance() {
        return singleton.get();
    }

    private Singleton() {
    }
}

6

评论2

请先

  1. 静态内部类:它跟懒汉式不同的是(很细微的差别):懒汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果)。 这应该是饿汉式把
    珠光2023 2023-11-28 0
    • 是的,这里之前是手误写错了,我把这一行去掉了,算是个冗余的陈述。
      自学精灵 2023-11-28 0
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录