面试题2:实现单例模式

题目:设计一个类,只能生成该类的一个实例。

  • 单例模式分为懒汉式(需要才去创建对象)和饿汉式(创建类的实例时就去创建对象)

饿汉式

属性实例化对象
public class HugerSingletonTest {
	//该对象的引用不可修改
	private static final HugerSingletonTest ourInstance = new HugerSingletonTest();
	
	public static HugerSingletonTest getInstance() {
		return ourInstance;
	}

	private HugerSingletonTest() {}
}

在静态代码块实例对象
public class Singleton {
	private static Singleton ourInstance;

	static {
		ourInstance = new Singleton();
	}
	
	public static Singleton getInstance() {
		return ourInstance;
	}
	
	private Singleton() {}
}

分析:饿汉式单例模式只要调用了该类,就会实例化一个对象,但有时我们只需要调用该类中的一个方法,而不需要实例化一个对象,所以饿汉式是比较消耗资源的。

懒汉式

非线程安全
public class Singleton {
	private static Singleton ourInstance;
	
	private static Singleton getInstance() {
		if(null == ourInstance) {
			ourInstance = new Singleton();
		}	
		return outInstance;
	}
	
	private Singleton() {}
}

分析:如果有两个线程同时调用 getInstance() 方法,则会创建两个实例化对象,所以是非线程安全的。


线程安全:给方法加锁
public class Singleton {
	private static Singleton ourInstance;

	public synchronized static Singleton getInstance() {
		if(null == ourInstance) {
			ourInstance = new Singleton();
		}
		return ourInstance;
	}

	private Singleton() {}
}

分析:如果有多个线程调用 getInstance() 方法,当一个线程获取该方法,其他线程就必须等待,消耗资源。


线程安全:双重检查锁(同步代码块)
public class Singleton {
	private static Singleton ourInstance;

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

	private Singleton() {}
}

分析:为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了。然后空对象的线程进入同步代码块,如果不加第二次空对象检查,两个线程都获取了同步代码块,一个线程先进入了同步代码块,另一个则在等待,此时先进入的线程创建了实例对象,而后线程也进入同步代码块时,也会创建一个实例对象,此时就会创建两个实例对象,所以需要在线程进入同步代码块后再次j进行空对象检查,才能确保只创建一个实例对象。


线程安全:静态内部类
public class Singleton {
	private static class SingletonHodler {
		private static final Singleton ourInstance = new Singleton();
	}
	
	public static Singleton getInstance() {
		return SingletonHodler.ourInstance;
	}

	private Singleton() {}
}

分析:

  • 静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。具体来说当 Singleton 第一次被加载时,并不需要去加载 Singleton,只有当 getInstance() 方法第一次被调用时,使用 INSTANCE 的时候,才会导致虚拟机加载 SingletonHodler 类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
  • 为何线程安全:虚拟机会保证一个类的类构造器 < clinit >() 在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器 < clinit >(),其他线程都需要阻塞等待,直到活动线程执行 < clinit >() 方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行 < clinit >() 方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行 < clinit >() 方法,因为在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的 < clinit >() 方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

线程安全:枚举
enum SingletonTest {
	INSTANCE;

	public void doSomething() {}
}

分析:默认枚举实例的创建是线程安全的,而且还能防止反序列化重新创建新对象,但是在枚举中的其它任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。


线程安全:使用ThreadLocal
public class Singleton {
	private static final ThreadLocal<Singleton> tlSingleton = 
		    new ThreadLocal<Singleton>() {
		    	@Override
		    	protected Singleton initialValue() {
		    		return new Singleton();
		    	}
		    };

	public static Singleton getInstance() {
		return tlSinglrton.get();
	}

	private Singleton() {}
}

分析:ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。


指令重排序

其实创建一个对象,往往包含三个过程。对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含的三个过程。

  • 给 singleton 分配内存
  • 调用 Singleton 的构造函数来初始化成员变量,形成实例
  • 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被l另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了。

针对这种情况,我们有什么解决方法呢?那就是把singleton声明成 volatile

public class Singleton {
    //volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性
    private volatile static Singleton ourInstance;

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

    private Singleton() {
    }
}

原文地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是几个常见的C++单例模式面试和笔试,以及它们的解答。 1. 请用C++实现单例模式。 解答: 单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点。以下是单例模式的C++实现: ```c++ class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; ``` 在 getInstance() 函数中,我们使用 static 变量来存储单例实例。因为 static 变量在程序生命周期内只会被初始化一次,所以这个实例也只会被创建一次。 注意,我们将构造函数、拷贝构造函数和赋值运算符都声明为私有,这样外部就无法创建实例或复制实例。这是为了保证单例的唯一性。 2. 如何避免多线程环境下的竞态条件? 解答: 在多线程环境下,如果多个线程同时调用 getInstance() 函数,就会出现竞态条件(race condition)。为了避免这种情况,我们可以使用双重检查锁定(double-checked locking)技术。 ```c++ class Singleton { public: static Singleton& getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { instance = new Singleton(); } } return *instance; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static Singleton* instance; static std::mutex mutex; }; Singleton* Singleton::instance = nullptr; std::mutex Singleton::mutex; ``` 在 getInstance() 函数中,我们首先检查 instance 是否为空,如果为空就加锁,然后再次检查 instance 是否为空。这种方式可以避免多个线程同时创建实例。 3. 如何实现懒加载(lazy initialization)? 解答: 懒加载是指在需要时才创建对象,而不是在程序启动时就创建。这种方式可以减少程序启动时间和内存占用。 在单例模式中,我们可以在 getInstance() 函数中实现懒加载。 ```c++ class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; ``` 在这个例子中,我们使用 static 变量来存储单例实例。由于 static 变量在第一次使用时才会被初始化,因此这个实例也是在第一次调用 getInstance() 函数时被创建的。 4. 如何实现线程局部存储(thread-local storage)? 解答: 线程局部存储是指每个线程都有自己的变量副本,这些变量只能被当前线程访问。在单例模式中,我们可以使用线程局部存储来实现每个线程都有自己的单例实例。 ```c++ class Singleton { public: static Singleton& getInstance() { static thread_local Singleton instance; return instance; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; ``` 在这个例子中,我们使用 thread_local 关键字来声明 instance 变量为线程局部存储。这样每个线程都有自己的 instance 实例,互相之间不影响。 5. 如何实现单例模式的序列化(serialization)? 解答: 序列化是指将对象转换为字节序列,以便在网络上传输或保存到本地文件。在单例模式中,我们可以使用序列化技术来保存单例实例,以便下次程序启动时恢复。 ```c++ class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } void save(const std::string& filename) { std::ofstream ofs(filename); if (ofs) { boost::archive::text_oarchive oa(ofs); oa << Singleton::getInstance(); } } void load(const std::string& filename) { std::ifstream ifs(filename); if (ifs) { boost::archive::text_iarchive ia(ifs); ia >> Singleton::getInstance(); } } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; friend class boost::serialization::access; template <class Archive> void serialize(Archive& ar, const unsigned int version) { ar & instance; } static Singleton instance; }; Singleton Singleton::instance; ``` 在这个例子中,我们使用 Boost 序列化库来实现序列化和反序列化。我们定义了 save() 和 load() 函数来保存和加载单例实例。在 serialize() 函数中,我们将 instance 变量序列化到存档文件中。 注意,我们将 serialize() 函数声明为友元函数,这样 Boost 序列化库才能访问 Singleton 类的 private 成员。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值