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());
+ }
+ }
+}