23

This is about volatile piggyback. Purpose: I want to reach a lightweight vars visibilty. Consistency of a_b_c is not important. I have a bunch of vars and I don't want to make them all volatile.

Is this code threadsafe?

class A {
    public int a, b, c;
    volatile int sync;

    public void setup() {
        a = 2;
        b = 3;
        c = 4;
    }

    public void sync() {
        sync++;
    }
}

final static A aaa = new A();

Thread0:
aaa.setup();
end

Thread1:
for(;;) {aaa.sync(); logic with aaa.a, aaa.b, aaa.c}

Thread2:
for(;;) {aaa.sync(); logic with aaa.a, aaa.b, aaa.c}
Adam Zalcman
  • 26,643
  • 4
  • 71
  • 92
temper
  • 395
  • 2
  • 9

5 Answers5

42

Java Memory Model defines the happens-before relationship which has the following properties (amongst others):

  • "Each action in a thread happens-before every action in that thread that comes later in the program order" (program order rule)
  • "A write to a volatile field happens-before every subsequent read of that same volatile" (volatile variable rule)

These two properties together with transitivity of the happens-before relationship imply the visibility guarantees that OP seeks in the following manner:

  1. A write to a in thread 1 happens-before a write to sync in a call to sync() in thread 1 (program order rule).
  2. The write to sync in the call to sync() in thread 1 happens-before a read to sync in a call to sync in thread 2 (volatile variable rule).
  3. The read from sync in the call to sync() in thread 2 happens-before a read from a in thread 2 (program order rule).

This implies that the answer to the question is yes, i.e. the call to sync() in each iteration in threads 1 and 2 ensures visibility of changes to a, b and c to the other thread(s). Note that this ensures visibility only. No mutual exclusion guarantees exist and hence all invariants binding a, b and c may be violated.

See also Java theory and practice: Fixing the Java Memory Model, Part 2. In particular the section "New guarantees for volatile" which says

Under the new memory model, when thread A writes to a volatile variable V, and thread B reads from V, any variable values that were visible to A at the time that V was written are guaranteed now to be visible to B.

mkobit
  • 43,979
  • 12
  • 156
  • 150
Adam Zalcman
  • 26,643
  • 4
  • 71
  • 92
  • 2
    @PhilippWendler The two rules imply visibility of changes to `a`, `b` and `c` made in thread 1 to thread 2 in the following manner: change to `a` in thread 1 _happens-before_ a write to `sync` in a subsequent call to `sync()` in thread 1 (rule 1) which _happens-before_ a read from `sync` in a call to `sync` in thread 2 (rule 2) which _happens-before_ a read from `a` in thread 2 (again, rule 1). – Adam Zalcman Jan 07 '12 at 13:05
  • 2
    @PhilippWendler See also the article I linked to, in particular the section "New guarantees for volatile". One more relevant quote: "Under the new memory model, when thread A writes to a volatile variable V, and thread B reads from V, any variable values that were visible to A at the time that V was written are guaranteed now to be visible to B. " – Adam Zalcman Jan 07 '12 at 13:27
  • 1
    Could you explain `change to a in thread 1 happens-before a write to sync in a subsequent call to sync() in thread 1 (rule 1)`. What I see is `{aaa.sync(); logic with aaa.a, aaa.b, aaa.c}` so `sync()` is called before access to `a`. Thanks – michael nesterenko May 28 '13 at 08:59
  • The "subsequent call to `sync()`" takes place next time around the infinite loop wrapping the rest of the logic. – Adam Zalcman May 28 '13 at 09:34
  • 1
    @Adam Zalcman **Note that this ensures visibility only. No mutual exclusion guarantees exist and hence all invariants binding a, b and c may be violated.** Can you clarify this quote, please. – gstackoverflow Apr 29 '14 at 06:51
  • 1
    @gstackoverflow What this means is that the part of the iteration hidden under the placeholder "logic with aaa.a, aaa.b, aaa.c" may be executed concurrently by more than one thread. Thus any invariants that involve a, b or c and which would hold true in single-thread execution model may be violated under concurrent execution. – Adam Zalcman Apr 29 '14 at 07:40
  • 1
    @gstackoverflow Suppose for example that the logic uses `aaa.a` to count iterations by `aaa.a++`. This will fail if thread 1 reads `aaa.a`, then thread 2 reads `aaa.b`, thread 1 increments the value, thread 2 increments the value, thread 1 writes out the new value to `aaa.a`, thread 2 writes out the new value to `aaa.a`. The sequence incremented `aaa.a` by one instead of two. This demonstrates why exclusion is necessary if you require `aaa.a` to count the iterations. – Adam Zalcman Apr 29 '14 at 07:41
  • @Adam Zalcman At this case you can use Atomics. I don't understand(problem with English) meaning of phrase **This demonstrates why exclusion is necessary if you require aaa.a to count the iterations** I have understood all words but I don't understand sense. – gstackoverflow Apr 29 '14 at 08:47
  • @AdamZalcman what would happen if 2 threads try to write to a volatile variable at the same time? –  Jun 14 '17 at 18:50
  • How can you guarantee that thread 1 will be executed before thread 2 and so that the write to sync in the call to sync() in thread 1 happens-before a read to sync in a call to sync in thread 2? – Luke Jul 05 '18 at 08:33
  • @michaelnesterenko, did you understand the explanation? If so, could you reformulate? – Ekaterina Oct 03 '21 at 15:13
  • @user6091735 They both will write & could lead to race condition. volatile is not meant to be used to guard against race conditions introduced by multiple threads writing to the same variable. What volatile does is provide a way to ensure that any _**writes**_ to the volatile variable are immediately visible to all _**reads**_ even in other threads once the write completes (more formally the memory model's _**happens-before**_ relationship is guaranteed). – lmk Jan 12 '23 at 00:23
6

Incrementing a value between threads is never thread-safe with just volatile. This only ensures that each thread gets an up to date value, not that the increment is atomic, because at the assembler level your ++ is actually several instructions that can be interleaved.

You should use AtomicInteger for a fast atomic increment.

Edit: Reading again what you need is actually a memory fence. Java has no memory fence instruction, but you can use a lock for the memory fence "side-effect". In that case declare the sync method synchronized to introduce an implicit fence:

void synchronized sync() {
    sync++;
}
Tudor
  • 61,523
  • 12
  • 102
  • 142
3

The pattern is usually like this.

public void setup() {
    a = 2;
    b = 3;
    c = 4;
    sync();
}

However, while this guarantees the other threads will see this change, the other threads can see an incomplete change. e.g. the Thread2 might see a = 2, b = 3, c = 0. or even possibly a = 2, b = 0, c = 4;

Using the sync() on the reader doesn't help much.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • I have misunderstanding. in thread1 I invoke setup method. after it in thread2 can I see b=0 ? – gstackoverflow Apr 29 '14 at 06:42
  • @gstackoverflow You need two parts. A write barrier at the end on the writing side and a read barrier at the start on the reading side. This ensures you read what was written in another thread. Writing to a volatile involves a write barrier, reading from a volatile involves a read barrier. – Peter Lawrey Apr 29 '14 at 07:14
  • 2
    @PeterLawrey, But in the question he *did* have a read barrier right? Is the `aaa.sync()` in Thread 1 and Thread 2 considered sufficient? – Pacerier Jun 17 '14 at 15:19
  • sync() is indeed both read and write barrier (because it reads, increments and then writes), so calling `sync` before reader should help. Yes, sync's increment won't be atomic, but this variable's value doesn't hold any logic except for piggybacking – Semyon Danilov Dec 19 '21 at 11:44
0

From javadoc:

An unlock (synchronized block or method exit) of a monitor happens-before every subsequent lock (synchronized block or method entry) of that same monitor. And because the happens-before relation is transitive, all actions of a thread prior to unlocking happen-before all actions subsequent to any thread locking that monitor.

A write to a volatile field happens-before every subsequent read of that same field. Writes and reads of volatile fields have similar memory consistency effects as entering and exiting monitors, but do not entail mutual exclusion locking.

So I think that writing to volatile var is not an equivalent to syncronization in this case and it doesn't guarantee happens-before order and visibility of changes in Thread1 to Thread2

dbf
  • 6,399
  • 2
  • 38
  • 65
-1

You don't really have to manually synchronize at all, just use an automatically synchronized data structure, like java.util.concurrent.atomic.AtomicInteger.

You could alternatively make the sync() method synchronized.

P Varga
  • 19,174
  • 12
  • 70
  • 108