This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 24955094ee88 CAMEL-23686: Replace ConcurrentHashMap with FlatMap for 
exchange properties
24955094ee88 is described below

commit 24955094ee88f96ac2403266393e71d564db1dc7
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Jun 5 18:19:20 2026 +0200

    CAMEL-23686: Replace ConcurrentHashMap with FlatMap for exchange properties
    
    Exchange properties used ConcurrentHashMap since CAMEL-715 (2008) to
    avoid ConcurrentModificationException in the old thread() DSL. The
    modern routing engine ensures single-threaded exchange access, and
    headers already use a non-thread-safe map, so this is unnecessary.
    
    Replace with FlatMap — a Map backed by a flat Object[] with alternating
    key/value pairs. For the typical 2-5 exchange properties, linear scan
    is faster than hashing and allocates only a single array instead of
    HashMap table + Node objects.
    
    Also make properties lazy in DefaultPooledExchange (was eagerly created
    in all 3 constructors even though many pooled exchanges never use user
    properties).
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../org/apache/camel/support/AbstractExchange.java |  13 +-
 .../camel/support/DefaultPooledExchange.java       |   6 -
 .../java/org/apache/camel/support/FlatMap.java     | 247 +++++++++++++++++++++
 3 files changed, 253 insertions(+), 13 deletions(-)

diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
index 2674beae335f..e14b699d37cb 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
@@ -22,7 +22,6 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.camel.AsyncCallback;
@@ -285,7 +284,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
         } else if (value != null) {
             // avoid the NullPointException
             if (properties == null) {
-                this.properties = new ConcurrentHashMap<>(8);
+                this.properties = new FlatMap<>(4);
             }
             properties.put(name, value);
         } else if (properties != null) {
@@ -357,13 +356,13 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     @Override
     public Map<String, Object> getProperties() {
         if (properties == null) {
-            this.properties = new ConcurrentHashMap<>(8);
+            this.properties = new FlatMap<>(4);
         }
         return properties;
     }
 
     private Map<String, SafeCopyProperty> copySafeCopyProperties() {
-        Map<String, SafeCopyProperty> copy = new ConcurrentHashMap<>();
+        Map<String, SafeCopyProperty> copy = new 
FlatMap<>(this.safeCopyProperties.size());
         for (Map.Entry<String, SafeCopyProperty> entry : 
this.safeCopyProperties.entrySet()) {
             copy.put(entry.getKey(), entry.getValue().safeCopy());
         }
@@ -948,7 +947,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     public void setSafeCopyProperty(String key, SafeCopyProperty value) {
         if (value != null) {
             if (safeCopyProperties == null) {
-                this.safeCopyProperties = new ConcurrentHashMap<>(2);
+                this.safeCopyProperties = new FlatMap<>(2);
             }
             safeCopyProperties.put(key, value);
         } else if (safeCopyProperties != null) {
@@ -1001,7 +1000,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     @Override
     public void setProperties(Map<String, Object> properties) {
         if (this.properties == null) {
-            this.properties = new ConcurrentHashMap<>(8);
+            this.properties = new FlatMap<>(4);
         } else {
             this.properties.clear();
         }
@@ -1059,6 +1058,6 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
         if (properties == null) {
             return null;
         }
-        return new ConcurrentHashMap<>(properties);
+        return new FlatMap<>(properties);
     }
 }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
index 551559912734..9397fe32455f 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
@@ -16,8 +16,6 @@
  */
 package org.apache.camel.support;
 
-import java.util.concurrent.ConcurrentHashMap;
-
 import org.apache.camel.CamelContext;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
@@ -40,14 +38,12 @@ public final class DefaultPooledExchange extends 
AbstractExchange implements Poo
     public DefaultPooledExchange(CamelContext context) {
         super(context);
         this.originalPattern = getPattern();
-        this.properties = new ConcurrentHashMap<>(8);
         this.clock = new ResetableClock();
     }
 
     public DefaultPooledExchange(Exchange parent) {
         super(parent);
         this.originalPattern = parent.getPattern();
-        this.properties = new ConcurrentHashMap<>(8);
 
         Clock parentClock = parent.getClock();
 
@@ -61,8 +57,6 @@ public final class DefaultPooledExchange extends 
AbstractExchange implements Poo
     public DefaultPooledExchange(CamelContext context, ExchangePattern 
pattern) {
         super(context, pattern);
         this.originalPattern = getPattern();
-        this.properties = new ConcurrentHashMap<>(8);
-
         this.clock = new ResetableClock();
     }
 
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/FlatMap.java 
b/core/camel-support/src/main/java/org/apache/camel/support/FlatMap.java
new file mode 100644
index 000000000000..b2c04f52e824
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/FlatMap.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.support;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A lightweight {@link Map} backed by a flat {@code Object[]} with 
alternating key/value pairs. Optimized for very
+ * small maps (0-8 entries) where linear scan is faster than hashing due to 
cache locality and zero per-entry object
+ * overhead.
+ * <p/>
+ * Not thread-safe. Keys are compared by {@link Object#equals}.
+ */
+class FlatMap<K, V> extends AbstractMap<K, V> {
+
+    private static final int DEFAULT_CAPACITY = 4;
+
+    private Object[] data;
+    private int size;
+
+    FlatMap() {
+        this(DEFAULT_CAPACITY);
+    }
+
+    FlatMap(int initialCapacity) {
+        this.data = new Object[initialCapacity * 2];
+    }
+
+    FlatMap(Map<? extends K, ? extends V> source) {
+        this.data = new Object[Math.max(source.size(), DEFAULT_CAPACITY) * 2];
+        for (Entry<? extends K, ? extends V> e : source.entrySet()) {
+            put(e.getKey(), e.getValue());
+        }
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return indexOf(key) >= 0;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public V get(Object key) {
+        int i = indexOf(key);
+        return i >= 0 ? (V) data[i + 1] : null;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public V put(K key, V value) {
+        int i = indexOf(key);
+        if (i >= 0) {
+            V old = (V) data[i + 1];
+            data[i + 1] = value;
+            return old;
+        }
+        int pos = size * 2;
+        if (pos >= data.length) {
+            data = Arrays.copyOf(data, data.length * 2);
+        }
+        data[pos] = key;
+        data[pos + 1] = value;
+        size++;
+        return null;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public V remove(Object key) {
+        int i = indexOf(key);
+        if (i < 0) {
+            return null;
+        }
+        V old = (V) data[i + 1];
+        int last = (size - 1) * 2;
+        if (i < last) {
+            data[i] = data[last];
+            data[i + 1] = data[last + 1];
+        }
+        data[last] = null;
+        data[last + 1] = null;
+        size--;
+        return old;
+    }
+
+    @Override
+    public void putAll(Map<? extends K, ? extends V> m) {
+        int needed = (size + m.size()) * 2;
+        if (needed > data.length) {
+            data = Arrays.copyOf(data, Math.max(needed, data.length * 2));
+        }
+        for (Entry<? extends K, ? extends V> e : m.entrySet()) {
+            put(e.getKey(), e.getValue());
+        }
+    }
+
+    @Override
+    public void clear() {
+        Arrays.fill(data, 0, size * 2, null);
+        size = 0;
+    }
+
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        return new EntrySet();
+    }
+
+    private int indexOf(Object key) {
+        for (int i = 0, len = size * 2; i < len; i += 2) {
+            if (Objects.equals(key, data[i])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private class EntrySet extends AbstractSet<Entry<K, V>> {
+        @Override
+        public int size() {
+            return size;
+        }
+
+        @Override
+        public Iterator<Entry<K, V>> iterator() {
+            return new Iterator<>() {
+                private int index;
+                private int lastReturned = -1;
+
+                @Override
+                public boolean hasNext() {
+                    return index < size;
+                }
+
+                @Override
+                @SuppressWarnings("unchecked")
+                public Entry<K, V> next() {
+                    if (index >= size) {
+                        throw new NoSuchElementException();
+                    }
+                    lastReturned = index;
+                    int pos = index * 2;
+                    index++;
+                    return new FlatEntry(pos);
+                }
+
+                @Override
+                public void remove() {
+                    if (lastReturned < 0) {
+                        throw new IllegalStateException();
+                    }
+                    FlatMap.this.removeAt(lastReturned * 2);
+                    index = lastReturned;
+                    lastReturned = -1;
+                }
+            };
+        }
+
+        @Override
+        public void clear() {
+            FlatMap.this.clear();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void removeAt(int pos) {
+        int last = (size - 1) * 2;
+        if (pos < last) {
+            data[pos] = data[last];
+            data[pos + 1] = data[last + 1];
+        }
+        data[last] = null;
+        data[last + 1] = null;
+        size--;
+    }
+
+    private class FlatEntry implements Entry<K, V> {
+        private final int pos;
+
+        FlatEntry(int pos) {
+            this.pos = pos;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public K getKey() {
+            return (K) data[pos];
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public V getValue() {
+            return (V) data[pos + 1];
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public V setValue(V value) {
+            V old = (V) data[pos + 1];
+            data[pos + 1] = value;
+            return old;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof Entry<?, ?> e)) {
+                return false;
+            }
+            return Objects.equals(getKey(), e.getKey()) && 
Objects.equals(getValue(), e.getValue());
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
+        }
+    }
+}

Reply via email to