diff --git a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java
index cd74949cbb53cedb8c339449c04f41de3d777772..0f336dbeac3077f9ea4477ee515775d973980ce4 100644
--- a/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/SSLContextImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2023, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -71,9 +71,6 @@ public abstract class SSLContextImpl extends SSLContextSpi {
     private volatile StatusResponseManager statusResponseManager;
 
     private final ReentrantLock contextLock = new ReentrantLock();
-    final HashMap<Integer,
-            SessionTicketExtension.StatelessKey> keyHashMap = new HashMap<>();
-
 
     SSLContextImpl() {
         ephemeralKeyManager = new EphemeralKeyManager();
diff --git a/src/java.base/share/classes/sun/security/ssl/SSLSessionContextImpl.java b/src/java.base/share/classes/sun/security/ssl/SSLSessionContextImpl.java
index 9584a35c5de1e576453e1b505e1ca9ccd641fcff..2dd629b1f1ecb52f1e414a1a1bb446e2e54f4ade 100644
--- a/src/java.base/share/classes/sun/security/ssl/SSLSessionContextImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/SSLSessionContextImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2019, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2023, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -28,7 +28,11 @@ package sun.security.ssl;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.Iterator;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
 import javax.net.ssl.SSLSession;
 import javax.net.ssl.SSLSessionContext;
 
@@ -69,6 +73,11 @@ final class SSLSessionContextImpl implements SSLSessionContext {
     private int cacheLimit;             // the max cache size
     private int timeout;                // timeout in seconds
 
+    // The current session ticket encryption key ID (only used in server context)
+    private int currentKeyID;
+    // Session ticket encryption keys and IDs map (only used in server context)
+    private final Map<Integer, SessionTicketExtension.StatelessKey> keyHashMap;
+
     // Default setting for stateless session resumption support (RFC 5077)
     private boolean statelessSession = true;
 
@@ -80,6 +89,14 @@ final class SSLSessionContextImpl implements SSLSessionContext {
         // use soft reference
         sessionCache = Cache.newSoftMemoryCache(cacheLimit, timeout);
         sessionHostPortCache = Cache.newSoftMemoryCache(cacheLimit, timeout);
+        if (server) {
+            keyHashMap = new ConcurrentHashMap<>();
+            // Should be "randomly generated" according to RFC 5077,
+            // but doesn't necessarily has to be a true random number.
+            currentKeyID = new Random(System.nanoTime()).nextInt();
+        } else {
+            keyHashMap = Map.of();
+        }
     }
 
     // Stateless sessions when available, but there is a cache
@@ -170,6 +187,51 @@ final class SSLSessionContextImpl implements SSLSessionContext {
         return cacheLimit;
     }
 
+    private void cleanupStatelessKeys() {
+        Iterator<Map.Entry<Integer, SessionTicketExtension.StatelessKey>> it =
+            keyHashMap.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<Integer, SessionTicketExtension.StatelessKey> entry = it.next();
+            SessionTicketExtension.StatelessKey k = entry.getValue();
+            if (k.isInvalid(this)) {
+                it.remove();
+                try {
+                    k.key.destroy();
+                } catch (Exception e) {
+                    // Suppress
+                }
+            }
+        }
+    }
+
+    // Package-private, used only from SessionTicketExtension.KeyState::getCurrentKey.
+    SessionTicketExtension.StatelessKey getKey(HandshakeContext hc) {
+        SessionTicketExtension.StatelessKey ssk = keyHashMap.get(currentKeyID);
+        if (ssk != null && !ssk.isExpired()) {
+            return ssk;
+        }
+        synchronized (this) {
+            // If the current key is no longer expired, it was already
+            // updated by a concurrent request, and we can return.
+            ssk = keyHashMap.get(currentKeyID);
+            if (ssk != null && !ssk.isExpired()) {
+                return ssk;
+            }
+            int newID = currentKeyID + 1;
+            ssk = new SessionTicketExtension.StatelessKey(hc, newID);
+            keyHashMap.put(Integer.valueOf(newID), ssk);
+            currentKeyID = newID;
+        }
+        // Check for and delete invalid keys every time we create a new stateless key.
+        cleanupStatelessKeys();
+        return ssk;
+    }
+
+    // Package-private, used only from SessionTicketExtension.KeyState::getKey.
+    SessionTicketExtension.StatelessKey getKey(int id) {
+        return keyHashMap.get(id);
+    }
+
     // package-private method, used ONLY by ServerHandshaker
     SSLSessionImpl get(byte[] id) {
         return (SSLSessionImpl)getSession(id);
diff --git a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java
index af9cd69575f3f059c41b8da571b71b7f1815ec95..1132ec120da505c3b997ebf4b1a5e65950ae79ab 100644
--- a/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java
+++ b/src/java.base/share/classes/sun/security/ssl/SessionTicketExtension.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -37,6 +37,7 @@ import javax.crypto.KeyGenerator;
 import javax.crypto.SecretKey;
 import javax.crypto.spec.GCMParameterSpec;
 import javax.net.ssl.SSLProtocolException;
+import javax.net.ssl.SSLSessionContext;
 
 import static sun.security.ssl.SSLExtension.CH_SESSION_TICKET;
 import static sun.security.ssl.SSLExtension.SH_SESSION_TICKET;
@@ -76,7 +77,6 @@ final class SessionTicketExtension {
     // Time in milliseconds until key is changed for encrypting session state
     private static final int TIMEOUT_DEFAULT = 3600 * 1000;
     private static final int keyTimeout;
-    private static int currentKeyID = new SecureRandom().nextInt();
     private static final int KEYLEN = 256;
 
     static {
@@ -117,7 +117,8 @@ final class SessionTicketExtension {
         final SecretKey key;
         final int num;
 
-        StatelessKey(HandshakeContext hc, int newNum) {
+        // package-private, used only by SSLContextImpl
+        StatelessKey(HandshakeContext hc, int num) {
             SecretKey k = null;
             try {
                 KeyGenerator kg = KeyGenerator.getInstance("AES");
@@ -128,8 +129,7 @@ final class SessionTicketExtension {
             }
             key = k;
             timeout = System.currentTimeMillis() + keyTimeout;
-            num = newNum;
-            hc.sslContext.keyHashMap.put(Integer.valueOf(num), this);
+            this.num = num;
         }
 
         // Check if key needs to be changed
@@ -138,7 +138,8 @@ final class SessionTicketExtension {
         }
 
         // Check if this key is ready for deletion.
-        boolean isInvalid(long sessionTimeout) {
+        boolean isInvalid(SSLSessionContext sslSessionContext) {
+            int sessionTimeout = sslSessionContext.getSessionTimeout() * 1000;
             return ((System.currentTimeMillis()) > (timeout + sessionTimeout));
         }
     }
@@ -147,9 +148,11 @@ final class SessionTicketExtension {
 
         // Get a key with a specific key number
         static StatelessKey getKey(HandshakeContext hc, int num)  {
-            StatelessKey ssk = hc.sslContext.keyHashMap.get(num);
+            SSLSessionContextImpl serverCache =
+                (SSLSessionContextImpl)hc.sslContext.engineGetServerSessionContext();
+            StatelessKey ssk = serverCache.getKey(num);
 
-            if (ssk == null || ssk.isInvalid(getSessionTimeout(hc))) {
+            if (ssk == null || ssk.isInvalid(serverCache)) {
                 return null;
             }
             return ssk;
@@ -157,69 +160,9 @@ final class SessionTicketExtension {
 
         // Get the current valid key, this will generate a new key if needed
         static StatelessKey getCurrentKey(HandshakeContext hc) {
-            StatelessKey ssk = hc.sslContext.keyHashMap.get(currentKeyID);
-
-            if (ssk != null && !ssk.isExpired()) {
-                return ssk;
-            }
-            return nextKey(hc);
-        }
-
-        // This method locks when the first getCurrentKey() finds it to be too
-        // old and create a new key to replace the current key.  After the new
-        // key established, the lock can be released so following
-        // operations will start using the new key.
-        // The first operation will take a longer code path by generating the
-        // next key and cleaning up old keys.
-        private static StatelessKey nextKey(HandshakeContext hc) {
-            StatelessKey ssk;
-
-            synchronized (hc.sslContext.keyHashMap) {
-                // If the current key is no longer expired, it was already
-                // updated by a previous operation and we can return.
-                ssk = hc.sslContext.keyHashMap.get(currentKeyID);
-                if (ssk != null && !ssk.isExpired()) {
-                    return ssk;
-                }
-                int newNum;
-                if (currentKeyID == Integer.MAX_VALUE) {
-                    newNum = 0;
-                } else {
-                    newNum = currentKeyID + 1;
-                }
-                // Get new key
-                ssk = new StatelessKey(hc, newNum);
-                currentKeyID = newNum;
-                // Release lock since the new key is ready to be used.
-            }
-
-            // Clean up any old keys, then return the current key
-            cleanup(hc);
-            return ssk;
-        }
-
-        // Deletes any invalid SessionStateKeys.
-        static void cleanup(HandshakeContext hc) {
-            int sessionTimeout = getSessionTimeout(hc);
-
-            StatelessKey ks;
-            for (Object o : hc.sslContext.keyHashMap.keySet().toArray()) {
-                Integer i = (Integer)o;
-                ks = hc.sslContext.keyHashMap.get(i);
-                if (ks.isInvalid(sessionTimeout)) {
-                    try {
-                        ks.key.destroy();
-                    } catch (Exception e) {
-                        // Suppress
-                    }
-                    hc.sslContext.keyHashMap.remove(i);
-                }
-            }
-        }
-
-        static int getSessionTimeout(HandshakeContext hc) {
-            return hc.sslContext.engineGetServerSessionContext().
-                    getSessionTimeout() * 1000;
+            SSLSessionContextImpl serverCache =
+                (SSLSessionContextImpl)hc.sslContext.engineGetServerSessionContext();
+            return serverCache.getKey(hc);
         }
     }