单例模式
定义:确保一个类只有一个实例对象,自行实例化并向整个系统提供这个实例,提供全局访问的方法
优点:
- 提供了唯一受控访问
- 节约资源
- 可以扩展多例模式
缺点:
- 扩展困难
- 职责过重,一定程度上违背了单一职责
- 可能会被自动回收
单例的基本实现思路:
1、单例模式不允许外部创建实例,那么我们把构造方法私有化
private Singleton() {...}
2、需要在内部定义一个静态的User类型私有成员变量
private static Singleton mInstance = null;
3、外界如何实用和实例化这个成员变量,我们增加一个静态方法
public class Singleton{
private static Singleton mInstance = null;
private Singleton() {
}
public static Singleton getInstance{
if (mInstance == null) {
mInstance = new User();
}
return mInstance;
}
}
多种单例的实现:
1、饿汉式
顾名思义,一个饿汉,没等饭好呢,就在哪里等着了
优点:在类加载的时候就会去创建单例对象,线程安全
缺点:没有用到他的时候就创建了,浪费系统资源
public class Singleton {
// 加载该类时,单例就会自动被创建
private static Singleton mInstance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return mInstance;
}
}
2、懒汉式
顾名思义,一个懒汉,等饭好了,他才过来拿饭
优点:被使用的时候才被创建,节省系统资源
缺点:因为对象初始化需要时间,线程不安全,可能会创建多个单例,上了同步锁后也会导致效率不高
public class Singleton{
public static Singleton mInstance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
}
3、懒汉式加锁
在方法上直接加锁开销比较大
public class Singleton{
public static Singleton mInstance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (mInstance == null) {
mInstance = new Singleton();
}
return mInstance;
}
// 这个和在方法名前上锁一样
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
return mInstance;
}
}
4、懒汉式改进(双重校验锁)
改进的懒汉,既实现了线程安全,又不会多余的调用同步锁
public class Singleton{
public volatile static Singleton mInstance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (mInstance == null) {
synchronized(Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
}
双重校验锁这边有两个关键问题:
为什么需要双重判空?
首先双重校验锁这种写法我们在需要的时候才去初始化,然后里面的判空是为了保证线程安全,外面的判空是保证了初始化之后我们getInstance()的时候不再进行同步锁,避免不必要的同步,提高性能
为什么intance需要用volatile来修饰?
intance = new Instance();
这并不是一个原子操作(原子操作是指执行顺序不会被切割、打乱)
实际上new一个对象,我们是有三个步骤:
分配内存空间 // 1
初始化对象 // 2
将内存地址分配给变量 // 3
因为JVM可能会对它们进行重排序,所以执行的步骤可能会是132,这就可能会出现一个问题,在执行完3还没执行2的时候,intance指向的是一个没有初始化的对象,此刻另外一个线程进来以后可能会出现访问异常,拿到的是没有初始化完成的半个对象,用volatile通过内存屏障可以来禁止指令重排序,其次可以保证内存可见性,一个线程对变量进行修改,可以立马被其他线程可见,从而避免这种错误的发生
变量存储在主内存中,每个线程是将数据读取到自己的工作内存中,然后在工作内存中进行计算后再写入主内存中,很有可能在某个线程写入主内存之前,其他线程读取了还没修改的数据,导致出现错误。
那么volatile通过添加内存屏障,保证读入前能拿到最新的数据,写入之后其他线程能获得最新值
5、静态内部类
根据 静态内部类 的特性,同时解决了按需加载、线程安全的问题,同时实现简洁
1、外部类调用getInstance()
2、自动调用UserHolder.mInstance
- UserHolder被初始化
- 类加载时初始化静态域
- 静态域JVM只会加载一次,保证了线程安全
3、只创建了一个单例
public class Singleton{
private static class SingletonHolder{
private static Singleton mInstance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.mInstance;
}
}
6、枚举型
枚举型的优势就是写法简单,也可以拥有字段和方法,而且默认枚举是线程安全的,反序列化的时候依然会返回之前的那个对象,也不用担心会生成新的实例
public enum Singleton{
INSTANCE;
}
非枚举的情况如果要杜绝单例对象在被反序列化时重新生成对象,必须加入readResolve()方法:
private Object readResolve() throws ObjectStreamException {
return instance;
}
7、使用容器
将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象,我们可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度
public class SingletonManager {
public static HashMap<String, Object> objMap = new HashMap<>();
private SingletonManager(){
}
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key))
objMap.put(key, instance);
}
public static Object getService(String key) {
return objMap.get(key);
}
}
总结
单例模式差不多就是这些,在学校的时候看单例还总是模模糊糊,懵懵懂懂,工作了一段时间后,再回过头来看看真实十分的清晰,学任何东西其实光看还是不行,就得去实践,学习得和实践结合在一起,才是最好的。
无论哪种到单例模式,核心的原理都是将构造函数私有化,通过静态方法获取唯一的一个实例,在获取的过程中必须保证线程安全、防止反序列化导致重新生成对象等问题
我们平时开发当中常用到的app中管理用户信息的类UserManager可能就需要单例实现、再比如关于地址省市区的编码转换之类的可能也需要一个单例的类来管理、一个app只有一个Applicationd对象,微信小程序中的app对象也只有一个。
分割线----------------------------
借用Android源码设计模式解析与实战一书中的内容,再理解一下单例模式
// listview中常见LayoutInflater
view = LayoutInflater.from(context).inflate(layoutId, null);
// from方法内部通过getService方法获得对象
public static LayoutInflater from(Context context) {
LayoutInflater layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// ...
return layoutInflater;
}
书中还分析了一些源码,这里我就不贴了,其实看到上面就能知道是通过容器来实现单例模式了。虚拟机第一次加载时会注册各种ServiceFatcher,这些服务以键值对的形式存储在一个HashMap中,用户在使用时只需要根据key来获取到对应的对象即可
作者继续将书中的ImageLoader用单例模式写了一下,书中用的是一个双重校验锁的模式,这里不重复写了
总结
通常在app这边并没有高并发的情况,选用哪种方式没多大的影响,不过一般还是选择双重校验锁和静态内部类的两种写法
优点:
- 当一个对象需要频繁的创建、销毁时,使用单例模式很有优势
- 当一个对象的产生需要比较多的资源时,比如读取配置、产生其他依赖对象等,可以在应用启动的时候直接生成一个单例对象
- 单例可以避免对资源的多重占用,避免多个对象同时执行写文件操作
- 单例模式更多的是设置一个全局的访问点,优化和共享资源访问
缺点:
- 单例模式没有接口,扩展困难,而且可以说是违背了单一职责
- 注意不要持有Context对象,容易引发内存泄露,传递给单例的最好是Application Context