Synchronization
When data is shared between threads, one needs to synchronize read-write
access to the data in order to avoid corruption. Java offers the synchronized
keyword as a synchronization primitive. When we use a synchronized block, Java internally uses a monitor, also known as a monitor lock to provide synchronization:
public class ExampleClass {
static int data = 0;
public static void main(String[] args) throws InterruptedException {
var dataLock = new Object(); // declare a monitor lock object
var threads = new ArrayList<Thread>();
for (var i = 0; i < 10; i++) {
var thread = new Thread(() -> {
for (var j = 0; j < 1000; j++) {
synchronized (dataLock) {
data++;
}
}
});
threads.add(thread);
thread.start();
}
for (var thread : threads) {
thread.join();
}
System.out.println(data); // prints: 10000
}
}
In Rust, one must make explicit use of concurrency structures like Mutex
:
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let data = Arc::new(Mutex::new(0)); // (1)
let mut threads = vec![];
for _ in 0..10 {
let data = Arc::clone(&data); // (2)
let thread = thread::spawn(move || { // (3)
for _ in 0..1000 {
let mut data = data.lock().unwrap();
*data += 1; // (4)
}
});
threads.push(thread);
}
for thread in threads {
thread.join().unwrap();
}
println!("{}", data.lock().unwrap()); // prints: 10000
}
A few things to note:
-
Since the ownership of the
Mutex
instance and in turn the data it guards will be shared by multiple threads, it is wrapped in anArc
(1).Arc
provides atomic reference counting, which increments each time it is cloned (2) and decrements each time it is dropped. When the count reaches zero, the mutex and in turn the data it guards are dropped. -
The closure instance for each thread receives ownership (3) of the cloned reference (2).
-
The
move
keyword (3) is required to move or pass the ownership ofdata
(2) to the closure for the thread. -
The pointer-like code that is
*data += 1
(4), is not some unsafe pointer access even if it looks like it. It's updating the data wrapped in the mutex guard.
Unlike the Java version, where one can render it thread-unsafe by commenting out
the synchronized
statement, the Rust version will refuse to compile if it's changed
in any way (e.g. by commenting out parts that renders it thread-unsafe). This
demonstrates that writing thread-safe code is the developer's responsibility
in Java, by careful use of synchronized structures, whereas in Rust, one
can rely on the compiler to enforce thread-safety.
The compiler is able to help because data structures in Rust are marked by
special traits: Sync
and Send
. Sync
indicates that references to a type's instances are safe to share between threads. Send
indicates that it's safe to send instances of a type across thread boundaries. For more information, see the “Fearless Concurrency” chapter of the Rust book.