diff --git a/library/std/src/sys_common/thread_local_key.rs b/library/std/src/sys_common/thread_local_key.rs
index 70beebe86d20b..032bf604d7388 100644
--- a/library/std/src/sys_common/thread_local_key.rs
+++ b/library/std/src/sys_common/thread_local_key.rs
@@ -69,8 +69,10 @@ use crate::sys_common::mutex::StaticMutex;
 /// ```ignore (cannot-doctest-private-modules)
 /// use tls::os::{StaticKey, INIT};
 ///
+/// // Use a regular global static to store the key.
 /// static KEY: StaticKey = INIT;
 ///
+/// // The state provided via `get` and `set` is thread-local.
 /// unsafe {
 ///     assert!(KEY.get().is_null());
 ///     KEY.set(1 as *mut u8);
diff --git a/library/std/src/thread/local.rs b/library/std/src/thread/local.rs
index f4750cdf764dc..8aedfc4a6b819 100644
--- a/library/std/src/thread/local.rs
+++ b/library/std/src/thread/local.rs
@@ -1036,6 +1036,7 @@ pub mod fast {
 }
 
 #[doc(hidden)]
+#[cfg(not(target_thread_local))]
 pub mod os {
     use super::lazy::LazyKeyInner;
     use crate::cell::Cell;
@@ -1044,6 +1045,8 @@ pub mod os {
     use crate::ptr;
     use crate::sys_common::thread_local_key::StaticKey as OsStaticKey;
 
+    /// Use a regular global static to store this key; the state provided will then be
+    /// thread-local.
     pub struct Key<T> {
         // OS-TLS key that we'll use to key off.
         os: OsStaticKey,
diff --git a/library/std/src/thread/mod.rs b/library/std/src/thread/mod.rs
index 479669647c128..a17185b6f7075 100644
--- a/library/std/src/thread/mod.rs
+++ b/library/std/src/thread/mod.rs
@@ -192,21 +192,31 @@ pub use scoped::{scope, Scope, ScopedJoinHandle};
 #[stable(feature = "rust1", since = "1.0.0")]
 pub use self::local::{AccessError, LocalKey};
 
-// The types used by the thread_local! macro to access TLS keys. Note that there
-// are two types, the "OS" type and the "fast" type. The OS thread local key
-// type is accessed via platform-specific API calls and is slow, while the fast
+// Select the type used by the thread_local! macro to access TLS keys. There
+// are three types: "static", "fast", "OS". The "OS" thread local key
+// type is accessed via platform-specific API calls and is slow, while the "fast"
 // key type is accessed via code generated via LLVM, where TLS keys are set up
-// by the elf linker. Note that the OS TLS type is always available: on macOS
-// the standard library is compiled with support for older platform versions
-// where fast TLS was not available; end-user code is compiled with fast TLS
-// where available, but both are needed.
+// by the elf linker. "static" is for single-threaded platforms where a global
+// static is sufficient.
 
 #[unstable(feature = "libstd_thread_internals", issue = "none")]
 #[cfg(target_thread_local)]
+#[cfg(not(test))]
 #[doc(hidden)]
 pub use self::local::fast::Key as __FastLocalKeyInner;
+#[unstable(feature = "libstd_thread_internals", issue = "none")]
+#[cfg(target_thread_local)]
+#[cfg(test)] // when building for tests, use real std's key
+pub use realstd::thread::__FastLocalKeyInner;
+
+#[unstable(feature = "libstd_thread_internals", issue = "none")]
+#[cfg(target_thread_local)]
+#[cfg(test)]
+pub use self::local::fast::Key as __FastLocalKeyInnerUnused; // we import this anyway to silence 'unused' warnings
+
 #[unstable(feature = "libstd_thread_internals", issue = "none")]
 #[doc(hidden)]
+#[cfg(not(target_thread_local))]
 pub use self::local::os::Key as __OsLocalKeyInner;
 #[unstable(feature = "libstd_thread_internals", issue = "none")]
 #[cfg(all(target_family = "wasm", not(target_feature = "atomics")))]