volatile 关键字

因为CPU缓存的关系,以及指令的重排等等原因。有时候线程操作的变量往往是它从内存中复制过来到CPU缓存内的,所以修改完了如果没有flush到内存,那么可能会出现不一致。道理大概是这么个道理,在Java中,每个线程有自己的缓存大小,对于数据操作的时候,先操作线程内部的数据。所以其他线程对于该数据的变化看不到。所以引入了volatile 关键词来保证,对于数据的读取和写入都是直接从内存来的,而不是仅限于缓存。

下面以一段代码来解释,这一段代码是知乎上转的:

public class Test {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true){
            if(td.isFlag()){
                System.out.println("主线程获取到flag为true");
                break;
            }
        }
    }
}
class ThreadDemo implements Runnable{

    private boolean  flag = false;

    @Override
    public void run() {
        /*睡眠2秒*/
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag="+ isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

思路很简单的,子线程休眠两秒后来将flag设置为true。然后我们希望主线程发现这一点,结束自己的死循环。然而,实际运行这段代码,是不行的。将flag以volatile修饰,问题就解决了,因为它对于flag的修改会直接影响到其他线程的缓存中flag的值。在java中,synchronized也是支持这一点的。被synchronized包裹起来的对象也可以保证内存的可见性。就是说,synchronized包起来的对象都是从内存中读取的,代码如下:

/*
这里flag是boolean类型的,synchronized不能用于primitive type,或者将flag改为Boolean
然后synchronized(td.isFlag())也行
*/
while (true){
   // <----------这里还未持有锁
    synchronized(td) { 
        if(td.isFlag()){
            System.out.println("主线程获取到flag为true");
            break;
        }
    }
}

一个小问题:

我最开始以为synchronized(td)会使得线程一直无法运行,因为它被主线程先持有锁了。不过想想其实有点问题,确实td是被锁持有了。到下次循环开始时,语句执行到我画了箭头的地方的时候,这是是可以让线程执行的,因为锁已经被释放了。

汇编的角度来看这个问题

下面是一段C语言程序。

int a = 0;
void bar() {
    a++;
}

查看它的汇编代码,结果如下:

movl    a(%rip), %eax //将a从内存中读取到eax寄存器
addl    $1, %eax      //a++
movl    %eax, a(%rip) //写回到内存

如果在addl指令发生中断,那么就会出现计算结果的不正确。volatile关键字只能保证对于数据的读写是直接面向内存的,保证了不同线程之间对于共享变量的可见性,但是并不保证写入的原子性。面对这种读取-计算-写入的形式,volatile于事无补。只有在修改的值不依赖于变量之前的值的时候才可以使用volatile

再来看另外一段C语言代码。

int a = 0;
void bar(int c) {
    a = c;
}

查看它的汇编代码如下:

movl    %edi, -4(%rbp)  //从寄存器中取出 int c放到栈当中
movl    -4(%rbp), %eax  //再从栈当中移出来
movl    %eax, a(%rip)    //赋值给int a

对于这样的形式,无论中断发生在哪一条语句,都不会出现错误的行为。在这情况下,使用多线程编程的时候volatile是合适的。

原子性操作

对于依赖于之前的值的操作,要保证它的原子性,只能使用锁来实现或者使用synchronized关键字。如下:

public synchronized void bar() {
    i++;
}

这样就保证了i++的原子性。synchronized的排他性使得最多只有一个线程可以执行i++语句,实现了同步。但是synchronized会使得其他线程直接阻塞。可能会引起线程的切换,浪费系统资源。

Happens Before

JMM(java memory model)定义了一种偏序关系(partial ordering)叫作happen-before。在JMM中,将读取volatile变量,获得锁,释放锁,创建和join线程都视为是actions。那么什么叫作happens-before呢?如果说按照中文意思,这似乎再说,某件事发生在另外一件事之前,但是happens-before在JMM中并不是指代在时间顺序上的关系。根据《Java concurrency in practice》中的说法,happens-before要保证action B在执行的时候可以看见action A的结果,如果两个action之间没有happens-before的联系,那么JVM可以自由的reorder这些指令。原文提到:

To guarantee that the thread executing action B can see the results of action A (whether or not A and B occur in different threads), there must be a happens-before relationship between A and B. In the absence of a happens-before ordering between two operations, the JVM is free to reorder them as it pleases

那么为什么需要这总不知所云的关系定义?

Reorder

因为CPU为了获得更好的性能,可能会对指令重排,对变量缓存,以至于在多线程程序当中。当前线程对于变量的改变对于其他CPU可能不可见。所以,在Java中引入了volatile关键字,来使得对于变量的读取都是从内存中,而不是在当前的缓存中。在《Java concurrency in practice》提到:

When a field is declared volatile, the compiler and runtime are put on notice that this variable is shared and that operations on it should not be reordered with other memory operations. Volatile variables are not cached in registers or in caches where they are hidden from other processors, so a read of a volatile variable always returns the most recent write by any thread

这段话比较重要,翻译一下:

当一个变量被声明为volatile的时候,编译器和runtime需要注意到这个变量是和其他操作共享的,所以在内存中不能将它与其他操作重排。volatiel变量也不能缓存在寄存器以及缓存中,这会使得它对其CPU不可见,所以对于一个volatile变量的读取的时候总是会返回它最近(被其他线程)写入的值。

主要有两点,对于volatile修改的时候,会使得它不会被重排。Java中重排不好找,这里提供了一个在C语言中语句被重排的例子。

C语言程序:

int A = 0;
int B = 0;

void foo()
{
    A = B + 1;              // (1)
    B = 1;                  // (2)
}

对应的汇编语句:

movl    $1, B(%rip) #B = 1
addl    $1, %eax    #eax = B+1
movl    %eax, A(%rip) # A = B+1

可以看到确实发生了重排。不过在int B中加上volatile似乎没有改变这点,不管怎么说,主要目的还是阐述确实会发生指令的重排

volatile的可见性

如果说底层来讲,volatile使用了内存屏障来实现对于重排的禁止。再来回顾下,在JMM中happens-before的意思,在当前action完成之前看到的东西对于下一个action来说也是可见的

具体一点,在java规范中提到,对一个volatile变量的写happens-before对于一个volatile变量的读

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

引入一个例子,这篇博客是JSR 133的作者讲述关于volatile的内容。下图中ready是volatile修饰的,answer是普通变量。

他提到:

all of the memory contents seen by Thread 1, before it wrote to ready, must be visible to Thread 2, after it reads the value true for ready

结合上图来说,volatile禁止了指令的重排,所以在thread 1中它是可以看到answer已经是42了,此时内存中的内容,对于Thread 2在读取ready之前,都是可见的。说白了,就是thread 2也可以知道answer是42。即使他不是以volatile修饰,但是仍然没有被缓存

这里也提到了一样的内容,注意它说的Thread A可以见到anything对于Thread B都是可见的:

In effect, because the new memory model places stricter constraints on reordering of volatile field accesses with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.

那么我们将volatile的意思在进一步解释下,倘若一个线程对volatile变量进行修改,此时它看到的内存的东西,在接下来对volatile变量读取的线程都是可见的

接下来,在上一个代码:

public class Test2 {
    static int x = 0;
    static boolean ready = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
            while(!ready) {}
            System.out.println(x);
        });
        x = 42;
        t.start();
        Thread.sleep(10);
        ready = true;
    }
}

如果ready不用volatile修饰,那么程序就无法进行,没有用volatile修饰,那么就没有happen-before的关系,所以在主线程中的操作不会影响到子线程。将ready修改为volatile,带入到上述的语义中,主线程在修改ready的时候,此时所有的变量的值(就是x=42),对于在Thread t中的while(!ready)的时候,都是可见的,而对于ready它本身,前面说过它会是最近被其他线程写入的值

用happens-before的套路来说,对于x=42 happens-beforewhile(!ready)

不一定使用volatile

不一定使用volatile,在while循环修改一下,该如下的代码,也能够实现一样的效果。

public class Test2 {
    static int x = 0;
    static boolean ready = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
            while(!ready) {
                System.out.println(x);
            }
            System.out.println(x);
        });
        x = 42;
        t.start();
        Thread.sleep(10);
        ready = true;
    }
}

这是因为,在System.out.println也用happens-before关系。文档-17.4.提到,System.out.println确实使用了监视器锁(monitor),所谓监视器锁就是在代码中使用synchronized关键字。

An unlock on a monitor happens-before every subsequent lock on that monitor.

这里提到对于监视器锁的使用都会使得缓存的刷新。

After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory.

与之类似,线程的Interrupt()方法也有这样的关系。引用《Java Concurrency in Practice》:

Interruption rule. A thread calling interrupt on another thread happensͲbefore the interrupted thread detects the interrupt (either by having InterruptedException thrown, or invoking isInterrupted or interrupted).

再将上面的代码稍微修改下:

public class Test2 {
    static int x = 0;
    static boolean ready = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()-> {
            while(!Thread.currentThread().isInteruppted()){}
            System.out.println(x);
        });
        x = 42;
        t.start();
        Thread.sleep(10);
        ready = true;
        t.interrupted();
    }
}

也可以实现一样的效果。

没有volatile的影响

如下代码:

class VolatileExample {
  int x = 0;
  boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

假设线程A操作writer()函数,线程B操作reader(),如果v不是volatile,那么,编译器的重排可能会导致,v = true,x = 42,两条指令交换。如果此时reader()开始执行(但是x = 42还没执行),那么v = true,但是此时可能会得到x为0。

Happens-Before的所有规则

《Java concurrency in practice》概括了happens-before的规则,注意这里的锁除了synchronized,其他的锁也是一样的。

  • Program order rule. Each action in a thread happens-before every action in that thread that comes later in the program order
  • Monitor lock rule. An unlock on a monitor lock happens-before every subsequent lock on that same monitor lock
  • Volatile variable rule. A write to a volatile field happens-before every subsequent read of that same field
  • Volatile variable rule. A write to a volatile field happens-before every subsequent read of that same field
  • Thread termination rule. Any action in a thread happens-before any other thread detects that thread has
    terminated, either by successfully return from Thread.join or by Thread.isAlive returning false.
  • Interruption rule. A thread calling interrupt on another thread happens-before the interrupted thread detects the
    interrupt (either by having InterruptedException thrown, or invoking isInterrupted or interrupted)
  • Finalizer rule. The end of a constructor for an object happens-before the start of the finalizer for that object
  • Thread start rule. A call to Thread.start on a thread happens-before every action in the started thread.

最后在对一个锁的使用举个例子:

public class Test2 {
    static int x = 0;
    static boolean ready = false;
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock(false);
        Thread t = new Thread(()-> {
            while(true) {
                lock.lock();
                if (ready) break;
                lock.unlock();
            }
            System.out.println(x);
        });
        x = 42;
        t.start();
        Thread.sleep(10);
        lock.lock();
        ready = true;
        lcok.unlock()    
    }
}

也有一样的效果。

一个小问题

public class Test2 {
    static int x = 0;
    static boolean ready = false;
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
           while(!ready) {} 
           System.out.println(x);
        });
        t.start();
        ready = true;
        x = 42;
    }
}
//output:
42

在我的实验中,它确实输出了42,也就是说明ready的修改确实影响到了线程的行为。根据前面的happens-before规则

Thread.start on a thread happens-before every action in the started thread.

照理来说,线程内部应该看不到外部线程对于ready和x的修改。这确实是的,happens-before只能保证在start()之前内存中的内容对子线程可见。然而ready = true; x = 42;所需要执行的时间太短了,当线程开始真的执行,它需要这些变量的时候,从内存读取过来的数据已经是新的了。为了验证这一点,稍微修改一下代码。

public class Test2 {
    static int x = 0;
    static boolean ready = false;
    public static void main(String[] args) {
        Thread t = new Thread(()-> {
           while(!ready) {} 
           System.out.println(x);
        });
        t.start();
        Thread.sleep(1);
        ready = true;
        Thread.sleep(1);
        x = 42;
    }
}
//output:
0

线程只休眠了1毫秒,结果就不一样了。在现实开发中,可能是某些函数的调用的远远会超过Thread.sleep(1)然而这就会导致程序出现不希望出现的结果,所以要十分小心。

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇