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
- 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.
- Stop the background CheckTimeThread or rewrite it so it does not drain the QUEUE (it should peek, not poll).
- Fix remove() to remove the matching Node from the queue rather than blindly polling the head.
- Replace the hand-rolled size counter with QUEUE.size() so the eviction predicate reflects queue reality, not a divergent AtomicInteger.
- 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
- Never create more than one LRUAbstractMap instance in the same JVM — the backing QUEUE is static but size is per-instance.
- Do not use this class from multiple threads — it is not thread-safe despite AtomicInteger/volatile fields.
- Consider replacing it with Caffeine or Guava Cache, which implement bounded LRU eviction correctly.
- If you fork the class, make QUEUE non-static or make size static so they share the same lifecycle.
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.