入门
(what)是什么
所谓单例,就是在整个应用程序中,确保一个类只有一个实例,并提供一个全局访问点。
简单来说,就是一个类只能有一个对象。
(how)怎么实现
实现方式很简单:
- 重写类的默认构造方法,并把它设为private类型的,确保其他类不能调用构造方法。
- 该类提供一个方法获取对象,供其他类使用。
代码如下:
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
如果不是多线程场景应用,这样实现是没有问题的,但是多线程环境下并不能保证唯一性。最简单的解决办法是让获取对象方法同步:
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
但是同步带来的问题是性能低下,优化方案是让同步降级,不必每次调用方法马上同步:
/**
* 优化同步的实现方法
*/
public class SynCheckSingleton {
/**
* 借助volatile关键字,1.5开始支持
* 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。
*/
private volatile static SynCheckSingleton instance;
private SynCheckSingleton(){
}
/**
* synchronized降级,在需要实例化才同步
* @return
*/
public static SynCheckSingleton getInstance(){
if(instance == null){
synchronized (SynCheckSingleton.class) {
if(null == instance){
instance = new SynCheckSingleton();
}
}
}
return instance;
}
}
还可以换一个思路解决多线程造成的问题,在类加载时就实例化:
/**
* 快速实例化
*/
public class FastSingleton {
/**
* 加载类时实例化
*/
private static FastSingleton instance = new FastSingleton();
private FastSingleton(){
}
public static FastSingleton getInstance(){
return instance;
}
}
精通
(why)为什么
实现原理
要理解单例模式的实现,首先要弄懂几个知识点:
- 构造方法
- private
- static
- volatile
- synchronized
蓄意破坏单例的攻与防
以上只是在new的角度防止了多实例化,但是java中还有其他生成对象的方法:克隆、序列化、反射。那么这三种方式能否破坏单例?能破坏我们又该怎么防止呢?
- 克隆
我们知道Object中有一个clone方法,其作用是克隆对象。源码如下:
protected native Object clone() throws CloneNotSupportedException;
虽然所有的类都是Object的子类,然而要让该类能调用clone方法,必须实现Cloneable接口,所以,clone方法是不能破坏单例的。
- 序列化
序列化也可以生成对象,但是需要实现Serializable接口,同理,序列化也不会破坏单例。 - 反射
反射是可以实例化对象的,能破坏单例,而且破坏力极强,以上单例实现均可破坏,代码如下:
@Test
public void testIvokeSingleton() throws Exception {
Singleton singleton = Singleton.getInstance();
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
// 一定要先将accessible设为true
constructor.setAccessible(true);
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton == singleton2);
}
解决的方法是在构造方法中放入一个标识,第一次调用时改变值,不允许第二次调用。然后在多线程中,还要注意同步,然而这样性能又会降低,一般不会这么实现。
(more)延伸
约定和约束
依靠程序约定可以结合全局变量或者静态变量达到和单例一样的效果,但是单例是约束,通常情况下,约束比约定更有规范意义,前者最多是应该不会出错,后者是绝对不会。
使用场景
有两方面需求时都可以考虑使用单例模式:节省资源和防止混乱。
- 工具类
- 缓存
- 线程池
- 连接池
- 日志对象
- 配置文件
- 对话框