crossoverJie/JCSprout · error · RuntimeException

data error

Error message

data error

What it means

Thrown inside sizeUp() of the custom LRUAbstractMap when the internal size counter has reached MAX_SIZE (1024) but QUEUE.poll() returns null — an invariant violation signalling that the AtomicInteger size tracker and the backing ArrayBlockingQueue have drifted out of sync. The map is not actually thread-safe despite using AtomicInteger/volatile, and several code paths drain the queue independently of the size counter, so the two can (and do) diverge under real use.

Source

Thrown at src/main/java/com/crossoverjie/actual/LRUAbstractMap.java:248

    }

    /**
     * 增加size
     */
    private void sizeUp(){

        //在put值时候认为里边已经有数据了
        flag = true ;

        if (size == null){
            size = new AtomicInteger() ;
        }
        int size = this.size.incrementAndGet();
        if (size >= MAX_SIZE) {
            //找到队列头的数据
            Node node = QUEUE.poll() ;
            if (node == null){
                throw new RuntimeException("data error") ;
            }

            //移除该 key
            Object key = node.key ;
            remove(key) ;
            lruCallback() ;
        }

    }

    /**
     * 数量减小
     */
    private void sizeDown(){

        if (QUEUE.size() == 0){
            flag = false ;
        }

View on GitHub (pinned to fc4c6e5f6d)

Solutions

  1. If you must use this class, restrict to a single instance and single-threaded access — the static QUEUE / per-instance size split makes multi-instance use fundamentally broken.
  2. Stop the background CheckTimeThread or rewrite it so it does not drain the QUEUE (it should peek, not poll).
  3. Fix remove() to remove the matching Node from the queue rather than blindly polling the head.
  4. Replace the hand-rolled size counter with QUEUE.size() so the eviction predicate reflects queue reality, not a divergent AtomicInteger.
  5. Prefer ConcurrentHashMap + ConcurrentLinkedDeque, or Caffeine/Guava Cache, which handle bounded LRU eviction correctly under concurrency.

Example fix

// before (broken: static queue, per-instance size, blind poll in remove)
private final static ArrayBlockingQueue<Node> QUEUE = new ArrayBlockingQueue<>(MAX_SIZE);
private volatile AtomicInteger size;
// sizeUp checks size >= MAX_SIZE then QUEUE.poll() → null → throws

// after (queue and size co-located, single lock)
private final ArrayDeque<Node> queue = new ArrayDeque<>();
private final Object lock = new Object();

private void sizeUp() {
    synchronized (lock) {
        if (queue.size() >= MAX_SIZE) {
            Node node = queue.pollFirst();
            if (node != null) {
                remove(node.key);
                lruCallback();
            }
        }
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// This class is not safe for concurrent or multi-instance use.
// Best validation: avoid it. If you must, at least check coherence.
LRUAbstractMap map = new LRUAbstractMap();
// There is no public queue-depth accessor; the only pre-check is size(),
// which does NOT reflect the static QUEUE. Wrap every put in try-catch.

Try / catch

try {
    map.put(key, value);
} catch (RuntimeException e) {
    if ("data error".equals(e.getMessage())) {
        // size counter and queue are out of sync — recreate the map instance
        LOGGER.error("LRU map invariant violated, size counter desynced from queue", e);
        map = new LRUAbstractMap(); // last resort: reset state
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling put() repeatedly until size >= 1024 at a moment when the shared static QUEUE is already empty. This is almost guaranteed because: (a) QUEUE is declared static final (shared across all LRUAbstractMap instances) while the size counter is per-instance, so a second instance's eviction polls a queue it never populated; (b) remove() unconditionally calls QUEUE.poll() on the head regardless of which key was removed, corrupting the mapping; (c) the background CheckTimeThread continuously polls QUEUE.poll() without offering back, draining entries that the size counter still thinks exist.

Common situations: Creating more than one LRUAbstractMap instance in the same JVM (static queue, per-instance size). Concurrent put/remove from multiple threads. Long-lived instances where the daemon CheckTimeThread has had time to drain the queue. Any production use beyond a single-threaded, single-instance toy demo.

Related errors


AI-assisted analysis of crossoverJie/JCSprout@fc4c6e5f6d (2026-08-14). Data as JSON: /api/errors/a947b6001866a7e3. Report an issue: GitHub.