Why Volatile is behaving weirdly

偶尔善良 提交于 2019-12-05 03:32:27

The volatile keyword is not a synchronization primitive. It merely prevents caching of the value on the thread, but it does not prevent two threads from modifying the same value and writing it back concurrently.

Let's say two threads come to the point when they need to increment the counter, which is now set to 5. Both threads see 5, make 6 out of it, and write it back into the counter. If the counter were not volatile, both threads could have assumed that they know the value is 6, and skip the next read. However, it's volatile, so they both would read 6 back, and continue incrementing. Since the threads are not going in lock-step, you may see a value different from 10000 in the output, but there's virtually no chance that you would see 20000.

count = count + 1 is not atomic. It has three steps:

  1. read the current value of the variable
  2. increment the value
  3. write the new value back to the variable

These three steps are getting interwoven, resulting in different execution paths, resulting in an incorrect value. Use AtomicInteger.incrementAndGet() instead if you want to avoid the synchronized keyword.

So although the volatile keyword acts pretty much as you described it, that only applies to each seperate operation, not to all three operations collectively.

The fact that a variable is volatile does not mean every operation it's involved in is atomic. For instance, this line in SynVsVol.Go:

count = count + 1;

will first have count read, then incremented, and the result will then be written back. If some other thread will execute it at the same time, the results depend on the interleaving of the commands.

Now, when you add the syncronized, SynVsVol.Go executes atomically. Namely, the increment is done as a whole by a single thread, and the other one can't modify count until it is done.

Lastly, caching of member variables that are only modified within a syncronized block is much easier. The compiler can read their value when the monitor is acquired, cache it in a register, have all changes done on that register, and eventually flush it back to the main memory when the monitor is released. This is also the case when you call wait in a synchronized block, and when some other thread notifys you: cached member variables will be synchronized, and your program will remain coherent. That's guaranteed even if the member variable is not declared as volatile:

Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor.

Your code is broken because it treats the read-and-increment operation on a volatile as atomic, which it is not. The code doesn't contain a data race, but it does contain a race condition on the int.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!