单例模式 java-单例模式给面试官,你准备好了吗?
单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为,比如:全局信息配置。
【面试题】
单例模式的思想是什么?写一个代码体现。
(我们最好写懒汉式的单例模式给面试官,这个才是他想要的答案)
开发使用:恶汉式(是不会出问题的单例模式)
面试时写:懒汉式(可能会出现问题的单例模式)
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(){
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) {
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;
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;
/**
* 懒汉汉式单例,在需要单例对象的时候,才创建唯一的单例对象
* 以后再次调用,返回的也是第一创建的单例对象
*/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;
}
/**
* 测试反射攻击
*/
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()方法):
如果不添加readResolve方法的结果(只执行test2()方法,注释掉readResolve()部分代码):
添加readResolve方法的结果:(只执行test2()方法,添加readResolve()部分代码):
总结
一般单例模式包含了5种写法,分别是饿汉、懒汉、双重校验锁、静态内部类和枚举。相信看完之后你对单例模式有了充分的理解了,根据不同的场景选择最你最喜欢的一种单例模式吧!
号外:测试结果的话判断对象的hashcode是否一致来判断是否为同一个对象。