设计模式单例模式你用对了吗

Posted Misout的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式单例模式你用对了吗相关的知识,希望对你有一定的参考价值。


设计模式(七)单例模式你用对了吗

单例模式的作用

有一些对象,我们只希望只有一个实例,例如:线程池对象,缓存对象,对话框,日志对象,打印机对象等。事实上,这类对象只能有一个,如果多于一个,就会导致许多问题的产生,例如超多两个日志对象,会造成写日志文件失败的情况。


实现单例模式的设计要点

  • 保证类的构造方法为私有,这样就无法通过new关键字主动创建对象。

  • 对外提供一个静态方法,用于获取该类的独一无二的实例对象。


懒汉 Or 饿汉

  • 懒汉:在获取实例的静态方法被调用前,类的对象还未实例化,等到真正使用的时候(这个人比较懒,饿了的时候才开始做饭)再创建对象。属于懒加载的一种。

  • 饿汉:在类被JVM加载的时候,类的对象就被创建了,也就是获取实例的静态方法被调用前,对象已经被创建好了(这个人一开始就很饿,早早的把饭做好了),属于预加载的一种。


实现单例模式的八种方法

1.懒汉非线程安全

public class Singleton1 {

    private static Singleton1 instance;

    private Singleton1({}

    public static Singleton1 getInstance({
        if(instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }
}

非线程安全的写法,具体原因分析就不做过多赘述了,请看多线程场景下获取单例得到不同对象的测试代码和结果:

@Test
public void testLazymanNonThreadSafeSingleon() {
    Runnable myRunable = new Runnable() {
        @Override
        public void run() {
            Singleton1 instance = Singleton1.getInstance();
            System.out.println(instance);
        }
    };

    Thread thread1 = new Thread(myRunable);
    Thread thread2 = new Thread(myRunable);

    thread1.start();
    thread2.start();
}
com.misout.singleton.Singleton1@1b8feeb
com.misout.singleton.Singleton1@c44cf9

推荐程度:不推荐

2.懒汉线程安全-同步方法

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {}

    public static synchronized Singleton2 getInstance() {
        if(instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

在方法上加了synchronized关键字,能保证获取的对象都是同一个。
推荐程度:可用,但不推荐。虽然线程安全,但synchronized关键字加载整个方法上,性能太差,在高并发性能敏感的场景下不适用。

3.懒汉线程安全-同步代码块

public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {}

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

这种写法,只是简单的把加在方法上的synchronized关键字移到了方法内部,性能上没有任何优化,一样很差。
推荐程度:可用,但不推荐

下面的写法是否线程安全?

public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {}

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

很多面试官会哪这种写法和上面的写法考察应聘者,比如我就很喜欢。但不幸的是,很多应聘者都回答错误,让我很失望。答案当然是否定的。下面用测试用例来测试下到底是否线程安全:

@Test
public void testLazymanSingleon() {
    Runnable myRunable = new Runnable() {
        @Override
        public void run() {
            Singleton3 instance = Singleton3.getInstance();
            System.out.println(instance);
        }
    };

    Thread thread1 = new Thread(myRunable);
    Thread thread2 = new Thread(myRunable);

    thread1.start();
    thread2.start();
}
com.misout.singleton.Singleton3@e8bc6d
com.misout.singleton.Singleton3@d9b5b

原因很简单:if判断条件并不能保证多线程下只有一个线程在执行。

4.懒汉线程安全-双重检测锁


双重检测锁的写法是对方法上加锁或代码块加锁的一种性能优化,多增加一次if判断,能减少进入同步代码块的线程数从而降低竞争锁的激烈程度,减少线程阻塞切换上下文环境带来的性能损耗。

public class Singleton4 {
    private static volatile Singleton4 instance;

    private Singleton4() {}

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

即达到了懒加载的目的,也优化了部分性能。
推荐程度:推荐。

这里我们继续扩展下,上面代码中,变量声明时增加了关键字volatile,这里会是面试官经常重点考察的点,比如我就很喜欢问不加volatile有什么问题?
很多应聘者答,不加volatile关键字就无法保证对象的可见性,对象赋值后有可能是空的,但这种回答有点牵强。

  • synchronized关键字已经保证了对象的可见性,volatile的作用在这里并非可见性。


多线程场景下测试没有volatile关键字的代码和测试结果:
测试代码:

@Test
public void testDoubleCheckSingleon() {
    Runnable myRunable = new Runnable() {
        @Override
        public void run() {
            Singleton4 instance = Singleton4.getInstance();
            System.out.println(instance);
        }
    };

    Thread thread1 = new Thread(myRunable);
    Thread thread2 = new Thread(myRunable);

    thread1.start();
    thread2.start();
}
"D:\Program Files (x86)\Java\jdk1.8.0_102\bin\java.exe-ea ...省略
com.misout.singleton.Singleton4@1ce9ad6

Process finished with exit code 0

5.饿汉线程安全-静态常量写法

public class Singleton5 {
    private static final Singleton5 instance = new Singleton5();

    private Singleton5() {}

    public static Singleton5 getInstance() {
        return instance;
    }
}
  • 优点:实现简单,无线程同步问题。

  • 缺点:在类装载时完成实例化。若该实例一直未被使用,则会造成资源浪费。
    推荐程度:推荐。

6.饿汉线程安全-静态代码块写法

public class Singleton6 {
    private static final Singleton6 instance;

    static {
        instance = new Singleton6();
    }

    private Singleton6() {}

    public static Singleton6 getInstance() {
        return instance;
    }
}
  • 优点:无线程同步问题,线程安全。

  • 缺点:类装载时创建实例,无Lazy Loading。实例一直未被使用时,会浪费资源
    推荐程度:推荐。

7.懒汉线程安全-静态内部类写法

public class Singleton7 {
    static class Inner {
        private static final Singleton7 instance = new Singleton7();
    }

    private Singleton7() {}

    public static Singleton7 getInstance() {
        return Inner.instance;
    }
}
  • 优点:无线程同步问题,实现了懒加载(Lazy Loading)。因为只有调用getInstance时才会装载内部类,才会创建实例。同时因为使用内部类时,先调用内部类的线程会获得类初始化锁,从而保证内部类的初始化(包括实例化它所引用的外部类对象)线程安全。即使内部类创建外部类的实例Singleton INSTANCE = new Singleton()发生指令重排也不会引起双重检查(Double-Check)下的懒汉模式中提到的问题,因此无须使用volatile关键字。

  • 缺点:NA

推荐程度:推荐。


8.枚举写法

public enum Singleton8 {
    instance;

    public static Singleton8 getInstance() {
        return instance;
    }
}
  • 优点:枚举本身是线程安全的,且能防止通过反射和反序列化创建多实例。

  • 缺点:使用的是枚举,而非类。


总结

上文的八种写法,除了枚举写法无法通过反射来破坏单例外,其他方式都可以通过反射破坏单例。但在实际应用场景下,这种情况比较少见,故而忽略。

推荐阅读






如果对你有用,欢迎分享到朋友圈

Misout的博客

长按二维码关注我


以上是关于设计模式单例模式你用对了吗的主要内容,如果未能解决你的问题,请参考以下文章

Java 单例真的写对了么?

色彩搭配注意事项!你用对了吗?

色彩搭配注意事项!你用对了吗?

数据可视化或信息图,你用对了吗?

这里的设计模式你用过几种?

java设计模式之单例模式