数据结构与算法--实现Singleton模式

题目:设计一个类,我们只生成该类的一个实例。
  • 只生成一个实例的类就是实现Singleton(单例)模式的类型。本题其实主要考察我们设计模式,因为面试的时候先来一个简单的,并且喜欢面设计模式相关的题目,而且,在常用的设计模式中,Singleton是比较简单的,而且可以通过简洁的代码来实现。所有Singleton是常见的面试题目。
以下解题思路
  • 由于只生成一个实例,我们可以将构造方法设置成私有构造,使得其他方法无法通过实例创建。我们可以定义一个静态实例,在需要的时候创建该实例,如下思路:
解法一:只适用单线程
//不好解法一
/**
 * @author liaojiamin
 * @Date:Created in 10:18 2020/10/27
 */
public class Singleton {
    private Singleton(){}
    public static Singleton singleton = null;
    public static Singleton getInstance(){
        if(null == singleton){
            singleton = new Singleton();
        }
        return singleton;
    }
}
  • 分析,以上代码是在不考虑并发的情况下的简单单例模式,从下面几点来保证了得到的实例是唯一的:
    • 静态实例:带有static关键字的熟悉在每个类中都是唯一的(在class文件被加载到jvm中的准备阶段,方法区为这些类变量进行内存分配,并且进行初始化。比如被static修饰的字段。非static修饰属性会在类实例化时候在对内存中分配存储空间。因此类变了在class文件被加载的时候才有,并不受实例化的影响)
    • 私有构造方法限制客户通过实例创建
    • 提供getInstance唯一入口
  • 以上代码存在并发问题,用如下方法进行检测:
/**
 * @author liaojiamin
 * @Date:Created in 14:44 2020/10/27
 */
public class TestSingletonRunnableMain {
    private Boolean lock;

    public Boolean getLock() {
        return lock;
    }

    public void setLock(Boolean lock) {
        this.lock = lock;
    }

    public static void main(String[] args) throws InterruptedException {
        Long startTime = System.currentTimeMillis();
        int num = 100;
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
        final Set<String> set = Collections.synchronizedSet(new HashSet<String>());
        ThreadFactory nameThreadFactory = new ThreadFactoryBuilder().setNameFormat("nameThreadFactory-01").build();
        ExecutorService executorService = new ThreadPoolExecutor(100, 100, 1,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), nameThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 0; i < num; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        cyclicBarrier.await();
                        Singleton  singletonThree = Singleton.getInstance();
                        set.add(singletonThree.toString());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        Thread.sleep(2000);
        System.out.println("in more thread get singleton");
        for (String s : set) {
            System.out.println(s);
        }
        executorService.shutdown();

    }
}
//输出:
//in more thread get singleton
//com.ljm.resource.math.Singleton@53fe75ec
//com.ljm.resource.math.Singleton@4eba943c
解法二:并发安全,但是效率低
/**
 * @author liaojiamin
 * @Date:Created in 14:33 2020/10/27
 */
public class SingletonOne {
    private SingletonOne(){}
    public static SingletonOne singletonOne;
    public static SingletonOne getInstance(){
        synchronized (SingletonOne.class){
            if(null == singletonOne){
                singletonOne = new SingletonOne();
            }
            return singletonOne;
        }
    }
}
  • 区别在于获取对象时,用synchronized关键字进行加锁,同一时刻只能有一个线程执行,等第一个线程创建完时间后。第一个线程释放同步锁,此时第二个线程可以加上同步锁,并允许接下来的代码。这个时候,实例已经被第一个线程创建,所以第二个线程不会再重复创建实例,保证得到的是同一个实例(synchronized加锁是一个非常耗时的操作,应该尽量避免)
可行的解法:加同步锁前后两次判断
  • 我们可以优化以上方法,只在我们需要的时候镜像同步锁,如下。
/**
 * @author liaojiamin
 * @Date:Created in 14:38 2020/10/27
 */
public class SingletonTwo {
    private SingletonTwo(){}
    public static SingletonTwo singletonTwo;

    public static SingletonTwo getInstance(){
        if(null == singletonTwo){
            synchronized (SingletonOne.class){
                if(null == singletonTwo){
                    singletonTwo = new SingletonTwo();
                }
            }
        }
        return singletonTwo;
    }
}
  • 以上SingletonTwo中只有instance为null的时候,才需要加锁。当instance已经创建出来后,无须加锁。因为只有第一次instance为null,所以执行的结果就是只有第一次的时候才会加锁,其他时候都无需锁,所有效率高得多。
  • 两次判断的原因:
    • 假设我们去掉同步块中的是否为null的判断,有这样一种情况,假设A线程和B线程都在同步块外面判断了synchronizedSingleton为null,结果A线程首先获得了线程锁,进入了同步块,然后A线程会创造一个实例,此时synchronizedSingleton已经被赋予了实例,A线程退出同步块,直接返回了第一个创造的实例,此时B线程获得线程锁,也进入同步块,此时A线程其实已经创造好了实例,B线程正常情况应该直接返回的,但是因为同步块里没有判断是否为null,直接就是一条创建实例的语句,所以B线程也会创造一个实例返回,此时就造成创造了多个实例的情况。
推荐的解法:利用静态属性
/**
 * @author liaojiamin
 * @Date:Created in 14:40 2020/10/27
 */
public class SingletonThree {
    private SingletonThree(){}
    private static SingletonThree singletonThree = new SingletonThree();
    public static SingletonThree getInstance(){
        return singletonThree;
    }
}
  • SingletonThree实现的方式简洁。我们初始化静态变量singletonThree 时候创建一个实例。由于Java是在类加载的时候在方法区对静态属性分配内存,并且只初始化一次,这样我们就能保证只初始化一次singletonThree 。
  • 因为SingletonThree 中singletonThree 的初始化并不是调用getInstance的时候创建的,而且在类加载的时候就已经创建,我们使用的时候调用getInstance方法他其实是不会创建新的实例,所有他会提前创建好实例,不管你之后是否需要
最优的解法:实现按需创建实例
/**
 * @author liaojiamin
 * @Date:Created in 15:45 2020/10/27
 */
public class SingletonFour {
    private SingletonFour(){}
    private static class SingletonInstance{
        static SingletonFour singletonFour = new SingletonFour();
    }
    public static SingletonFour getInstance(){
        return SingletonInstance.singletonFour;
    }
}
  • SingletonFour 中我们定义了一个私有的内部类SingletonInstance我们利用:**内部静态类不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。**的特点来实现的此处的单例
  • 由于静态内部类是私有的,只有我们在调用getInstance方法的时候被用到,因此当我们试图通过属性SingletonFour .getInstance得到SingletonFour 时候,会自动调用内部类SingletonInstance的静态构造方法创建实例,并初始化内部类中的静态变量 singletonFour
解法比较
  • 以上五种实现方案中,第一张方法在多线程环境中不能正常工作,第二种线程安全的方法但是实际效率低下,都不是我们所期待的可运行的解法。第三种方法中,我们通过两次判断加一次锁确保在多线程环境能高效运转。第四种利用java静态属性的特性,确保值创建一个实例。第五种方法利用私有嵌套类型的特性,做到只在真正需要的时候才创建实例,提高空间使用率。第五种解法是最优解。

下一篇: 数据结构与算法–数组:二维数组中查找

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值