Ayfri
11/09/2021, 10:27 PMJoffrey
11/09/2021, 10:30 PMi++
. What happens here is that you will first have to read the value of i
, then write i + 1
in this memory location. Because 2 things happen, these 2 things can be interlaced when multiple threads are doing such operations. Imagine 2 threads are doing i++
at the same time. Without atomic guarantees, you can end up in a situation where the following events occur:
1. thread 1 reads value 42
2. thread 2 reads value 42
3. thread 1 writes value 43 (42+1)
4. thread 2 writes value 43 as well
So you expected 2 increments, but you end up with 43. When you use something that performs this increment "atomically", it means the read and write cannot be interlaced with other things happening to the same memory location, so thread 2 would wait for thread 1's write before reading the value, and you wouldn't have these problems.
Atomic integers, long or booleans are sort of "synchronized values" that provide this kind of guarantees with operations like incrementAndGet
, getAndIncrement
, etc.Joffrey
11/09/2021, 10:35 PMephemient
11/10/2021, 1:11 AMlong
and double
) and all references are atomic: this means each read or write operation is completed in full, without being interleaved with any other operation. in practice, this means that those types are always consistent; you cannot read a value that was not explicitly written.
by contrast, look at long
and double
, where the specification allows implementations of read and write that perform multiple 32-bit operations non-atomically instead of a single atomic 64-bit operation: it is possible for
var value = 0L
Thread { value = -1L }.start()
println(value)
to print 0
, -1
, -4294967296
, or 4294967295
as the 64-bit read or write may be implemented as two 32-bit reads or writes which may be interleaved.ephemient
11/10/2021, 1:26 AM++
as Joffrey said), nor does it imply any memory ordering either. for example,
var value = 0
Thread { value = 1 }.start()
while (true) {
when (value) {
0 -> continue
1 -> break
else -> throw Exception()
}
}
will not throw (value
will not be read as anything other than 0
or 1
), but it may loop forever, and is it likewise permitted for
var x = 0
var y = 0
Thread {
x = 1
y = 1
}.start()
while (y == 0) {}
println(x)
to print either 0
or 1
or loop forever, because there is nothing that causes the writes to memory from one thread to be observed (at all, or in any particular order) when reading memory from another thread, unless volatile
or other things that imply ordering are involvedephemient
11/10/2021, 1:31 AMvolatile
variables form "happens-before" links between two threads, as do object monitors (and synchronized
and locks) and higher-level tools such as AtomicInteger
and kotlinx.atomicfu. in fact, the guarantee is stronger than in many other languages, with all memory operations becoming ordered, not only the specific ones involving the volatile
or atomic that is being operated onephemient
11/10/2021, 1:38 AMclass Foo(var value: Int)
var foo: Foo? = null
Thread { foo = Foo(1) }.start()
while (foo != null) {}
println(foo!!.value)
as mentioned earlier, this may not terminate, or it may print either 0
or 1
, but if we change it to
class Foo(val value: Int)
then it may still not terminate, or print 1
, but it may no longer print 0
, as initialization of final
fields are guaranteed to be published before a reference to a fully-constructed objectephemient
11/10/2021, 2:09 AMval i = AtomicInteger(0)
arrayOf(Thread { i.getAndIncrement() }, Thread { i.getAndIncrement() }).onEach { it.start() }.forEach { it.join() }
println(i.get())
prints 2
, because the read-increment-write cannot be interleaved, but can also provide similar power as volatile
when dealing with the other scenarios I brought upJoffrey
11/10/2021, 2:34 PMephemient
11/11/2021, 1:08 AMFor the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.
Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency's sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.
ephemient
11/11/2021, 1:15 AM