2019年5月

java volatile 只能保证可见行, 不能操作保证一致性

前段时间, 有个来面试的Java工程师说: 由volatile修饰的变量相当于synchronized修饰的块, 能保证一致性.
这个错误的结论其实很容易被证明是错误的, 以下代码由两个线程对同一个 volatile 变量进行操作, 一个顺序加100000, 另外一个顺序减100000, 为了防止JVM 对于for loop 进行优化, 每一次循环都打印当前的i变量, 结果有时候不是0.

package com.tianxiaohui.art;

public class Main {

public static volatile int count = 0;

public static void main(String[] args) {
    Thread t1 = new Thread(new Runnable(){

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                System.out.print(i);
                count++;
            }
        }
    });
    
    Thread t2 = new Thread(new Runnable(){

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                System.out.print(i);
                count--;
            }
        }
    });
    
    t2.start();
    t1.start();
    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    System.out.println();
    System.out.println(count);
}
}

运行结果:
01234567891001234....
1

为什么会出现这种情况?
使用 volatile 修饰的变量在进行写操作的时候, 会发生两件事情:

  1. 当前缓存行数据写回主存, 也有书上说当前线程workspace的数据写回主存;
  2. 写回主存的操作会使其它CPU里缓存该内存地址的缓存无效.
    也就是说一旦有一个CPU里的volatile的变量有写操作, 它会立即写回主内存, 并且使其它CPU的缓存里有个变量的值都无效. 效果类似于对于这个volatile 修饰的变量没有缓存, 全部直接使用内存操作.

上面的代码里面对于 volatile 变量的操作 ++ 其实不是一个原子操作, 尽管它能保证每个CPU 拿到count值时候都是最新的值, 但是对它的加1, 再写回内存并不是原子操作. 举例来说, 某时间点CPU-0 和 CPU-1要对它操作之前同时拿到count的值都是5, 然后CPU-0对5加1, 得到6, CPU-1对5减1, 得到4, 那么赋值回去的时候, CPU-0先写回, 主内存值变成6, 同时导致CPU-1里面的值失效, 不过这个时候CPU-1已经在做加操作,之后CPU-1把加的结果4赋值给count, 写回主内存, 变成4. 更细一点讲是一个+操作包含好几条内存指令, 从内存load 它的值只是最早一步.

volatile变量能保证每个线程读到的数据都是主内存最新的值. 一旦有个写操作, 就会导致其它CPU里面的缓存的该值无效, 对它的read都会重新从主内存读取. 但是对它的操作不能保证一致性. 操作一致性要通过同步块或者CAS来保证.

volatile最适合的使用场景是作为一个开关, 一旦有一个线程对这个开关做了操作, 其它线程立马就感知到.

读懂 thread heap

  1. 第一行是线程基本信息, 分别是 线程名字, 是否是Daemon线程(如果不是就不显示), 线程优先级, 线程id, OS native 线程id, 当前运行的状态[当前在那个对象对象].
  2. 当前线程的状态
  3. 下面就是当前线程的Stack;

    "DefaultThreadPool-89" daemon prio=10 tid=0x00007f3974036000 nid=0xb4a waiting on condition [0x00007f393e7e4000]
    java.lang.Thread.State: WAITING (parking)

    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000007ac99baf8> (a com.ebay.raptor.orchestration.impl.FutureCallableTask)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
    at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:425)
    at java.util.concurrent.FutureTask.get(FutureTask.java:187)
    
  4. Java 线程有6种状态: New, Runnable, Blocked, Waiting, Timed Waiting, Terminated. 对应到 thread dump 里面: 到现在做的 thread dump 里面没有看到状态是 New的. 也没有看到 Terminated 的. 其它都看到过:

java.lang.Thread.State: RUNNABLE

java.lang.Thread.State: BLOCKED (on object monitor)

java.lang.Thread.State: TIMED_WAITING (sleeping)
java.lang.Thread.State: TIMED_WAITING (parking)
java.lang.Thread.State: TIMED_WAITING (on object monitor)

java.lang.Thread.State: WAITING (parking)
java.lang.Thread.State: WAITING (on object monitor)

  1. JVM 6种线程定义:

    A thread state. A thread can be in one of the following states:
    NEW
    A thread that has not yet started is in this state.
    RUNNABLE
    A thread executing in the Java virtual machine is in this state.
    BLOCKED
    A thread that is blocked waiting for a monitor lock is in this state.
    WAITING
    A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
    TIMED_WAITING
    A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
    TERMINATED
    A thread that has exited is in this state.
    A thread can be in only one state at a given point in time. These states are virtual machine states which do not reflect any operating system thread states.

  2. 什么情况下会进入 blocked 状态?
    根据 Thread.State 类的描述: 1. 当一个线程在准备进入Synchronized的块/方法的时候, 2. 或者该线程之前已经进入synchronized 块/方法, 之后又call 了 Object.wait, 这个时候, 该线程进入 Waiting状态 或者 timed_waiting 状态, 之后又被 notify 或notifyAll 唤醒, 等待重新进入Synchronized同步块/方法, 这时候又进入blocked 状态.
  3. 什么情况会进入 waiting 状态?
    等待某种事件发生.
    Object.wait with no timeout -> 等待notify 或 notifyAll
    Thread.join with no timeout -> 等待特定线程终止
    LockSupport.park -> 等待 unpark
  4. 什么情况会进入 timed_waiting 状态?
    虽然等待某种特殊事件发生, 不过最多只等待特定时间
    Thread.sleep
    Object.wait with timeout
    Thread.join with timeout
    LockSupport.parkNanos
    LockSupport.parkUntil