玩转单例模式

玩转单例模式
1 什么是单例模式
单例模式是指,在运行期间,保证某个类只创建一个实例,且这个对象是全局的,可以给别的类访问。
2 单例模式的使用场景
2.1 使用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。
应用场景举例:
1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
2. Windows的Task Manager(任务管理器)就是很典型的单例模式,你能打开两个windows task manager吗?
3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

2.2 Java中单例模式与普通工具类的对比
在Java中工具类一般是这么实现的,以求绝对值为例
public class Demo {
static int lastNum;

public static int abs(int num) {
	if(num<0) {
		lastNum=-num;
		return lastNum;
	}
	lastNum=num;
	return lastNum;
}

}调用时不用创建对象直接Demo.abs(num),然后求出的lastNum是全局的,可以被别的类访问,但这么做的话,就无法在调用方法时使用非静态的对象了,这个时候就可以使用单例模式了,比如用用最简单的饿汉式来实现上面的例子。
public class Demo {
private static Demo instance;
int lastNum;

private Demo () {
}
public static Demo getInstance() {
    if (instance == null) {
        instance = new Demo ();
    }
    return instance;
}
public int abs(int num) {
	if(num<0) {
		lastNum=-num;
		return lastNum;
	}
	lastNum=num;
	return lastNum;
}

}
可以看到这么写后abs就不是一个静态方法了,那么可以使用一些和业务相关的,非静态的对象了。
总结一下,当我们需要一个类提供方法,全局地保存状态,就可以使用工具类或者单例,当这个类与业务无关,不使用非静态对象时,使用工具类即可。当这个类与业务相关,需要使用别的非静态的类时,就要使用单例模式了。

3 Java实现单例模式
3.1 饿汉式
public class Test {
private Test() {
}
public static Test instance = new Test();
public static Test getInstance() {
return instance;
}
}

优点
1.线程安全
2.在类加载的同时已经创建好一个静态对象,调用时反应速度快
缺点
new Test()在类加载时就执行了(类加载即使用这个类的时候),这意味着,即使不使用这个对象时,这个对象依然会占用存,我们可以把上面这个例子改的更直观一些。
public class Test {
static int num;
int[] arr =new int[1024*1024];
private Test() {
System.out.println(“Test”);
}
public static Test instance = new Test();
public static Test getInstance() {
return instance;
}
}
public class Main {

public static void main(String[] args){
	Test.num=1;

}
}
输出如下;

可以看到Test.num=1,触发了类加载,从而触发了构造函数,arr就被创建了,白白占用了4MB的内存,如果对内存要求较高,那么可以考虑使用懒汉式
3.2 懒汉式
首先是最容易想到的写法,注意构造函数为私有
public class Demo {

private static Demo instance;

private Demo () {
System.out.println(“new a Demo”);
}

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

}但这么写在多线程并发时存在问题,我们可以做一个实验
public static void main(String[] args) {
// TODO Auto-generated method stub

	for(int i=0;i<6;i++) {
		MyThread deviceLoginThread = new MyThread();
        Thread t = new Thread(deviceLoginThread);
        t.start();
	}
}

public class MyThread implements Runnable{
public void run() {
Demo.getInstance();
}
}
几乎在同时开启了六个线程,结果如下,对象被创建了六次,单例失效了

原因是,六个线程进入instance == null这句话时,对象没有被创建,于是都进入了if语句块内,创建了对象,因此,这么写在多线程下是行不通的。
最容易想到的修改的方法是直接加同步关键字。
public class Demo {

private static Demo instance;

private Demo () {
	System.out.println("new a Demo");
}

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

}
结果如下

可以看到只有一个Demo被创建,单例又生效了,但是这么写在getInstance之前先要获得锁,在高并发时效率较低,于是想到能不能避免直接获得锁,就有了双重检查。
public class Singleton {

private static Singleton singleton;

private Singleton() {}

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

}
如果对象已经不为空则直接返回该对象,这样就可以避免获得锁,提高了效率。但是这么写依然存在问题,对于singleton = new Singleton();,这不是一个原子性的操作,它在执行时会被拆分为三步
1 分配内存空间
2 执行构造方法,初始化对象,即给内存空间赋值
3 singleton这个对象的引用指向这个内存空间
它们执行的顺序并不总是我们指定的123,cpu为了优化执行的效率,也可能把顺序调整为132,这就是指令重排。现在假设有两个线程A和B,A按照132的顺序执行完了3,然后B上处理机运行进行singleton == null的判断,注意,由于A已经执行了3,B的判断会为false,然后return singleton,但此时的singleton对象是未被初始化的对象,就会引发NPE异常。
解决的方法是加上volatile的关键字,即private static Singleton singleton;
Volatile可以保证指令执行顺序与指令声明的顺序一致,不会发生指令重排。
除了双重检查之外,还有一种可以保障线程安全的懒汉式,就是使用内部匿名类。
public class Singleton {

private Singleton() {}

private static class SingletonInstance {
    private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
    return SingletonInstance.INSTANCE;
}

}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

3.3 枚举类实现单例
在之前的探讨中已经得到了双重检查和内部匿名类两种优秀的单例实现,但他们依然存在问题,那就是不能防止反射和序列化。这里主要讲枚举类如何防止反射的攻击。
3.3.1 私有化构造器并不保险
effective java中这么说明:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”下面我们做一个实验:
public class Main {

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Demo s=Demo.getInstance();
    Demo sUsual=Demo.getInstance();
   Constructor<Demo> constructor=Demo.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Demo sReflection=constructor.newInstance();
    System.out.println(s+"\n"+sUsual+"\n"+sReflection);
    System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));
    System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));

}

}输出为:
com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@677327b6
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false

3.3.2 枚举单例定义
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
枚举类和正常的类一样,可以有实例变量,实例方法,静态方法等等,只不过它的实例个数是有限的,不能再创建实例而已。由于EnumSingleton只有一个实例INSTANCE,且这个实例是全局的,那么它就是满足单例的,同时由于这个对象只在类加载的时候初始化一次(即它是饿汉式),它是线程安全的。
3.3.3 测试
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingleton singleton1=EnumSingleton.INSTANCE;
EnumSingleton singleton2=EnumSingleton.INSTANCE;
System.out.println(“正常情况下,实例化两个实例是否相同:”+(singleton1singleton2));
Constructor constructor= null;
// constructor = EnumSingleton.class.getDeclaredConstructor();
constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
constructor.setAccessible(true);
EnumSingleton singleton3= null;
//singleton3 = constructor.newInstance();
singleton3 = constructor.newInstance(“testInstance”,66);
System.out.println(singleton1+“\n”+singleton2+“\n”+singleton3);
System.out.println(“通过反射攻击单例模式情况下,实例化两个实例是否相同:”+(singleton1
singleton3));
}
执行结果为

可见constructor.newInstance(“testInstance”,66)是报了不能反射枚举类的错误。这说明枚举类确实可以防止反射破坏单例。原因如下
@CallerSensitive
2 public T newInstance(Object … initargs)
3 throws InstantiationException, IllegalAccessException,
4 IllegalArgumentException, InvocationTargetException
5 {
6 if (!override) {
7 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
8 Class<?> caller = Reflection.getCallerClass();
9 checkAccess(caller, clazz, null, modifiers);
10 }
11 }
12 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
13 throw new IllegalArgumentException(“Cannot reflectively create enum objects”);
14 ConstructorAccessor ca = constructorAccessor; // read volatile
15 if (ca == null) {
16 ca = acquireConstructorAccessor();
17 }
18 @SuppressWarnings(“unchecked”)
19 T inst = (T) ca.newInstance(initargs);
20 return inst;
21 }
请看第12行源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
4 总结
单例模式是指,在运行期间保证某个类只创建一个全局实例,如果这个类与业务无关,使用工具类即可,如果与业务相关,则需要使用单例,如果对内存要求不高使用饿汉式即可,在饿汉式中如果该类不继承别的类,那么推荐使用枚举。如果对内存要求较高,那么推荐懒汉式,在懒汉式中,推荐使用双重检查或者内部静态类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值