当前位置: 主页 > JAVA语言

单例模式 java-单例模式给面试官,你准备好了吗?

发布时间:2023-06-21 09:05   浏览次数:次   作者:佚名

单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为,比如:全局信息配置。

【面试题】

单例模式的思想是什么?写一个代码体现。

(我们最好写懒汉式的单例模式给面试官,这个才是他想要的答案)

开发使用:恶汉式(是不会出问题的单例模式)

面试时写:懒汉式(可能会出现问题的单例模式)

A. 懒汉式(延迟加载)

B. 线程安全问题

a. 是否多线程环境–是

b. 是否有共享数据–是

c. 是否有多条语句操作共享数据–是

零、单例模式的一般步骤

私有化构造方法使其外部不能直接创建对象保证对象的唯一性

私有化和静态化自己内部的对象(因为外部不能new对象只能内部来new了)

提供一个公共的静态的方法给外部直接使用自己内部创建的对象

一、饿汉式

饿汉式是最简单的实现方式,这种实现方式适合那些在初始化时就要用到单例的情况,这种方式简单粗暴,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。

// 单例模式最简单的实现:

public class Singleton{

// 由私有构造方法和static来确定唯一性。

private static Singleton instance = new Singleton();

private Singleton(){}

public static Singleton newInstance(){

return instance;

}

}

饿汉式有个致命缺点:何时产生实例不好控制。虽然我们知道,在类Singleton第一次被加载的时候,就产生了一个实例。但是如果这个类中有其他属性,如下代码:

public class Singleton {

private static Singleton instance = new Singleton();

public static int STATUS=1;

private Singleton() {} public static Singleton getInstance() {

return instance;

}

}

当使用

System.out.println(Singleton.STATUS);

这个实例就被产生了。也许此时你并不希望产生这个实例。如果系统特别在意这个问题,这种单例的实现方法就不太好。

还有一个“但是”,如果单例初始化的操作耗时比较长而应用对于启动速度又有要求,或者单例的占用内存比较大单例模式 java,再或者单例只是在某个特定场景的情况下才会被使用单例模式 java,而一般情况下是不会使用时,使用饿汉式的单例模式就是不合适的,这时候就需要用到懒汉式的方式去按需延迟加载单例。

二、懒汉式

懒汉式与饿汉式的最大区别就是将单例的初始化操作,延迟到需要的时候才进行,这样做在某些场合中有很大用处。比如某个单例用的次数不是很多,但是这个单例提供的功能又非常复杂,而且加载和初始化要消耗大量的资源,这个时候使用懒汉式就是非常不错的选择。

public class Singleton{

private static Singleton instance = null;

private Singleton(){}

// 此方法实现的单例,无法在多线程中使用,多线可以同时进入if方法,会导致生成多个单例对象。

public static Singleton getInstance(){

单例模式 java_java单例模式例子_单例模式 java

if(null == instance){

instance = new Singleton();

}

return instance;

}

}

还有种写法:

public class Singleton {

private static Singleton instance = null;

private Singleton() {

}

// 大家都会想到同步,可以同步方法实现多线程的单例

// 但是这种方法不可取,严重影响性能,因为每次去取单例都要检查方法,所以只能用同步代码块的方式实现同步。

public static synchronized Singleton getInstance() {

if (instance == null)

instance = new Singleton();

return instance;

}

}

让instance只有在调用getInstance()方式时被创建,并且通过synchronized来确保线程安全。这样就控制了何时创建实例。这种方法是延迟加载的典型。

但是有一个问题就是,在高并发的场景下性能会有影响,虽然只有一个判断就return了,但是在并发量很高的情况下,或多或少都会有点影响,因为都要去拿synchronized的锁。

为了高效,有了第三种方式:

public class StaticSingleton {

private StaticSingleton(){}

private static class SingletonHolder {

private static StaticSingleton instance = new StaticSingleton();

}

public static StaticSingleton getInstance() {

return SingletonHolder.instance;

}

}

由于加载一个类时,其内部类不会被加载。这样保证了只有调用getInstance()时才会产生实例,控制了生成实例的时间,实现了延迟加载。

并且去掉了synchronized,让性能更优,用static来确保唯一性。

三、多线程下的单例模式

上面介绍了一些单例模式的基本应用方法,但是上面所说的那些使用方式都是有一个隐含的前提,那就是他们都是应用在单线程条件下,一旦换成了多线程就有出错的风险。

如果在多线程的情况下,饿汉式不会出现问题,因为JVM只会加载一次单例类,但是懒汉式可能就会出现重复创建单例对象的问题。为什么会有这样的问题呢?因为懒汉式在创建单例时是 线程不安全的,多个线程可能会并发调用他的getInstance方法导致多个线程可能会创建多份相同的单例出来。

那有没有办法,使懒汉式的单利模式也是线程安全的呢?答案肯定是有的,就是使用加同步锁的方式去实现。

3.1 懒汉式同步锁

public class Singleton {

private static Singleton instance = null;

private Singleton(){}

public static Singleton getInstance() {

synchronized (Singleton.class) {

java单例模式例子_单例模式 java_单例模式 java

if (instance == null) {

instance = new Singleton();

}

}

return instance;

}

}

这种是最常见的解决同步问题的一种方式,使用同步锁synchronized (Singleton.class)防止多线程同时进入造成instance被多次实例化。举个在Android使用这种方式的例子:

public final class InputMethodManager {

//内部全局唯一实例

static InputMethodManager sInstance; //对外api

public static InputMethodManager getInstance() {

synchronized (InputMethodManager.class) {

if (sInstance == null) {

IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);

IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);

sInstance = new InputMethodManager(service, Looper.getMainLooper());

}

return sInstance;

}

}

}

以上是Android源码中输入法类相关的单例使用方式。但其实还有一种更好的方式如下。

3.2 双重校验锁

public class Singleton {

private static volatile Singleton instance = null;

private Singleton(){}

public static Singleton getInstance() {

// if already inited, no need to get lock everytime

if (instance == null) {

synchronized (Singleton.class) {

if (instance == null) {

instance = new Singleton();

}

}

}

return instance;

}

}

可以看到上面在synchronized (Singleton.class)外又添加了一层if,这是为了在instance已经实例化后下次进入不必执行synchronized (Singleton.class)获取对象锁,从而提高性能。

以上两种方式还是挺麻烦的,我们不禁要问,有没有更好的实现方式呢?答案是肯定的。我们可以利用JVM的类加载机制去实现。在很多情况下JVM已经为我们提供了同步控制,比如:

class SingletonLazy{

private static SingletonLazy instance = null;

单例模式 java_单例模式 java_java单例模式例子

private SingletonLazy() {}

// 用同步代码块的方式,在判断单例是否存在的if方法里使用同步代码块,在同步代码块中再次检查是否单例已经生成,

// 这也就是网上说的 双重检查加锁的方法

public static synchronized SingletonLazy getInstance3(){

if(instance==null){

synchronized (SingletonLazy.class) {

if(instance==null){

instance = new SingletonLazy();

}

}

}

return instance;

}

}

因为在JVM进行类加载的时候他会保证数据是同步的,我们可以这样实现:

采用内部类,在这个内部类里面去创建对象实例。这样的话,只要应用中不使用内部类 JVM 就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载和线程安全。实现代码如下:

四、枚举类型单例模式

public enum Singleton{

//定义一个枚举的元素,它就是Singleton的一个实例

instance;

public void doSomething(){

// do something ...

}

}

使用方法如下:

public static void main(String[] args){

Singleton singleton = Singleton.instance;

singleton.doSomething();

}

默认枚举实例的创建是线程安全的.(创建枚举类的单例在JVM层面也是能保证线程安全的), 所以不需要担心线程安全的问题,所以理论上枚举类来实现单例模式是最简单的方式。

五、再说一点野的——单例防止反射

饿汉式、懒汉式的方式还不能防止反射来实现多个实例,通过反射的方式,设置ACcessible.setAccessible方法可以调用私有的构造器,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

其实这样还不能保证单例,当序列化后,反序列化是还可以创建一个新的实例,在单例类中添加readResolve()方法进行防止。

package sun.geoffery.singleton ;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.Serializable;

import java.lang.reflect.Constructor;

/**

* 懒汉汉式单例,在需要单例对象的时候,才创建唯一的单例对象

单例模式 java_单例模式 java_java单例模式例子

* 以后再次调用,返回的也是第一创建的单例对象

*/public class Singleton implements Serializable {

private static final long serialVersionUID = -5271538729559621645L;

// 将静态成员初始化为null,在获取单例的时候才创建,故此叫懒汉式。

private static Singleton instance = null;

private static int i = 1;

// 防止反射攻击,只运行调用一次构造器,第二次抛异常

private Singleton() {

if (i == 1) {

i++;

} else {

throw new RuntimeException("只能调用一次构造函数");

}

System.out.println("调用Singleton的私有构造器");

}

/**

* 用同步代码块的方式,在判断单例是否存在的if方法里使用同步代码块,

* 在同步代码块中再次检查是否单例已经生成,这也就是网上说的 双重检查加锁的方法

*

* @return

*/

public static synchronized Singleton getInstance() {

if (instance == null) {

synchronized (Singleton.class) {

if (instance == null) {

instance = new Singleton();

}

}

}

return instance;

}

/**

* 防止反序列生成新的单例对象

* 这是 Effective Java 一书中说的用此方法可以防止,具体细节我也不明白

*

* @return

*/

private Object readResolve() {

return instance;

}

/**

* 测试反射攻击

*/

单例模式 java_单例模式 java_java单例模式例子

public static void test1() {

Singleton s = Singleton.getInstance();

Class c = Singleton.class;

Constructor privateConstructor;

try {

privateConstructor = c.getDeclaredConstructor();

privateConstructor.setAccessible(true);

privateConstructor.newInstance();

} catch (Exception e) {

e.printStackTrace();

}

}

/**

* 测试 反序列 仍然为单例模式

*

* @throws Exception

*/

public static void test2() throws Exception {

Singleton singleton = Singleton.getInstance();

ObjectOutputStream objectOutputStream =

new ObjectOutputStream(new FileOutputStream(new File("D:\\Singleton.txt")));

objectOutputStream.writeObject(singleton);

ObjectInputStream objectInputStream =

new ObjectInputStream(new FileInputStream(new File("D:\\Singleton.txt")));

Object readObject = objectInputStream.readObject();

Singleton singleton1 = (Singleton) readObject;

System.out.println("s.hashCode():" + singleton.hashCode() +

",s1.hashCode():" + singleton1.hashCode());

objectOutputStream.flush();

objectOutputStream.close();

objectInputStream.close();

}

public static void main(String[] args) throws Exception {

test1();

test2();

}

}

验证反射攻击结果(只执行test1()方法):

java单例模式例子_单例模式 java_单例模式 java

如果不添加readResolve方法的结果(只执行test2()方法,注释掉readResolve()部分代码):

单例模式 java_java单例模式例子_单例模式 java

添加readResolve方法的结果:(只执行test2()方法,添加readResolve()部分代码):

单例模式 java_java单例模式例子_单例模式 java

总结

一般单例模式包含了5种写法,分别是饿汉、懒汉、双重校验锁、静态内部类和枚举。相信看完之后你对单例模式有了充分的理解了,根据不同的场景选择最你最喜欢的一种单例模式吧!

号外:测试结果的话判断对象的hashcode是否一致来判断是否为同一个对象。