详细说说单例模式
Posted 轻舸泛舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细说说单例模式相关的知识,希望对你有一定的参考价值。
单例模式
特点:全局唯一,在整个程序中,只有一个对象。
什么样的类适合单例?
-
全局使用的类 -
创建和销毁会消耗很多系统资源的类 -
数据库连接池 -
工厂类 -
数据源
应用:
-
Spring的Bean默认情况下是单例 -
项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。 -
应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 -
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。 -
操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。 -
Application 也是单例的典型应用(Servlet编程中会涉及到) -
在servlet编程中,每个Servlet也是单例 -
在spring MVC框架/struts1框架中,控制器对象也是单例
饿汉式
优点:
-
类一加载就创建实例,没有延迟 -
线程安全
缺点:
-
反射和反序列化会破坏单例 -
如果没有用到该类就会产生内存浪费 -
如果加载的资源很大,程序启动的时候就会产生效率问题
class Singleton implements Serializable {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
反射破坏单例:
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton instance = Singleton.getInstance();
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
System.out.println(instance == instance1); // false
}
反序列化破坏单例:
@Test
public void test() throws IOException, ClassNotFoundException {
Singleton i1 = Singleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./singleton.obj"));
out.writeObject(i1);
out.close();
FileInputStream in = new FileInputStream("./singleton.obj");
ObjectInputStream ois = new ObjectInputStream(in);
Singleton i2 = (Singleton) ois.readObject();
System.out.println(i1);
System.out.println(i2);
// com.qianyu.thread.Singleton@6842775d
// com.qianyu.thread.Singleton@1ae369b7
}
加入readResolve方法解决反序列化问题
import java.io.*;
class Singleton implements Serializable {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
为了防止反射破坏单例,我们可以在构造器中添加判断
import java.io.*;
class Singleton implements Serializable {
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("不能通过反射创建对象");
}
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式
特点:
-
延时加载 -
线程不安全
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
模拟线程不安全情况:
@Test
public void test() throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(() -> System.out.println(Singleton.getInstance())).start();
}
Thread.sleep(1000);
}
使用同步解决线程不安全问题
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
双重检测锁模式
懒汉式的单例模式虽然可以使用synchronized
解决线程不安全问题,但是每次获取单例对象的时候都要执行synchronized
代码块,造成效率低,为了解决这个问题,提出双重检测锁模式
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
指令重排问题
instance = new Singleton()会执行如下操作:
-
分配内存空间 -
初始化对象 -
instance执行(1)中分配的空间
但是在某些编译器上,可能会发生指令重排:
-
分配内存空间 -
instance执行(1)中分配的空间(但此时对象没有初始化) -
初始化对象
这样的话,如果多个线程同时访问的话,有可能会出现某些对象为空的情况。为了防止JVM进行指令重排,我们可以加上volatile关键字
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
登记式
登记式,也可以叫做“静态内部类”式。
首先我们要知道:
-
只有调用内部类的时候,内部类才开始加载(延时加载) -
非静态内部类要依附于外部类,也就是说外部类必须创建对象才能使用非静态内部类 -
静态内部类不用外部类创建对象就可以使用
class Test {
class Inner {
}
static class SInner {
}
}
public class Demo {
public static void main(String[] args) {
Test.Inner inner = new Test().new Inner();
Test.SInner s = new Test.SInner();
}
}
登记式相对于饿汉式的好处:
-
可以实现延迟加载 -
可以通过改造,使其对反射安全
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
// 如果使用反射调用会报错
private Singleton() {
if (SingletonHolder.instance != null) {
throw new IllegalStateException();
}
}
public Singleton getInstance() {
return SingletonHolder.instance;
}
}
枚举式
特点:
-
java1.5之后出现 -
目前推荐实现单例的最佳方式 -
线程安全 -
立即初始化 -
自动支持序列化,防止反序列化创建新的对象 -
防止反射攻击
public enum Singleton {
INSTANCE{
@Override
protected void dosomething() {
System.out.println("Singleton.dosomething");
}
};
protected abstract void dosomething();
}
反射调用构造器,会抛出异常:
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
// java.lang.NoSuchMethodException: com.qianyu.thread.Singleton.<init>()
// at java.lang.Class.getConstructor0(Class.java:3082)
// at java.lang.Class.getDeclaredConstructor(Class.java:2178)
// at com.qianyu.thread.Application.test(Application.java:11)
}
ThreadLocal
优点:
-
空间换时间 -
延时加载
缺点:
-
只能是在同一个线程中获得的两个对象才是单例
代码实现:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
private static ThreadLocal<Singleton> threadLocalSingleton = new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
public static Singleton getInstance() {
return threadLocalSingleton.get();
}
}
多线程环境模拟
@Test
public void test() throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("s2 = " + s2 + " , s1 = " + s1);
}).start();
}
Thread.sleep(1000);
}
CAS
缺点:
-
可能会产生垃圾对象
代码实现
import java.util.concurrent.atomic.*;
public class Singleton {
private static final AtomicReference<Singleton> instance = new AtomicReference<>();
private Singleton() {
}
public static final Singleton getInstance() {
while (true) {
Singleton current = instance.get();
if (current != null) {
return current;
}
current = new Singleton();
if (instance.compareAndSet(null, current)) {
return current;
}
}
}
}
总结
单例实现要点:
-
私有构造器 -
持有该类的属性 -
对外提供获取实例的静态方法
上述几种单例模式的比较
-
饿汉式:线程安全、反射不安全、反序列化不安全、非延时加载 -
懒汉式:线程不安全、反射不安全、反序列化不安全、延时加载 -
双重检测锁:线程安全、反射不安全、反序列化不安全、延时加载 -
登记时:线程安全、反射安全、反序列化不安全、延时加载 -
枚举式:线程安全、反射安全、反序列化安全、非延时加载 -
ThreadLocal:不加锁,以空间换时间,为每个线程提供独立副本,可以保证各自线程是单例的,但是不同线程之间不是单例的 -
CAS:无锁乐观策略,线程安全
以上是关于详细说说单例模式的主要内容,如果未能解决你的问题,请参考以下文章