题目:设计一个类,我们只生成该类的一个实例。
- 只生成一个实例的类就是实现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() {@Overridepublic 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静态属性的特性,确保值创建一个实例。第五种方法利用私有嵌套类型的特性,做到只在真正需要的时候才创建实例,提高空间使用率。第五种解法是最优解。
下一篇: 数据结构与算法–数组:二维数组中查找