介绍
好的设计是性能的关键,能带来程序“质”的优化,比如今天的单例模式,虽然非常简单,但是项目内使用非常多。
适合场景
将频繁使用的类,特别是创建需要花费大量时间或者内存的类,设置为单例模式,可以提高运行速度、同时可以减少GC压力
实现方式
单例模式的实现方式比较多,今天介绍主要的5种,以及他们的优缺点:恶汉模式、懒汉模式、内部类实现、枚举实现以及反射实现单例
恶汉模式
public class Singleton {
private Singleton() {
//创建单例的过程可能会比较慢
System.out.println("Singleton is create");
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
- 缺点是在系统启动的时候就加载,增加了系统启动时间,同时过早的消耗了内存
- 但如果类本身就需要在系统启动后立即使用,推荐使用此模式
懒汉模式(双锁机制 + volatile)
public class LazySingleton {
private LazySingleton(){
//创建单例的过程可能会比较慢
System.out.println("LazySingleton is create");
}
private static LazySingleton instance = null;
public static LazySingleton getInstance() {
if (instance == null){ //代码 3
synchronized(LazySingleton.class){ //这里同步其他对象也可以 代码 2
if(instance == null){ //代码1
instance = new LazySingleton();
}
}
}
return instance;
}
}
- 如果类创建非常耗时的时候,可以做延迟加载,提高系统启动速度
- 同时为了防止多线程,多次创建“LazySingleton ”类,增加双锁机制 + volatile,从而解决每次访问需要加锁的效率
- 同步代码块在代码里面判断里面,所以不会影响之后调用的效率,如非第一次和第二次调用存在竞争关系,synchronized是偏向锁,也不会影响效率,所以懒汉模式获取对象的效率极高
双锁机制+volatile
这里额外的提一下,为啥要用双锁机制+volatile
- 双锁机制,可以防止第一次访问的代码运行在代码1和代码3之间时,而第二次及之后的访问的代码已经运行了代码3,处于等待锁的情况。如果没有代码1的第二次校验,类将会创建多次
- volatile的作用呢?那就是更小的错误概率了,主要是类的创建过程非原子性,高并发情况下可能出现指令重排序,其它线程可能会拿到属性为空的对象,而误认为已经已经创建完成,而volatile可以防止代码重排序。
内部类实现
public class StaticSingleton {
private StaticSingleton(){
System.out.println("StaticSingleton is create");
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
- 将类加载放在内部类,既可以延迟加载类,又可以防止多线程多次创建类
- 缺点:使用反射,可以强行调用私有构造方法。如果使用反序列化得到的不是同一个类,违背了单例模式的原则
优化反序列化问题
- 序列化是,往内存地写再从内存里读出而"组装"成一个跟原来一模一样的对象,但可能不是用一个对象了
- 所以不能保证单列,所以我们需要单独处理,在对象中添加如下的一个方法
//这样当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证.
protected Object readResolve(){
return MyObjectHandler.myObject;
}
枚举实现
public enum Singleton {
INSTANCE;
public void introduction() {
System.out.println("This is a singleton pattern about enumeration");
}
}
public static void main(String[] args) {
Singleton.INSTANCE.introduction();
}
- 利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的。
- 枚举可以防止反序列化和反射创建对象,从而保证了对象的唯一性
反射实现
- 创建了一个private的构造方法的对象,因此不能通过new的方式获得对象
class Singleton {
private Singleton() {
System.out.println("****** Singleton类的构造方法 ******");
}
public void print() {
System.out.println("Bonjour~Bridge");
}
}
sun.misc.Unsafe类
可以利用反射来获取对象,直接利用C++来代替jvm执行,即:可以绕过JVM的管理机制,如果一旦使用了Unsafe,就不能用上JVM的内存管理机制以及垃圾回收处理
列表 1 | 列表 2 |
---|---|
构造方法 | private Unsafe() {} |
私有常量 | private static final Unsafe theUnsafe = new Unsafe(); |
- 从上面可知,要强获得Unsafe对象,就必须用反射机制
public static void main(String[] args) throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe") ; // 获取成员
field.setAccessible(true); // 解除封装
// 这里以及获得一个Unsafe对象了
Unsafe unsafeObject = (Unsafe) field.get(null) ; //原本的Field需要接收实例化对象,但是Unsafe不需要
//下面我要利用Unsafe获得一个Singleton对象
Singleton instance = (Singleton) unsafeObject.allocateInstance(Singleton.class) ; // 获取对象,不受JVM控制
instance.print();// 调用类中的方法
}
- 上面Unsafe绕过了JVM实例化的管理,上面有增加了一种单例模式的实现方法,但是这种方法不建议使用,只是提供给大家一个学习的参考
总结
- 项目中用的最多还是双重校验+volatile的懒汉模式
- 另外很多博主和书集都支持使用枚举的方式实现,因为他足够安全,且保证了对象的唯一