简化开发的单例依赖注入Singleton-ID

 

 

 

单例模式

所谓的单例其实就是在整个系统环境中,你的实例有且只有一个,保证在任何上下文中都能获取这个实例,基本上等于一个在堆上的全局数据,生命周期<=应用周期。单例的应用很广泛,比如各种系统的管理器,读取配置文件的配置实例,工具类等。

在Java中,一个简单的单例模式可以这样实现:

private static Object instance = new Object();
public static Object me(){
  return instance;
}

这种只是最简单的实现方式,他有很多缺点,比如在想用到的时候再实例化出来(惰性加载),就必须在获取的时候初始化

public static Object me() {
    if (instance == null) {
        instance = new Object();
    }
    return instance;
}

了解多线程的同学可能就会问,这里如果在多线程的情况下,一个线程判断==null的时候通过了,CPU突然调度到其他线程,由于new的操作还没执行,所以其他线程也会通过==null的判断,这样子数据就不安全了,怎么办,加锁呗。

public static Object me() {
    synchronized (Singleton.class) {
      if (instance == null) {
          instance = new Object();
      }
      return instance;
    }
}

虽然Java新版本当中对于synchronized做了优化,他会从乐观锁的形式慢慢变为悲观锁,虽然优化了,但是单例大概率是会被频繁调用的,这个锁迟早会变为悲观锁,每次调用me()就会锁住,其他线程等待,性能完全不能接受。(乐观锁和悲观锁,感兴趣的可以去百度百度,不感兴趣的就直接理解为:乐观锁快,悲观锁慢)。

线程知识扎实的同学可能会想到一种优化,就是先在不锁的情况判断instance是否为空,为空才锁住接下来的代码,保证安全的创建。

public static Object me() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Object();
            }
        }
    }
    return instance;
}

这种做法有个专业的名称叫做Double-Check,双重检查。实际上他还会有问题,因为你始终未保证instance变量的值对于各个线程是否是最新的,只是保证了创建实例的过程不被打扰。这是为什么呢?我这里简单比喻一下,线程其实有一个线程栈,他为了保证速度,都是优先读取他自己的变量副本,在某些情况下,由于指令重排的机制,他会在值更新的时候先读取到了老值,instance还是可能在某个时候不是正确的值。所以我们需要在变量的声明处,加上volatile关键字,禁止指令重排。

private static volatile Object instance = new Object();

其实还有更优的方式,只不过要牺牲一些代码的灵活性,比如用内部类来持有instance,或者直接使用枚举,具体的可以百度,这里就不多讲。

为什么我很执着的要讲单例,其实看题目就知道,我们需要集体管理一组单例,并且让他们可以做到依赖注入(IOC)。

依赖注入(Dependency Injection)

听这个名字很高大上,其实非常简单,就是我不需要new这个操作或者从其他地方来获取这个变量了,直接声明就可以使用。举个简单的例子,我们有个 UI管理器UIManager,里面有打开UI和关闭UI的操作,对于很多模块来讲,比如战斗涉及血条之类的变化会用到UIManager,任务更新需要用到UIManager,简直太多了。简单点的写法我们可以把UIManager做成一个单例,在获取的地方这样用

 

UIManager uiManager = UIManager.me();
uiManager.open("task_panel");
uiManager.updateData("task_panel",data);
uiManager.setDisable("award_button",false);

更暴力点的情况

UIManager.me().open("task_panel");
UIManager.me().updateData("task_panel",data);
UIManager.me().setDisable("award_button",false);

没有最丑,只有更丑。

我这里提一点,可能UI管理器对于服务器来说意象有点偏差,但是道理是一样的。这里我必须要提一下,在做服务器的时候,为了能热加载,我们通常会把程序分为两个子项目,一个是数据层data,一个是逻辑层logic。logic是存放游戏大部分逻辑的地方,以jar包的形式被data加载,热更新的原理就是替换这个jar包。为了应付这种情况,我们的UIManager就只有写在logic层,而所有的Manager我们需要集体管理,好控制jar包被重加载时处理一些事情。比如我们有个ManagerPool,专门存放这些Manager,那调用的代码就更冗长了。

 

ManagerPool.me().getUIManager().open("task_panel");
ManagerPool.me().getUIManager().updateData("task_panel",data);
ManagerPool.me().getUIManager()setDisable("award_button",false);

这不是我们想要的,最理想的情况是什么呢?

 

private UIManager uiManager;

public void onTaskUpdate(TaskData data){
  uiManager.open("task_panel");
  uiManager.updateData("task_panel",data);
  uiManager.setDisable("award_button",false);
}

接下来我们就需要在合适的时候把 UIManager的实例赋给uiManager了,所有单例统一管理。

private final Map<Class<?>, Object> instances = new ConcurrentHashMap<>();

利用两个注解来为我们辨别 1.此类是否需要被容器管理成单例 2.这个字段是否需要注入实例。

/**
 * 需要指定为单例的类
 *
 * @author hank
 */
@Target({ElementType.TYPE})
@Retention(RUNTIME)
@Documented
public @interface Singleton {
}
/**
 * 引用单例的字段
 *
 * @author Hank
 * @version 2017/11/11 21:06
 */
@Target({FIELD})
@Retention(RUNTIME)
@Documented
public @interface Ref {
}

接下来我们就去查找所有带@Singleton注解的类,并把它们全部一次性实例化,然后存储起来,在全部查找一次他们的字段,如果发现有引用,就利用Java的反射技术,在刚才存储起来的单例去寻找合适的实例去赋值。先全部实例化再注入字段就是防止相互引用和初始化先后顺序的问题。

/**
  * 寻找单例
  *
  * @return
  */
 public Singletons search(ClassLoader classLoader) {
     //寻找所有有@Singleton注解的类
     List<Class<?>> classes;
     try {
         classes = Conditions.notNull(ReflectionUtil.getClasses("", classLoader, null));
     } catch (IOException | ClassNotFoundException e) {
         LOGGER.error("reflect error", e);
         return this;
     }
     for (Class<?> clazz : classes) {
         Singleton annotation = clazz.getAnnotation(Singleton.class);
         if (annotation == null) {
             continue;
         }
         Object singletonInstance = null;
         try {
             singletonInstance = clazz.newInstance();
         } catch (InstantiationException | IllegalAccessException e) {
             LOGGER.error("can not create instance of {} from an empty constructor", clazz.getName());
         }
         Conditions.notNull(singletonInstance);
         instances.put(clazz, singletonInstance);
         LOGGER.trace("find singleton class:{}", clazz.getName());
     }
     // 全部遍历一次,注入所有需要被注入的字段
     for (Object obj : instances.values()) {
         injectByFieldAndMethod(obj);
     }
     return this;
 }
private void injectByFieldAndMethod(Object obj) {
       Field[] fields = obj.getClass().getDeclaredFields();
       for (Field field : fields) {
           Ref annotation = field.getAnnotation(Ref.class);
           if (annotation == null) {
               continue;
           }
           if (!field.isAccessible()) {
               field.setAccessible(true);
           }
           Class<?> type = field.getType();
           if (!instances.containsKey(type)) {
               throw new NullPointerException("can not find singleton :" + type.getName());
           }
           try {
               field.set(obj, instances.get(type));
           } catch (IllegalAccessException e) {
               LOGGER.error("reflect error", e);
           }
           LOGGER.trace("inject field {} into {}", type.getName(), obj.getClass().getName());
       }
   }

最后是通过Class来获取实例

 

/**
 * 获取实例
 *
 * @param clazz
 * @param <T>
 * @return
 */
public <T> T instance(Class<T> clazz) {
    if (instances.containsKey(clazz)) {
        return (T) instances.get(clazz);
    } else {
        try {
            T t = clazz.newInstance();
            LOGGER.trace("create instance :{}", t);
            injectByFieldAndMethod(t);
            return t;
        } catch (InstantiationException | IllegalAccessException e) {
            LOGGER.error("instance error", e);
        }
    }
    return null;
}

在Singleton类的内部,直接声明并@Ref那个字段,如果在Singleton类的外部,则拿到这个容器,并调用instance获取就行了,用容器获取instance其实是非常少用的,因为大部分逻辑都活跃在这些Singleton之间,所以最后是很好的达到了简化代码的效果,当然我们这里举例的只是特别特别轻量并且特殊的例子,如果你觉得不够用,我推荐你可以试一下Google的Guice,非常好用,非常方便。

@Singleton
public class SingletonA {
    @Ref
    SingletonB singletonB;

    public void say() {
        System.out.println(singletonB);
    }
}




@Singleton
public class SingletonB {
    @Ref
    SingletonA singletonA;

    public void say() {
        System.out.println(singletonA);
    }
}

声明两个单例,然后调用他们

public class SingletonDemo {
    @Ref
    SingletonB singletonB;
    @Ref
    SingletonA singletonA;
    public void say(){
        singletonB.say();
        singletonA.say();
    }
    public static void main(String[] args) {
        Singletons.create().search(Thread.currentThread().getContextClassLoader()).instance(SingletonDemo.class).say();
    }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注