暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Collection 太快受不了 - 集合迭代稳定性

阿哲是哲学的哲 2021-02-05
2543

一. 问题的产生

什么是迭代稳定性?

对同个集合 高频的读操作, 可能会读到 修改一半的 状态结果 .例如: 在一次 put 10 个 值的时候, put到第5个的时候, 对集合进行了读操作, 此时读到的数据就不是一致性的, 读到了一半的数据.


这么说懂了吧兄嘚


Eurka源码中的具体表现

集合想必大家都知道, 不就是 Collection 包下的集合对吧, 稳定性又是从何说起呢? 下面咱们看一下Eurka中的源码

Eurka中 的 AbstractInstanceRegistry 中 针对 recentlyChangedQueue 最近修改队列 和 registry 注册表的迭代稳定性, 而选择用 读写锁做读写分离 .

    //所有注册实例集合
    private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
    = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
    //被修改过的实例集合,用于增量更新实例是读取
    private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();


    // 注册方法使用了读锁
    public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication){
    read.lock();
    ...
    gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); // 往注册表内添加实例
    ...
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }
    // 取消实例的注册使用读锁
    protected boolean internalCancel(String appName, String id, boolean isReplication) {
    read.lock();
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToCancel = null;
    if (gMap != null) {
    leaseToCancel = gMap.remove(id); // 这里开始去注册表中移除 实例
    }
    .....
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }
    //更新实例的状态。使用读锁
    public boolean statusUpdate(String appName, String id,
    InstanceStatus newStatus, String lastDirtyTimestamp,
    boolean isReplication) {
    read.lock();
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> lease = null;
    if (gMap != null) {
    lease = gMap.get(id); //获取要修改的实例
    }
    ....
    lease.serviceUp(); // 对要修改的实例进行某个状态的修改
    ....
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }
    // 下线状态赋值给实例
    public boolean deleteStatusOverride(String appName, String id,
    InstanceStatus newStatus,
    String lastDirtyTimestamp,
    boolean isReplication) {
    read.lock();
    // 对实例进行状态修改 , 修改为下线状态
    recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel)); //添加最近修改队列
    }


    //获取所有 增量实例Applications (这里是用的写锁)
    public Applications getApplicationDeltas(){
    write.lock();
    Iterator<RecentlyChangedItem> iter = this.recentlyChangedQueue.iterator(); //返回最近改变的队列, 即为增量的实例
    }


    • 上述代码中我们可以看到. Eurka 使用了读/写锁的添加方式就是为了解决两个共享集合 recentlyChangedQueue 与注册表 registry 的“集合迭代稳定性”问题。(当然细心的你可能会发现, 为啥这里  读的地方用了写锁, 写的地方使用了读锁呢?  这里咱先留个小预告, 请期待下一篇.)

    二 . 解决思路

    • 读写分离, 例如 有一个 OnlyreadMap 和 readwirdMap 进行读写分离, OnlyreadMap 定时的从 readwirdMap 中更新数据.

      当然, 如果在定时更新的时候也发生了读操作, 那么此时可以用读写锁进行限制

    • 读写锁, 做读写分离 rock.wird(); rock.read();

    三 . 示例

    1. 读写锁: 就是在更新前加入   做读写分离 rock.wird(); rock.read();   上面代码已经演示了, 这里咱就不写了

    2. 读写分离

      //被修改过的实例集合,用于增量更新实例是读取
      private ConcurrentLinkedQueue<Object> map = new ConcurrentLinkedQueue<>();


      public ConcurrentLinkedQueue getMap(){
      return this.map;
      }


      public void uploadMap(Object o){
      ConcurrentLinkedQueue copyMap = new ConcurrentLinkedQueue();
      copyMap.addAll(map); //将当前的map 拷贝一份
      for ( o in copyMap){
      // dosomething... 开始遍历迭代或者一些耗时较长的操作
      }
      this.map = copyMap; // 修改完成后直接替换 最新的 集合
      }

      四 . 总结

              对于多个地方都会修改到的集合, 建议加上读写锁, 进行读写分离. 必要的话也可以加上 synchronized 或者JUC包下线程安全的集合. 进行读写

              直接指向替换是一个非常快的操作, 能快速的将一个集合"更新"变成另外一个集合, 可以利用这一点, 对集合进行快速的改变 (相比起你一个一个去迭代去改, 修改中的状态会少很多对吧).


      今天七夕, 如果你看到这里, 能不能给个关注呢


      点个赞 👍 再走也行呐

      文章转载自阿哲是哲学的哲,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

      评论