This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push: new 9731d6b84f Add a new keyed reentrant read/write lock 9731d6b84f is described below commit 9731d6b84ffcd27013a3412c61caf405f8a91d27 Author: Mark Thomas <ma...@apache.org> AuthorDate: Wed Aug 27 10:33:04 2025 +0100 Add a new keyed reentrant read/write lock This class will be used when Tomcat needs to manage concurrent access to components identified by a key (e..g sessions). --- build.xml | 3 + .../concurrent/KeyedReentrantReadWriteLock.java | 179 +++++++++++++++++++++ .../tomcat/util/concurrent/LocalStrings.properties | 16 ++ .../TestKeyedReentrantReadWriteLock.java | 31 ++++ 4 files changed, 229 insertions(+) diff --git a/build.xml b/build.xml index d51e933e3d..bf6e35c7f8 100644 --- a/build.xml +++ b/build.xml @@ -453,6 +453,7 @@ <include name="org/apache/tomcat/util/codec/**" /> <include name="org/apache/tomcat/util/collections/**" /> <include name="org/apache/tomcat/util/compat/**" /> + <include name="org/apache/tomcat/util/concurrent/**" /> <include name="org/apache/tomcat/util/file/**" /> <include name="org/apache/tomcat/util/json/**" /> <include name="org/apache/tomcat/util/res/**" /> @@ -549,6 +550,7 @@ <include name="org/apache/tomcat/util/codec/**" /> <include name="org/apache/tomcat/util/collections/**" /> <include name="org/apache/tomcat/util/compat/**" /> + <include name="org/apache/tomcat/util/concurrent/**" /> <include name="org/apache/tomcat/util/descriptor/**" /> <include name="org/apache/tomcat/util/file/**" /> <include name="org/apache/tomcat/util/http/**" /> @@ -2507,6 +2509,7 @@ Apache Tomcat ${version} native binaries for Win64 AMD64/EMT64 platform. <exclude name="org/apache/tomcat/util/codec" /> <exclude name="org/apache/tomcat/util/collections" /> <exclude name="org/apache/tomcat/util/compat" /> + <exclude name="org/apache/tomcat/util/concurrent" /> <exclude name="org/apache/tomcat/util/descriptor" /> <exclude name="org/apache/tomcat/util/file" /> <exclude name="org/apache/tomcat/util/http" /> diff --git a/java/org/apache/tomcat/util/concurrent/KeyedReentrantReadWriteLock.java b/java/org/apache/tomcat/util/concurrent/KeyedReentrantReadWriteLock.java new file mode 100644 index 0000000000..d1c0541d62 --- /dev/null +++ b/java/org/apache/tomcat/util/concurrent/KeyedReentrantReadWriteLock.java @@ -0,0 +1,179 @@ +/* + * 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.tomcat.util.concurrent; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; + +import org.apache.tomcat.util.res.StringManager; + +/** + * Provides a reentrant read/write lock for a given key. Any locks obtained from an instance of this class using the + * same key will use the same underlying reentrant read/write lock as long as at least one lock for that key remains in + * use. Once no locks are in use for the given key, the lock is eligible for GC and the next lock obtained using that key + * will use a new underlying reentrant read/write lock. + * <p> + * The class is used when Tomcat needs to manage concurrent access to components identified by a key (e.g. sessions). + * <p> + * The map of keys to locks is maintained so that locks are created as required and removed when no longer used. + * <p> + * The locks provided by this class only implement {@code Lock#lock()} and {@code Lock#unlock()}. All other methods will + * throw {@code UnsupportedOperationException}. + */ +public class KeyedReentrantReadWriteLock { + + private final Map<String,CountedLock> locksMap = new HashMap<>(); + + + /** + * Obtain the reentrant read/write lock for the given key. + * + * @param key The key for which the lock should be obtained + * + * @return A reentrant read/write lock for the given key + */ + public ReadWriteLock getLock(String key) { + return new ReadWriteLockImpl(locksMap, key); + } + + + /* + * Reentrant read/write lock implementation that is passed back to the caller. It provides the lock wrappers that + * track usage. + */ + private static class ReadWriteLockImpl implements ReadWriteLock { + + private final Map<String,CountedLock> locksMap; + private final String key; + private volatile Lock readLock; + private volatile Lock writeLock; + + ReadWriteLockImpl(Map<String,CountedLock> locksMap, String key) { + this.locksMap = locksMap; + this.key = key; + } + + @Override + public Lock readLock() { + if (readLock == null) { + readLock = new LockImpl(locksMap, key, ReentrantReadWriteLock::readLock); + } + return readLock; + } + + @Override + public Lock writeLock() { + if (writeLock == null) { + writeLock = new LockImpl(locksMap, key, ReentrantReadWriteLock::writeLock); + } + return writeLock; + } + } + + + /* + * Lock wrapper implementation that provides both read locks and write locks from the underlying lock and tracks + * their usage. Most of the methods throw UnsupportedOperationException as Tomcat does not (currently) require + * implementations of those methods. + */ + private static class LockImpl implements Lock { + + private static final StringManager sm = StringManager.getManager(LockImpl.class); + + private final Map<String,CountedLock> locksMap; + private final String key; + private final Function<ReentrantReadWriteLock,Lock> function; + + LockImpl(Map<String,CountedLock> locksMap, String key, Function<ReentrantReadWriteLock,Lock> function) { + this.locksMap = locksMap; + this.key = key; + this.function = function; + } + + @Override + public void lock() { + CountedLock countedLock = null; + synchronized (locksMap) { + // Lookup / create the counted lock for the given key + countedLock = locksMap.compute(key, (k, v) -> v == null ? new CountedLock() : v); + // Increment usage count inside the sync block to ensure other threads are aware key is in use. + countedLock.count.incrementAndGet(); + } + // Lock outside of the sync block in case the call to lock() blocks. + function.apply(countedLock.reentrantLock).lock(); + } + + @Override + public void unlock() { + CountedLock countedLock = null; + // Unlocking so a lock should exist in the map for the given key. + synchronized (locksMap) { + countedLock = locksMap.get(key); + } + if (countedLock == null) { + throw new IllegalStateException(sm.getString("lockImpl.unlockWithoutLock")); + } + // No need to unlock inside sync block, so don't. + function.apply(countedLock.reentrantLock).unlock(); + synchronized (locksMap) { + /* + * Decrement usage count and check for zero inside the sync block to ensure usage tracking is consistent + * across multiple threads. + */ + if (countedLock.count.decrementAndGet() == 0) { + locksMap.remove(key); + } + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } + } + + + /* + * Holds the underlying reentrant read/write lock and the counter that tracks usage. + */ + private static class CountedLock { + AtomicInteger count = new AtomicInteger(); + ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock(); + } +} diff --git a/java/org/apache/tomcat/util/concurrent/LocalStrings.properties b/java/org/apache/tomcat/util/concurrent/LocalStrings.properties new file mode 100644 index 0000000000..c938a3b529 --- /dev/null +++ b/java/org/apache/tomcat/util/concurrent/LocalStrings.properties @@ -0,0 +1,16 @@ +# 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. + +lockImpl.unlockWithoutLock=An attempt was made to release a lock without first obtaining the lock \ No newline at end of file diff --git a/test/org/apache/tomcat/util/concurrent/TestKeyedReentrantReadWriteLock.java b/test/org/apache/tomcat/util/concurrent/TestKeyedReentrantReadWriteLock.java new file mode 100644 index 0000000000..f2a1d5f77b --- /dev/null +++ b/test/org/apache/tomcat/util/concurrent/TestKeyedReentrantReadWriteLock.java @@ -0,0 +1,31 @@ +/* + * 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.tomcat.util.concurrent; + +import java.util.concurrent.locks.ReadWriteLock; + +import org.junit.Test; + +public class TestKeyedReentrantReadWriteLock { + + @Test(expected = IllegalStateException.class) + public void testUnlockWithoutLock() { + KeyedReentrantReadWriteLock locks = new KeyedReentrantReadWriteLock(); + ReadWriteLock lock = locks.getLock("any"); + lock.readLock().unlock(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org