专业编程基础技术教程

网站首页 > 基础教程 正文

三分钟学会了6种方法来实现一个单例

ccvgpt 2024-07-28 12:11:12 基础教程 7 ℃

类为什么要设计成单例模式

在应用中如果有两个或者两个以上的实例会引起错误(配置文件信息),又或者在整个应用中,同一时刻,有且只能有一种状态,此时就可以创建一个单例。

设计成单例模式的好处

节约系统的内存资源消耗,降低内存空间的使用,减少GC消耗,提升系统响应时间。

三分钟学会了6种方法来实现一个单例

单例模式的几种方式

1懒汉式,适用于非并发情况下

public class Singleton {
    //一个静态的实例
    private static Singleton singleton;
    //私有化构造函数
    private Singleton(){}
    //给出一个公共的静态方法返回一个单一实例
    public static Singleton getInstance(){
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,节约内存空间。

这种方式通过以下几个地方来限制了我们取到的实例是唯一的:

  • 静态实例,带有static关键字的属性在每一个类中都是唯一的。
  • 限制客户端随意创造实例,即私有化构造方法,此为保证单例的最重要的一步。
  • 给一个公共的获取实例的静态方法,注意,是静态的方法(直接类名.方法名进行调用),因为这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。
  • 判断只有持有的静态实例为null时才调用构造方法创造一个实例,否则就直接返回。

测试并验证第一种单例模式在并发情况下存在的问题:

/**
 * 测试并验证并发情况下第一种单例模式存在的问题:会创建多个实例
 */
public class TestSingleton {
    public static void main(String[] args) throws InterruptedException {
         Set<String> instanceSet = new HashSet<String>();
        //创建100线程
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            newCachedThreadPool.execute(() -> {
                Singleton singleton = Singleton.getInstance();
                instanceSet.add(singleton.toString());
            });
        }
        System.out.println("------打印并发下实例数据--------");
        for (String instance : instanceSet) {
            System.out.println(instance);
        }
        newCachedThreadPool.shutdownNow();
    }
}

结果:

------打印并发下实例数据--------

Singleton@6e22e576

Singleton@e87508d

结论:造成这种情况的原因是因为,当并发访问的时候,第一个调用getInstance方法的线程A,在判断完singleton是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。


2第二种创建方式:为了避免第一种情况(创建在适用于并发情况下的单例模式)

可以在将创建单例方法加 synchronized 关键字进行同步整个方法

/**
 * synchronized 关键字进行同步整个方法的单例模式
 */
public class BadSynchronizedSingleton {
    //一个静态的实例
    private static BadSynchronizedSingleton synchronizedSingleton;
 
    //私有化构造函数
    private BadSynchronizedSingleton() {
    }
 
    //给出一个公共的静态方法返回一个单一实例
    public synchronized static BadSynchronizedSingleton getInstance() {
        if (synchronizedSingleton == null) {
            synchronizedSingleton = new BadSynchronizedSingleton();
        }
        return synchronizedSingleton;
    }
}

这种方式通过将整个获取实例的方法同步,不过这样在一个线程访问这个方法时,其它所有的线程都要处于挂起等待状态,倒是避免了刚才同步访问创造出多个实例的危险,但是这样会造成很多无谓的等待,所以这样写也不好。


3、第三种创建方式:双重加锁模式

第二种方式只是需要发生在单例的实例还未创建的时候,在实例创建以后,获取实例的方法就没必要再进行同步控制了,所以我们将上面的示例改为双重加锁模式。

/**
 * 双重加锁的单例模式
 */
public class SynchronizedSingleton {
 
    //一个静态的实例
    private static SynchronizedSingleton synchronizedSingleton;
 
    //私有化构造函数
    private SynchronizedSingleton() {
    }
 
    //给出一个公共的静态方法返回一个单一实例
    public static SynchronizedSingleton getInstance() {
        if (synchronizedSingleton == null) {
            synchronized (SynchronizedSingleton.class) {
                if (synchronizedSingleton == null) {
                    synchronizedSingleton = new SynchronizedSingleton();
                }
            }
        }
        return synchronizedSingleton;
    }
}

"双重检查加锁"机制指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

"双重检查加锁"机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确地处理该变量。但是由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用"双重检查加锁"机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?那么就请看第四种创建单例模式。


4、第四种创建方式(内部类方式)

/**
 * 创建静态内部类的单例模式
 */
public class SingletonInstance {
    private SingletonInstance() {
    }
 
    public static SingletonInstance getInstance() {
        return SingletonHolder.instance;
    }
 
    private static class SingletonHolder {
        static SingletonInstance instance = new SingletonInstance();
    }
}

这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

相应的知识:

 什么是类级内部类?

  简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

  类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

  类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

  类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。

 多线程缺省同步锁的知识

  大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

  2.访问final字段时

  3.在创建线程之前创建对象时

  4.线程可以看见它将要处理的对象时

这种方式安全的原因:因为JVM保证类的静态属性只会在第一次加载类时初始化,所以我们不需要担心并发访问的问题。

当getInstance方法第一次被调用的时候,它第一次读取InnerClassSingleton.instance,导致InnerClassSingleton类得到初始化;

而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

下面这种饿汉式的创建方式也能实现这个需求;采用静态初始化器的方式,它可以由JVM来保证线程的安全性。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。

而这种采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。


5、第五种创建方式:(饿汉式)

/**
 * @desc:饿汉式单例模式
 */
public class EagerSingleton {
    private static EagerSingleton instance = new EagerSingleton();
 
    /**
     * 私有默认构造子
     */
    private EagerSingleton() {
    }
 
    /**
     * 静态工厂方法
     */
    public static EagerSingleton getInstance() {
        return instance;
    }
}

这种方式创建是在这个类被加载时,静态变量instance会被初始化,此时类的私有构造子会被调用。这时候,单例类的唯一实例就被创建出来了。

不过这种方式最主要的缺点就不管你需不需要,会在类装载的时候就初始化对象,而事实是可能从始至终就没有使用这个实例,造成内存的浪费。

不过在有些时候,不过在应用启动时就需要加载的配置文件时,可以采取这种方式去保证单例,对系统几乎没什么影响。这个和第四种创建方式有点像。


6、枚举形式的单例模式

class Resource {
}
 
public enum EnumSingleton {
    INSTANCE;
  
    private Resource instance;
 
    EnumSingleton() {
        instance = new Resource();
    }
 
    public Resource getInstance() {
        return instance;
    }
}

有说法单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。

使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

Tags:

最近发表
标签列表