java单例模式五种实现方式

文章目录

  • 应用场景
  • 实现要点
  • 实现方式
    • 饿汉式
      • 存在问题
    • 静态内部类
    • 枚举方法 实现单例
    • 懒汉式 实现单例
      • 懒汉式,线程不安全的实现
      • 懒汉式,线程安全但效率低下的实现 ,synchronized 静态同步方法
      • 加双重检查机制
      • 禁止指令重排
      • 懒汉式 最佳方案(基于 synchronized +volatile 实现)
    • Java并发包中的Lock实现 单例模式
    • 基于CAS 实现单例
      • 基于CAS实现单例的优缺点
  • 单例模式的破坏与防御
    • 单例模式的破坏
      • 反射方式获取多个单例实例
      • 序列化 获取单例实例
    • 防御
  • 单例模式性能总结

应用场景

由于单例模式只生成一个实例, 减少了系统性能开销(如: 当一个对象的产生需要比较多的资源时, 如读取配置, 产生其他依赖对象, 则可以通过在应用启动时直接产生一个单例对象, 然后永久驻留内存的方式来解决)

  • Windows中的任务管理器;
  • 文件系统, 一个操作系统只能有一个文件系统;
  • 数据库连接池的设计与实现;
  • Spring中, 一个Component就只有一个实例Java-Web中, 一个Servlet类只有一个实例;

实现要点

  • 声明为private来隐藏构造器
  • private static Singleton实例
  • 声明为public来暴露实例获取方法

单例模式主要追求三个方面性能

  • 线程安全
  • 调用效率高
  • 延迟加载

实现方式

主要有五种实现方式,懒汉式(延迟加载,使用时初始化),饿汉式(声明时初始化),双重检查,静态内部类,枚举。

饿汉式

这种方式基于类ClassLoder机制,使用static来定义静态成员变量或静态代码,让instance在类加载时就进行初始化;避免了同步问题,实现线程安全。饿汉式的优势在于实现简单,劣势在于不是懒加载模式(lazy initialization)

存在问题

  • 在需要实例之前就完成了初始化,在系统中单例场景较多的情况下,会造成内存占用,加载速度慢问题
  • 由于在调用getInstance()之前就完成了初始化,如果需要给getInstance()函数传入参数,将会无法实现
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {
    };
    public static Singleton getInstance() {
        return instance;
    }
}  
//static 静态代码块
public class Singleton {
    private Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return this.instance;
    }
}

静态内部类

由于内部类不会在类的外部被使用,所以只有在调用getInstance()方法时JVM才会被加载静态内部类。同时依赖JVM的 ClassLoader 类加载机制保证了不会出现同步问题。

public class Singleton {
    private Singleton() {
    };
    public static Singleton getInstance() {
        return Holder.instance;
    }
    //静态内部类实现创建单例对象
    private static class Holder{
        private static Singleton instance = new Singleton();
    }
}  

枚举方法 实现单例

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒。

  • 线程安全

由于枚举类的会在编译期编译为继承自java.lang.Enum的类,其构造函数为私有,不能再创建枚举对象,枚举对象的声明和初始化都是在static块中,所以由JVM的ClassLoader机制保证了线程的安全性。但是不能实现延迟加载

  • 序列化

由于枚举类型采用了特殊的序列化方法,从而保证了在一个JVM中只能有一个实例。

枚举类的实例都是static的,且存在于一个数组中,可以用values()方法获取该数组
在序列化时,只输出代表枚举类型的名字属性 name
反序列化时,根据名字在静态的数组中查找对应的枚举对象,由于没有创建新的对象,因而保证了一个JVM中只有一个对象

public enum Singleton {
    INSTANCE;
    public String error(){
        return "error";
    }
} 

饿汉式,静态内部类,枚举类型方式实现单例模式 原理都是利用借助了类加载的时候初始化单例。即借助了ClassLoader的线程安全机制。这几种实现方式,虽然并没有显示的使用synchronized,但是还是其底层实现原理还是用到了synchronized。

所谓ClassLoader的线程安全机制,就是ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字,同步了多线程下的创建实例形式。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的,也就是保证了线程安全。

懒汉式 实现单例

懒汉式,线程不安全的实现

由于没有同步,多个线程可能同时检测到实例没有初始化而分别初始化,从而破坏单例约束。

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

懒汉式,线程安全但效率低下的实现 ,synchronized 静态同步方法

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

由于对象只需要在初次初始化时需要同步,多数情况下不需要互斥的获得对象,加锁会造成巨大无意义的资源消耗

加双重检查机制

这种方法对比于上面的方法确保了只有在初始化的时候需要同步,当初始化完成后,再次调用getInstance不会再进入synchronized块。 内部检查是必要的,由于在同步块外的if语句中可能有多个线程同时检测到instance为null,同时想要获取锁,所以在进入同步块后还需要再判断是否为null,避免因为后续获得锁的线程再次对instance进行初始化。

在DCL双重检查情况下 ,instance声明为volatile类型是必要的。

禁止指令重排

由于初始化操作 instance=new Singleton()是非原子操作的,主要包含三个过程

  • 给instance分配内存
  • 调用构造函数初始化instance
  • 将instance指向分配的空间(instance指向分配空间后,instance就不为空了)

虽然synchronized块保证了只有一个线程进入同步块,但是在同步块内部JVM出于优化需要可能进行指令重排,例如(1->3->2),instance还没有初始化之前其他线程就会在外部检查到instance不为null,而返回还没有初始化的instance,从而造成逻辑错误。

volatile保证变量的可见性

volatile类型变量可以保证写入对于读取的可见性,JVM不会将volatile变量上的操作与其他内存操作一起重新排序,volatile变量不会被缓存在寄存器,因此保证了检测instance状态时总是检测到instance的最新状态。

注意:volatile并不保证操作的原子性,例如即使count声明为volatile类型,count++操作被分解为读取->写入两个操作,虽然读取到的是count的最新值,但并不能保证读取与写入之间不会有其他线程再次写入,从而造成逻辑错误.

懒汉式 最佳方案(基于 synchronized +volatile 实现)

// 最终的代码是这样的,懒汉式 能保证线程安全和高效;基于 synchronized  +volatile  实现
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
    };
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}  

Java并发包中的Lock实现 单例模式

package org.vincent.singleton;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author PengRong
 * @package org.vincent.lock
 * @ClassName SingletonByLock.java
 * @date 2019/4/27 - 10:21
 * @ProjectName Multthread-in-action
 * @Description: 基于Lock 实现单例
 */
public class SingletonByLock {
    private static volatile SingletonByLock instance;
    private static ReentrantLock lock = new ReentrantLock();

    private SingletonByLock() {
    }

    public static SingletonByLock getInstance() {
        if (instance == null) {
            try {
                lock.lock();
                /** 如果 instance 未初始化 ,就初始化一下,volatile可以保证可见性 */
                if (instance == null) {
                    instance = new SingletonByLock();
                }
            } finally {
                lock.unlock();
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        SingletonByLock singletonByLock =SingletonByLock.getInstance();
        SingletonByLock singletonByLock1 =SingletonByLock.getInstance();
        SingletonByLock singletonByLock2 =SingletonByLock.getInstance();
        SingletonByLock singletonByLock3 =SingletonByLock.getInstance();
        SingletonByLock singletonByLock4 =SingletonByLock.getInstance();
        if (singletonByLock == singletonByLock1 && singletonByLock1 == singletonByLock2 && singletonByLock2 == singletonByLock3
                && singletonByLock3 == singletonByLock4){
            System.out.println(" 实现单例模式");
        }
    }
}


基于CAS 实现单例

CAS是项乐观锁技术,可以实现 无锁实现单例模式。上面所有实现方式都使用了 锁技术实现单例。

package org.vincent.singleton;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @author PengRong
 * @package org.vincent.singleton
 * @ClassName SingletonByCAS.java
 * @date 2019/4/27 - 10:32
 * @ProjectName Multthread-in-action
 * @Description: 基于CAS 无锁结构实现单例模式
 */
public class SingletonByCAS {
    /** CAS 实现 持有 单例实例的引用,CAS 技术保证原子性,AtomicReference 持有引用的变量使用 volatile 修饰保证 可见性 */
    private static final AtomicReference<SingletonByCAS> INSTANCE = new AtomicReference<>();

    private SingletonByCAS() {}

    public static SingletonByCAS getInstance() {
        for (;;) {
            /** 单例实现了 直接返回*/
            SingletonByCAS singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }
            /** 未创建 单例实例,直接 cas 原子创建 */
            singleton = new SingletonByCAS();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
    public static void main(String[] args) {
        SingletonByCAS singletonByLock =SingletonByCAS.getInstance();
        SingletonByCAS singletonByLock1 =SingletonByCAS.getInstance();
        SingletonByCAS singletonByLock2 =SingletonByCAS.getInstance();
        SingletonByCAS singletonByLock3 =SingletonByCAS.getInstance();
        SingletonByCAS singletonByLock4 =SingletonByCAS.getInstance();
        if (singletonByLock == singletonByLock1 && singletonByLock1 == singletonByLock2 && singletonByLock2 == singletonByLock3
                && singletonByLock3 == singletonByLock4){
            System.out.println(" 实现单例模式");
        }
    }
}

基于CAS实现单例的优缺点

  • 用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
  • CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。

单例模式的破坏与防御

单例模式的破坏

反射方式获取多个单例实例

对于枚举类,该破解方法不适用。

import java.lang.reflect.Constructor;
public class TestCase {
    public void testBreak() throws Exception {
        Class<Singleton> clazz = (Class<Singleton>) Class.forName("Singleton");
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance1 = constructor.newInstance();
        Singleton instance2 = constructor.newInstance();
        System.out.println("singleton? " + (instance1 == instance2));
    }
    public static void main(String[] args) throws Exception{
        new TestCase().testBreak();
    }
}  

序列化 获取单例实例

对于枚举类,该破解方法不适用。

该测试首先需要声明Singleton为实现了可序列化接口

public class Singleton implements Serializable
public class TestCase {
    private static final String SYSTEM_FILE = "save.txt";
    public void testBreak() throws Exception {
        Singleton instance1 = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
        oos.writeObject(instance1);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
        Singleton instance2 = (Singleton) ois.readObject();
        System.out.println("singleton? " + (instance1 == instance2));
    }
    public static void main(String[] args) throws Exception{
        new TestCase().testBreak();
    }
}  

ClassLoader

JVM中存在两种ClassLoader,启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader),在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。

防御

对于序列化与反序列化,我们需要添加一个自定义的反序列化方法,使其不再创建对象而是直接返回已有实例,就可以保证单例模式。

我们再次用下面的类进行测试,就发现结果为true。


public final class Singleton {
    private Singleton() {
    }
    private static final Singleton INSTANCE = new Singleton();
    public static Singleton getInstance() {
        return INSTANCE;
    }
    private Object readResolve() throws ObjectStreamException {
        // instead of the object we're on,
        // return the class variable INSTANCE
        return INSTANCE;
    }
public class TestCase {
    private static final String SYSTEM_FILE = "save.txt";
    public void testBreak() throws Exception {
        Singleton instance1 = Singleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
        oos.writeObject(instance1);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
        Singleton instance2 = (Singleton) ois.readObject();
        System.out.println("singleton? " + (instance1 == instance2));
    }
    public static void main(String[] args) throws Exception {
        new TestCase().testBreak();
    }
}  
}  

单例模式性能总结

方式 优点 缺点
饿汉式 线程安全, 调用效率高 不能延迟加载
懒汉式 线程安全, 可以延迟加载 调用效率不高
双重检测锁式 线程安全, 调用效率高, 可以延迟加载 -
静态内部类式 线程安全, 调用效率高, 可以延迟加载 -
枚举单例 线程安全, 调用效率高 不能延迟加载

单例性能测试

测试结果:

HungerSingleton 共耗时: 30 毫秒
LazySingleton 共耗时: 48 毫秒
DoubleCheckSingleton 共耗时: 25 毫秒
StaticInnerSingleton 共耗时: 16 毫秒
EnumSingleton 共耗时: 6 毫秒 

在不考虑延迟加载的情况下,枚举类型获得了最好的效率,懒汉模式由于每次方法都需要获取锁,所以效率最低,静态内部类与双重检查的效果类似。考虑到枚举可以轻松有效的避免序列化与反射,所以枚举是较好实现单例模式的方法

public class TestCase {
    private static final String SYSTEM_FILE = "save.txt";
    private static final int THREAD_COUNT = 10;
    private static final int CIRCLE_COUNT = 100000;
    public void testSingletonPerformance() throws IOException, InterruptedException {
        final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        FileWriter writer = new FileWriter(new File(SYSTEM_FILE), true);
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; ++i) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < CIRCLE_COUNT; ++i) {
                        Object instance = Singleton.getInstance();
                    }
                    latch.countDown();
                }
            }).start();
        }
        latch.await();
        long end = System.currentTimeMillis();
        writer.append("Singleton 共耗时: " + (end - start) + " 毫秒\n");
        writer.close();
    }
    public static void main(String[] args) throws Exception{
        new TestCase().testSingletonPerformance();
    }
}  


补充知识

类加载机制

static关键字的作用是把类的成员变成类相关,而不是实例相关,static块会在类首次被用到的时候进行加载,不是对象创建时,所以static块具有线程安全性

  • 普通初始化块

当Java创建一个对象时, 系统先为对象的所有实例变量分配内存(前提是该类已经被加载过了), 然后开始对这些实例变量进行初始化, 顺序是: 先执行初始化块或声明实例变量时指定的初始值(这两处执行的顺序与他们在源代码中排列顺序相同), 再执行构造器里指定的初始值.

静态初始化块

又名类初始化块(普通初始化块负责对象初始化, 类初始化块负责对类进行初始化). 静态初始化块是类相关的, 系统将在类初始化阶段静态初始化, 而不是在创建对象时才执行. 因此静态初始化块总是先于普通初始化块执行.

执行顺序

系统在类初始化以及对象初始化时, 不仅会执行本类的初始化块[static/non-static], 而且还会一直上溯到java.lang.Object类, 先执行Object类中的初始化块[static/non-static], 然后执行其父类的, 最后是自己.

顶层类(初始化块, 构造器) -> … -> 父类(初始化块, 构造器) -> 本类(初始化块, 构造器)

小结

static{} 静态初始化块会在类加载过程中执行;

{} 则只是在对象初始化过程中执行, 但先于构造器;

内部类

内部类访问权限

Java 外部类只有两种访问权限:public/default, 而内部类则有四种访问权限:private/default/protected/public. 而且内部类还可以使用static修饰;内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。成员内部类可以看做是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
内部类分为成员内部类与局部内部类, 相对来说成员内部类用途更广泛, 局部内部类用的较少(匿名内部类除外), 成员内部类又分为静态(static)内部类与非静态内部类, 这两种成员内部类同样要遵守static与非static的约束(如static内部类不能访问外部类的非静态成员等)
非静态内部类

非静态内部类在外部类内使用时, 与平时使用的普通类没有太大区别;
Java不允许在非static内部类中定义static成员,除非是static final的常量类型
如果外部类成员变量, 内部类成员变量与内部类中的方法里面的局部变量有重名, 则可通过this, 外部类名.this加以区分.
非静态内部类的成员可以访问外部类的private成员, 但反之不成立, 内部类的成员不被外部类所感知. 如果外部类需要访问内部类中的private成员, 必须显示创建内部类实例, 而且内部类的private权限对外部类也是不起作用的:
静态内部类

使用static修饰内部类, 则该内部类隶属于该外部类本身, 而不属于外部类的某个对象.
由于static的作用, 静态内部类不能访问外部类的实例成员, 而反之不然;
匿名内部类

如果(方法)局部变量需要被匿名内部类访问, 那么该局部变量需要使用final修饰.

枚举

枚举类继承了java.lang.Enum, 而不是Object, 因此枚举不能显示继承其他类; 其中Enum实现了Serializable和Comparable接口(implements Comparable, Serializable);
非抽象的枚举类默认使用final修饰,因此枚举类不能派生子类;
枚举类的所有实例必须在枚举类的第一行显示列出(枚举类不能通过new来创建对象); 并且这些实例默认/且只能是public static final的;
枚举类的构造器默认/且只能是private;
枚举类通常应该设计成不可变类, 因此建议成员变量都用private final修饰;
枚举类不能使用abstract关键字将枚举类声明成抽象类(因为枚举类不允许有子类), 但如果枚举类里面有抽象方法, 或者枚举类实现了某个接口, 则定义每个枚举值时必须为抽象方法提供实现,

        
            $(function () {
                $('pre.prettyprint code').each(function () {
                    var lines = $(this).text().split('\n').length;
                    var $numbering = $('').addClass('pre-numbering').hide();
                    $(this).addClass('has-numbering').parent().append($numbering);
                    for (i = 1; i <= lines; i++) {
                        $numbering.append($('').text(i));
                    };
                    $numbering.fadeIn(1700);
                });
            });
        
   

你可能感兴趣的:(java-8学习记录)