docs: @Override
      public void addNeighbors(final Collection<TrustGraphNodeId> neighborsIn) {
          if (neighborsIn.isEmpty()) {
              return;
          }
  
          // all modification operations are serialized
          synchronized (this) {
  
              /* filter out any neighbors that are already in the routing table
               * and the new ones to the newNeighbors list
               */
              final LinkedList<TrustGraphNodeId> newNeighbors =
                  new LinkedList<TrustGraphNodeId>();
              for (TrustGraphNodeId n : neighborsIn) {
                  if (!contains(n)) {
                      newNeighbors.add(n);
                  }
              }
              
              // if there is nothing new, we're done.
              if (newNeighbors.size() == 0) {
                  return;
              }
              // handle list of length 1 the same way as a single insertion
              else if (newNeighbors.size() == 1) {
                  addNeighbor(newNeighbors.get(0));
                  return;
              }
              // otherwise there is more than one new neighbor to add.
  
              /* if there are existing routes, a random route in the 
               * table will be split.  It is picked prior to adding 
               * any of the new routes.
               */
              Map.Entry<TrustGraphNodeId,TrustGraphNodeId> split = null;
              if (!routingTable.isEmpty()) {      
                  // Pick a random existing route X->Y to split
                  split = randomRoute();
              }
  
              /* Create a random permutation of the list.
               * Add in new neighbors from this permutation, routing
               * i->i+1. These routes do not disturb any existing
               * routes and create no self references.
               */
              Collections.shuffle(newNeighbors, rng);
              final Iterator<TrustGraphNodeId> i = newNeighbors.iterator();
              TrustGraphNodeId key = i.next();
              while (i.hasNext()) {
                  final TrustGraphNodeId val = i.next();
                  routingTable.put(key,val);
                  key = val;
              }
  
              /* if there was nothing in the routing table yet, the first 
               * item in the permutation is routed to the last.
               */
              if (split == null) {
                  // loop around, bind the last to the first
                  routingTable.put(newNeighbors.getLast(), newNeighbors.getFirst());
              }
  
              /* Otherwise, split the route chosen beforehand.  Map the
               * key to the first node in the chain, and map the
               * last node in the chain to the value. ie X->Y becomes 
               * X->first->....->last->Y.  Similarly to the single 
               * neighbor add method above, this preserves the structure
               * of the routing table forming some circular chain of 
               * nodes of length equal to the size of the table.
               */
              else {
                  TrustGraphNodeId splitKey = split.getKey();
                  TrustGraphNodeId splitVal = split.getValue();
  
                  /*
                   * Add routes X-> first and last->Y. The new route last->Y is
                   * inserted first. This preserves the existing routing behavior
                   * for readers until the entire operation is complete.
                  */ 
                  routingTable.put(newNeighbors.getLast(), splitVal);
                  routingTable.replace(splitKey, newNeighbors.getFirst());
              }
  
              /* add the new neighbors into the ordering */
              addNeighborsToOrdering(newNeighbors);
          }
      }