Creating two mutable references to the same data simultaneously. You are responsible for maintaining The aliasing invariant. Violating this is undefined behavior.
The compiler assumes that &T never allows mutation. UnsafeCell is the escape hatch that tells The compiler “I will manage the aliasing rules myself.” Without UnsafeCellIt would be impossible To implement Cell``RefCell``MutexOr any other interior mutability type.
Types containing UnsafeCell are not Sync by default. If you want to make a type containing UnsafeCell thread-safe, you must implement Sync manually with unsafe impl Sync:
use std :: cell :: UnsafeCell ;
use std :: sync :: atomic :: { AtomicIsize , Ordering };
unsafe impl Sync for AtomicCounter {}
fn new (value : i64 ) -> Self {
value : UnsafeCell :: new (value),
This is only sound if you can prove that all accesses to the interior are properly synchronized (e.g., via atomics, locks, or platform-specific memory barriers).
Cell<T> provides interior mutability for Copy types. The value is stored inline (no heap Allocation), and you can only access it by copying:
let counter = Cell :: new ( 0 );
assert_eq! (counter . get (), 42 );
counter . set (counter . get () + 1 );
assert_eq! (counter . get (), 43 );
Method Description new(value)Creates a new Cell containing value get()Returns a copy of the value (requires T: Copy) set(value)Replaces the interior value replace(value)Replaces and returns the old value (any T) take()Replaces with Default::default() and returns old into_inner()Consumes the Cell and returns the inner value
Cell works with non-Copy types for set``replaceAnd takeBut not get:
let cell = Cell :: new ( String :: from ( "hello" ));
// cell.get(); // ERROR: String does not implement Copy
let old = cell . replace ( String :: from ( "world" ));
assert_eq! (old, "hello" );
assert_eq! (cell . take (), "world" );
assert_eq! (cell . take (), "" ); // String::default()
1. Reference Counting:
Rc uses Cell internally for the reference count:
let a = Rc :: new ( Cell :: new ( 42 ));
assert_eq! ( Rc :: strong_count ( & a), 3 );
assert_eq! (b . get (), 100 );
2. Mutable Flags in Immutable Contexts:
fn new (level : log :: Level ) -> Self {
Logger { level : Cell :: new (level) }
fn set_level ( & self , level : log :: Level ) {
fn get_level ( & self ) -> log :: Level {
3. Interior Mutation in Closures:
fn count_calls () -> impl Fn () -> usize {
let counter = Cell :: new ( 0 );
let n = counter . get () + 1 ;
Cell has zero overhead beyond the inline storage. There is no reference counting, no runtime Borrow checking, and no heap allocation. The compiler inlines all Cell operations.
RefCell<T> provides interior mutability for any type T. It tracks borrows at runtime using a Reference count and panics if the borrowing rules are violated:
let data = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let borrow1 = data . borrow ();
let borrow2 = data . borrow ();
assert_eq! ( * borrow1, vec! [ 1 , 2 , 3 ]);
assert_eq! ( * borrow2, vec! [ 1 , 2 , 3 ]);
// data.borrow_mut(); // PANIC: already borrowed immutably
let mut borrow3 = data . borrow_mut ();
assert_eq! ( * borrow3, vec! [ 1 , 2 , 3 , 4 ]);
Method Returns Description new(value)RefCell<T>Creates a new RefCell borrow()Ref<T>Immutable borrow, panics if mutably borrowed borrow_mut()RefMut<T>Mutable borrow, panics if any borrow exists try_borrow()Result<Ref>Non-panicking immutable borrow try_borrow_mut()Result<RefMut>Non-panicking mutable borrow into_inner()TConsumes the RefCell and returns inner value
Ref<T> and RefMut<T> are RAII guards that track the borrow. When the guard is dropped, the Borrow count is decremented:
let data = RefCell :: new ( String :: from ( "hello" ));
let mut guard = data . borrow_mut ();
guard . push_str ( ", world" );
} // guard dropped here, borrow_mut count decremented
let guard = data . borrow ();
assert_eq! ( * guard, "hello, world" );
RefCell maintains two counters:
┌──────────────────────────────┐
│ ┌────────────┬────────────┐ │
│ │ borrow_count │ borrow_mut │ │
│ └────────────┴────────────┘ │
│ borrow_count > 0: can borrow immutably │
│ borrow_count == 0 && !borrow_mut: can borrow mutably │
│ borrow_mut: cannot borrow at all │
└──────────────────────────────────────────┘
neighbors : RefCell < Vec < & ' a Node <' a >>>,
let a = Node { value : 1 , neighbors : RefCell :: new ( vec! []) };
let b = Node { value : 2 , neighbors : RefCell :: new ( vec! []) };
let c = Node { value : 3 , neighbors : RefCell :: new ( vec! []) };
a . neighbors . borrow_mut () . push ( & b);
a . neighbors . borrow_mut () . push ( & c);
b . neighbors . borrow_mut () . push ( & a);
c . neighbors . borrow_mut () . push ( & a);
assert_eq! (a . neighbors . borrow () . len (), 2 );
assert_eq! (b . neighbors . borrow () . len (), 1 );
Use try_borrow and try_borrow_mut to handle borrow conflicts gracefully:
use std :: cell :: { RefCell , BorrowMutError };
let data = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let _guard = data . borrow ();
match data . try_borrow_mut () {
Err ( BorrowMutError { .. }) => {
eprintln! ( "cannot borrow mutably — already borrowed immutably" );
Mutex<T> provides mutual exclusion for interior mutability across threads. Only one thread can Access the data at a time:
use std :: sync :: { Arc , Mutex };
let counter = Arc :: new ( Mutex :: new ( 0 ));
let mut handles = vec! [];
let counter = Arc :: clone ( & counter);
handles . push ( thread :: spawn ( move || {
let mut num = counter . lock () . unwrap ();
assert_eq! ( * counter . lock () . unwrap (), 10 );
Property RefCell<T>Mutex<T>Thread safety Single-threaded only Multi-threaded Borrow check Runtime (panic) Runtime (blocking) Overhead Minimal (counters) System call on contention Poisoning No Yes (panic while locked) Send + SyncNeither Both (when T: Send)
RwLock<T> allows multiple concurrent readers or a single exclusive writer:
let lock = RwLock :: new ( 5 );
let r1 = lock . read () . unwrap ();
let r2 = lock . read () . unwrap ();
let mut w = lock . write () . unwrap ();
Need interior mutability?
│ ├── Copy types only? → Cell<T>
│ └── Any type? → RefCell<T>
├── Mostly writes? → Mutex<T>
├── Mostly reads? → RwLock<T>
└── One-time init? → OnceLock<T> / LazyLock<T>
OnceCell stores a value that is initialized at most once. It is useful for lazy initialization and For storing values that are set during construction:
database_url : OnceCell < String >,
database_url : OnceCell :: new (),
fn set_database_url ( & self , url : String ) -> Result <(), String > {
self . database_url . set (url) . map_err ( | _ | "already set" . to_string ())
fn get_database_url ( & self ) -> Option < & String > {
LazyLock initializes the value on first access using a closure:
use std :: collections :: HashMap ;
static GLOBAL_CONFIG : LazyLock < HashMap < String , String >> = LazyLock :: new ( || {
let mut m = HashMap :: new ();
m . insert ( "port" . to_string (), "8080" . to_string ());
m . insert ( "host" . to_string (), "localhost" . to_string ());
let port = GLOBAL_CONFIG . get ( "port" ) . unwrap ();
assert_eq! (port, "8080" );
LazyLock is thread-safe — the initialization closure runs exactly once, even if multiple threads Access the value concurrently.
Type Thread-safe Lazy init Set once OnceCell<T>No No Yes OnceLock<T>Yes No Yes LazyLock<T>Yes Yes Yes
Use OnceCell in single-threaded contexts, OnceLock for thread-safe one-time initialization with Manual set, and LazyLock for thread-safe lazy initialization with a closure.
listeners : RefCell < Vec < Weak < dyn Fn ( i32 )>>>,
listeners : RefCell :: new ( vec! []),
fn subscribe ( & self , listener : Weak < dyn Fn ( i32 )>) {
self . listeners . borrow_mut () . push (listener);
fn emit ( & self , value : i32 ) {
let mut listeners = self . listeners . borrow_mut ();
listeners . retain ( | weak | {
if let Some (listener) = weak . upgrade () {
Graphs with back-references require interior mutability because nodes reference each other in Cycles:
children : RefCell < Vec < Rc < Node >>>,
parent : RefCell < Weak < Node >>,
fn new (value : i32 ) -> Rc < Self > {
children : RefCell :: new ( vec! []),
parent : RefCell :: new ( Weak :: new ()),
fn add_child (parent : & Rc < Node >, child : Rc < Node >) {
child . parent . borrow_mut () . set ( Rc :: downgrade (parent));
parent . children . borrow_mut () . push (child);
use std :: collections :: HashMap ;
cache : RefCell < HashMap < u64 , u64 >>,
cache : RefCell :: new ( HashMap :: new ()),
fn call ( & self , arg : u64 ) -> u64 {
if let Some ( & result) = self . cache . borrow () . get ( & arg) {
let result = ( self . f)(arg);
self . cache . borrow_mut () . insert (arg, result);
Cell has zero runtime overhead. All operations compile to direct memory access. The value is Stored inline within the CellWhich itself has the same size as T.
assert_eq! ( std :: mem :: size_of :: < Cell < u64 >>(), 8 );
assert_eq! ( std :: mem :: size_of :: < Cell <[ u8 ; 1024]>>(), 1024 );
RefCell stores the value inline plus a borrow counter ( 2 bytes on 64-bit). Each borrow() and borrow_mut() increments or decrements the counter. try_borrow variants have the Same cost but return Result instead of panicking.
assert_eq! ( std :: mem :: size_of :: < RefCell < u64 >>(), 16 ); // 8 bytes value + overhead
Mutex has a system-level overhead: on Linux, it uses pthread_mutex_t (40 bytes). Locking is a System call on contention and a single atomic operation when uncontended. Mutex always allocates The value on the heap (it uses alloc::sys::Exclusive::new internally ).
assert_eq! ( std :: mem :: size_of :: < Mutex < u64 >>(), 40 ); // platform-dependent
RwLock is larger than Mutex (48 bytes on Linux) because it must track multiple readers. Read Locks are cheaper than write locks but still involve atomic operations. Write locks are comparable To Mutex locks.
Use Cell<T> when:
T is Copy and you only need simple get/set semanticsYou do not need to hold a reference to the interior value Performance is critical and you want zero overhead Use RefCell<T> when:
T is not Copy (e.g., String``VecCustom structs)You need to borrow the interior value (read or write) through a guard You need dynamic borrow checking with error handling Use Mutex<T> when:
Multiple threads need access to the data The critical section may be held across .await points (use tokio::sync::Mutex) You need poisoning semantics (detecting panics in critical sections) RefCell panics in production. borrow_mut() panics if there is an outstanding immutable borrow. In a long-running service, this crashes the process. Use try_borrow_mut() and handle the error, or restructure your code to avoid overlapping borrows.
Using RefCell across threads. RefCell is not Send or Sync. The compiler prevents cross-thread use, but if you bypass this with unsafeYou will have data races. Use Mutex or RwLock for multi-threaded interior mutability.
Holding Ref guards too long. A Ref or RefMut guard keeps the borrow active until it is dropped. If you store the guard in a struct or return it from a function, the borrow persists, potentially causing later borrow_mut() calls to panic. Drop guards as soon as possible.
Mutex poisoning causing cascading failures. If one thread panics while holding a Mutex the mutex becomes poisoned. Subsequent lock() calls return Err. Use lock().unwrap_or_else(|e| e.into_inner()) if you want to recover from poisoning, but be aware that the data may be in an inconsistent state.
Using std::sync::Mutex in async code. A std::sync::Mutex blocks the OS thread while held. If held across an .await point, it blocks all other async tasks on that thread. Use tokio::sync::Mutex for async contexts, or restructure to drop the lock before awaiting.
Cell with non-Copy types and get(). Cell::get() requires T: Copy. For non-Copy types, use borrow() on a RefCell or replace()/take() on a Cell.
Forgetting that UnsafeCell requires unsafe. Direct access to UnsafeCell::get() returns a raw pointer. Dereferencing it requires unsafe and you must maintain the aliasing invariant manually. Prefer Cell or RefCell unless you are building a custom synchronization primitive.
Overusing interior mutability. Interior mutability should be a deliberate design choice, not a default. If you find yourself wrapping everything in RefCellConsider restructuring your ownership model. Interior mutability hides mutation from the type system, making code harder to reason about.
LazyLock initialization panics. If the initialization closure panics, the LazyLock enters a poisoned state and all subsequent accesses panic. Guard against initialization failures if the closure can fail.
Deadlocks with Mutex and RwLock. Acquiring locks in inconsistent order across threads causes deadlocks. Always define and follow a lock ordering protocol. Use try_lock() with backoff for lock acquisition that can fail gracefully.
When a RefCell is dropped while a Ref or RefMut guard exists, the guard keeps the borrow alive Until it is dropped. This means the RefCell’s destructor runs after the guard is dropped:
let cell = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let guard = cell . borrow ();
// guard is still active — cell's data is borrowed
// When cell is dropped, the borrow is still tracked
// But since guard holds a reference to cell's data, the drop order is:
// 1. guard is dropped (borrow count decremented)
// 2. cell is dropped (data deallocated)
drop (cell); // ERROR: cannot move out of borrowed content
fn new (name : & str ) -> Self {
* self . count . borrow_mut () += 1 ;
impl Drop for SharedCounter {
// Safe to access count during drop — no other borrows can exist
// because we have &mut self
let final_count = * self . count . borrow ();
println! ( "{} was incremented {} times" , self . name, final_count);
use std :: cell :: { Cell , OnceCell , RefCell };
use std :: sync :: { LazyLock , Mutex , OnceLock , RwLock };
// Cell: Copy types, zero overhead
let cell = Cell :: new ( 42 );
cell . set (cell . get () + 1 );
// RefCell: Any type, runtime borrow checking
let refcell = RefCell :: new ( vec! [ 1 , 2 , 3 ]);
let mut guard = refcell . borrow_mut ();
// OnceCell: Single-threaded one-time init
let once = OnceCell :: new ();
once . set ( "initialized" . to_string ());
assert_eq! (once . get (), Some ( & "initialized" . to_string ()));
// OnceLock: Thread-safe one-time init
let lock = OnceLock :: new ();
assert_eq! ( * lock . get () . unwrap (), 42 );
// LazyLock: Thread-safe lazy init with closure
static CONFIG : LazyLock < String > = LazyLock :: new ( || {
std :: fs :: read_to_string ( "config.toml" ) . unwrap_or_default ()
// Mutex: Thread-safe exclusive access
let mutex = Mutex :: new ( vec! [ 1 , 2 , 3 ]);
let mut guard = mutex . lock () . unwrap ();
// RwLock: Thread-safe multiple readers or one writer
let rwlock = RwLock :: new ( vec! [ 1 , 2 , 3 ]);
let read1 = rwlock . read () . unwrap ();
let read2 = rwlock . read () . unwrap ();
let mut write = rwlock . write () . unwrap ();
Thread-safe interior mutability for Copy types:
let counter = Arc :: new ( Cell :: new ( 0 ));
let c1 = Arc :: clone ( & counter);
let c2 = Arc :: clone ( & counter);
assert_eq! (counter . get (), 2 );