We have spent last couple of months stabilizing the lock detection functionality in Plumbr. During this we have stumbled into many tricky concurrency issues. Many of the issues are unique, but one particular type of issues keeps repeatedly appearing.

You might have guessed it – misuse of thevolatile keyword. We have detected and solved bunch of issues where the extensive usage of volatile made arbitrary parts of the application slower, extended locks holding time and eventually bringing the JVM to its knees. Or vice versa – granting too liberal access policy has triggered some nasty concurrency issues.

I guess every Java developer recalls the first steps in the language. Days and days spent with manuals and tutorials. Those tutorials all had the list of keywords, among which the volatilewas one of the scariest. As days passed and more and more code was written without the need for this keyword, many of us forgot the existence of volatile. Until the production systems started either corrupting data or dying in unpredictable manner. Debugging such cases forced some of us to actually understand the concept. But I bet it was not a pleasant lesson to have, so maybe I can save some of you some time by shedding light upon the concept via a simple example.

Example of volatile in action

The example is simulating a bank office. The type of bank office where you pick a queue number from a ticketing machine and then wait for the invite when the queue in front of you has been processed. To simulate such office, we have created the following example, consisting of two threads.

First of the two threads is implemented as CustomerInLine. This is a thread doing nothing but waiting until the value in NEXT_IN_LINE matches customer’s ticket. Ticket number is hardcoded to be #4. When the time arrives (NEXT_IN_LINE>=4), the thread announces that the waiting is over and finishes. This simulates a customer arriving to the office with some customers already in queue.

The queuing implementation is in Queue class which runs a loop calling for the next customer and then simulating work with the customer by sleeping 200ms for each customer. After calling the next customer, the value stored in class variable NEXT_IN_LINE is increased by one.

public class Volatility {

    static int NEXT_IN_LINE = 0;

    public static void main(String[] args) throws Exception {
new CustomerInLine().start();
new Queue().start();
} static class CustomerInLine extends Thread {
@Override
public void run() {
while (true) {
if (NEXT_IN_LINE >= 4) {
break;
}
}
System.out.format("Great, finally #%d was called, now it is my turn\n",NEXT_IN_LINE);
}
} static class Queue extends Thread {
@Override
public void run() {
while (NEXT_IN_LINE < 11) {
System.out.format("Calling for the customer #%d\n", NEXT_IN_LINE++);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

So, when running this simple program you might expect the output of the program being similar to the following:

Calling for the customer #1
Calling for the customer #2
Calling for the customer #3
Calling for the customer #4
Great, finally #4 was called, now it is my turn
Calling for the customer #5
Calling for the customer #6
Calling for the customer #7
Calling for the customer #8
Calling for the customer #9
Calling for the customer #10

As it appears, the assumption is wrong. Instead, you will see the Queue processing through the list of 10 customers and the hapless thread simulating customer #4 never alerts it has seen the invite. What happened and why is the customer still sitting there waiting endlessly?

Analyzing the outcome

What you are facing here is a JIT optimization applied to the code caching the access to theNEXT_IN_LINE variable. Both threads get their own local copy and the CustomerInLine thread never sees the Queue actually increasing the value of the thread. If you now think this is some kind of horrible bug in the JVM then you are not fully correct - compilers are allowed to do this to avoid rereading the value each time. So you gain a performance boost, but at a cost - if other threads change the state, the thread caching the copy does not know it and operates using the outdated value.

This is precisely the case for volatile. With this keyword in place, the compiler is warned that a particular state is volatile and the code is forced to reread the value each time when the loop is executed. Equipped with this knowledge, we have a simple fix in place - just change the declaration of the NEXT_IN_LINE to the following and your customers will not be left sitting in queue forever:

static volatile int NEXT_IN_LINE = 0;

For those, who are happy with just understanding the use case for volatile, you are good to go. Just be aware of the extra cost attached - when you start declaring everything to be volatile you are forcing the CPU to forget about local caches and to go straight into main memory, slowing down your code and clogging the memory bus.

Volatile under the hood

For those who wish to understand the issue in more details, stay with me. To see what is happening underneath, lets turn on the debugging to see the assembly code generated from the bytecode by the JIT. This is achieved by specifying the following JVM options:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

Running the program with those options turned on both with volatile turned on and off, gives us the following important insight:

Running the code without the volatile keyword, shows us that on instruction 0x00000001085c1c5a we have comparison between two values. When comparison fails we continue through 0x00000001085c1c60 to 0x00000001085c1c66 which jumps back to 0x00000001085c1c60 and an infinite loop is born.

  0x00000001085c1c56: mov    0x70(%r10),%r11d
0x00000001085c1c5a: cmp $0x4,%r11d
0x00000001085c1c5e: jge 0x00000001085c1c68 ; OopMap{off=64}
;*if_icmplt
; - Volatility$CustomerInLine::run@4 (line 14)
0x00000001085c1c60: test %eax,-0x1c6ac66(%rip) # 0x0000000106957000
;*if_icmplt
; - Volatility$CustomerInLine::run@4 (line 14)
; {poll}
0x00000001085c1c66: jmp 0x00000001085c1c60 ;*getstatic NEXT_IN_LINE
; - Volatility$CustomerInLine::run@0 (line 14)
0x00000001085c1c68: mov $0xffffff86,%esi

With the volatile keyword in place, we can see that on instruction 0x000000010a5c1c40 we load value to a register, on 0x000000010a5c1c4a compare it to our guard value of 4. If comparison fails, we jump back from 0x000000010a5c1c4e to 0x000000010a5c1c40, loading value again for the new check. This ensures that we will see changed value of NEXT_IN_LINE variable.

  0x000000010a5c1c36: data32 nopw 0x0(%rax,%rax,1)
0x000000010a5c1c40: mov 0x70(%r10),%r8d ; OopMap{r10=Oop off=68}
;*if_icmplt
; - Volatility$CustomerInLine::run@4 (line 14)
0x000000010a5c1c44: test %eax,-0x1c1cc4a(%rip) # 0x00000001089a5000
; {poll}
0x000000010a5c1c4a: cmp $0x4,%r8d
0x000000010a5c1c4e: jl 0x000000010a5c1c40 ;*if_icmplt
; - Volatility$CustomerInLine::run@4 (line 14)
0x000000010a5c1c50: mov $0x15,%esi

Now, hopefully the explanation will save you from couple of nasty bugs.

原文:https://plumbr.eu/blog/understanding-volatile-via-example

UNDERSTANDING VOLATILE VIA EXAMPLE--reference的更多相关文章

  1. C关键字volatile总结

    做嵌入式C开发的相信都使用过一个关键字volatile,特别是做底层开发的.假设一个GPIO的数据寄存器地址是0x50000004,我们一般会定义一个这样的宏: #define GDATA *((vo ...

  2. [转]Keyword Reference (F#)

    Visual F# Development Portal http://msdn.microsoft.com/en-us/library/vstudio/ff730280.aspx 本文转自:http ...

  3. report for PA2

    目录 说明 Report for PA 2(writed with vim) Part i - pa2.1 Steps: instr(seperately) Part ii - 2.2 Part ii ...

  4. 【JUC】JDK1.8源码分析之SynchronousQueue(九)

    一.前言 本篇是在分析Executors源码时,发现JUC集合框架中的一个重要类没有分析,SynchronousQueue,该类在线程池中的作用是非常明显的,所以很有必要单独拿出来分析一番,这对于之后 ...

  5. linux内核设计模式

    原文来自:http://lwn.net/Articles/336224/ 选择感兴趣内容简单翻译了下: 在内核社区一直以来的兴趣是保证质量.我们需要保证和改善质量是显而易见的.但是如何做到却不是那么简 ...

  6. Rpc框架dubbo-client(v2.6.3) 源码阅读(二)

    接上一篇 dubbo-server 之后,再来看一下 dubbo-client 是如何工作的. dubbo提供者服务示例, 其结构是这样的!dubbo://192.168.11.6:20880/com ...

  7. 【Java并发编程】17、SynchronousQueue源码分析

    SynchronousQueue是一种特殊的阻塞队列,不同于LinkedBlockingQueue.ArrayBlockingQueue和PriorityBlockingQueue,其内部没有任何容量 ...

  8. 第十一章 dubbo通信框架-netty4

    netty4是2.5.6引入的,2.5.6之前的netty用的是netty3.在dubbo源码中相较于netty3,添加netty4主要仅仅改了两个类:NettyServer,NettyClient. ...

  9. .NET:CLR via C# Primitive Thread Synchronization Constructs

    User-Mode Constructs The CLR guarantees that reads and writes to variables of the following data typ ...

随机推荐

  1. http拦截器interceptors

    在服务里配置$httpProvider.interceptors的相关参数 包含 request请求拦截 response响应拦截 requestError请求错误抛出 responseError响应 ...

  2. 判断JS类型

    (function (root, factory) { if (typeof define === 'function' && define.amd) { define([], fac ...

  3. C#【数据库】 Excel打开到DataGridView

    if (openFileDialog1.ShowDialog() == DialogResult.OK) { Filename = openFileDialog1.FileName; string s ...

  4. 练习2 D 题- 第几天?

    Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u   Description 给定一个日 ...

  5. 把centos 网卡接口eth2改成eth0

    kvm 虚拟机 复制之后 默认网卡是 eth2了 用 ifconfig -a 命令查看所有的网络设置,果然没有eth0的相关设置,多出来一个eth2.显示如下:[root@neal ~]# ifcon ...

  6. BZOJ 1487 无归岛

    Description Neverland是个神奇的地方,它由一些岛屿环形排列组成,每个岛上都生活着之中与众不同的物种.但是这些物种都有一个共同的生活习性:对于同一个岛上的任意两个生物,他们有且仅有一 ...

  7. C# IOCP服务器

    C# IOCP服务器 @by  群63438968 这是我写的unity 网络斗地主的服务器源码,本来我想只公开部份服务端代码的,但是为了回报大家的热情,以及曾经和现在的好兄弟,我计划慢慢开源! 告诉 ...

  8. int 和String之间的互转

    int -> String int i=12345;String s="";第一种方法:s=i+"";第二种方法:s=String.valueOf(i); ...

  9. 除了创建时指定窗口位置之外,还有3种移动窗口位置的办法(移动的同时往往可以改变窗口大小)(SetWindowPos最有用,它有许多标志位)

    首先,在创立窗口对象的时候,CreateWindowEx就可以指定窗口的位置.除此之外,还有三种方法可以改变窗口的位置: procedure TWinControl.CreateWindowHandl ...

  10. Delphi 和 DFM

    Delphi et les DFM Depuis la toute première version de Delphi, celui-ci intègre des fichiers à l'exte ...