You really do use volatile

volatile concept

Or, volatile solve the problem?

My own conclusion: volatile memory visibility problem solving variables under multiple threads access for inter-thread communication.

How can it be understood that communication, writing a volatile variable thread A, then thread B reads the volatile variables, the process is essentially by A thread
sends a message to the thread B main memory.

java language description of the standards are such volatile:

The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).

The above excerpt this link, are interested can point to open their own look.

https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.1.4

Probably means, java language allows multiple threads to access shared variables. In order to ensure consistency of shared variables can accurately update the thread through the lock mechanism to ensure that individual to receive this variable. java provides a mechanism that allows you to define variables as volatile, and in some cases more convenient than the direct use of the lock.

If a variable is defined as volatile, java memory model ensures that all threads see the shared variable is consistent.

This is consistent how to understand it? Continue to look down.

Detailed volatile

First look at a picture,

Here Insert Picture Description

This is a memory architecture diagram calculation.

Most are now CPU multicore, inside the computer, read and write the process variables is such that:

  • When the processor needs to read a variable, the variable will first read the cache from main memory, there may be a register, and then make various calculations.
  • The results calculated by the register to refresh the cache, then the cache to the main memory refreshed.
  • A processor's cache memory is written back to causes other processor's cache is invalid, so that other processors

A key point here is that, when refreshed? The answer is not known. We can not assume anything when the CPU is refreshed. This will bring some problems, such as a thread finished writing a shared variable, not yet flushed to main memory. Then another thread reads the variable, or the old value, in many scenarios, and the results expected by the programmer is not consistent.

Fortunately, although we do not know when to refresh CPU, but we can force the CPU to perform a refresh.

Let's look at a map, it is JAVA memory model diagram.

Here Insert Picture Description

A local memory in the JVM is an abstract concept, it can cover a register, cache and the like.

We put together two figures correspond, it can be explained.

在JAVA中,当一个线程写变量时,会先把这个变量从主内存拷贝一份线程的本地内存,然后在本地内存操作。操作完成之后,再刷新到主内存。只有刷新后,另一个线程才能读取新的值。

来看个例子:

public class VolatileTest implements Runnable {
    private boolean running = true;

    @Override
    public void run() {
        if (running) {
            System.out.println("I am running");
        }
    }

    public void stop() {
        running = false;
    }
}

这段代码在多线程环境下执行的时候,假设A线程正在执行run方法,B线程执行了stop方法,我们的程序没法保证A线程什么时候会马上停止。因为这取决于CPU什么时候进行刷新,把最新变量的值同步到主内存。

解决方法是,把running这个共享变量用volatile修饰即可,这样可以保证B线程的修改会立刻刷新到主内存,对其它线程可见。

public class VolatileTest implements Runnable {
    private volatile boolean running = true;

再来看个稍微复杂一点的例子。

public class VolatileTest {
    public volatile int a = 0;
    volatile boolean flag = false;

    public void write() {
        a = 1; // 位置1
        flag = true; //// 位置2
    }
    public void read() {
        if (flag) { // 位置3
            int i = a; // 位置4
        }
    }
}

Java规范对于volatile变量规则是:对一个volatile域的写,happens-before于任意后续对这个volatile域的
读。

假设线程A执行writer()方法之后,线程B执行reader()方法。根据volatile变量的happens-before规则,位置2必然先于位置3执行。同时我们知道在同一个线程中,所有操作必须按照程序的顺序来执行,所以位置1肯定早于位置2,位置3早于位置4。然后我们能推出位置1早于位置4。

这样的顺序是符合我们预期的。

这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之
前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

什么时候需要使用volatile

通过上面的例子,我们可以总结下volatile的使用场景。

通常是,存在一个或者多个共享变量,会有线程对他们写操作,也会有其它线程对他们读操作。这样的变量都应该使用volatile修饰。

volatile在标准库里的应用

ConcurrentHashMap里用到了一些volatile的操作,比如:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
        ...

可以看到,用于存储值的value变量就是volatile类型,这样可以保证在多线程读取的时候,不会读到过期的值。之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

volatile会降低程序执行的效率

不要过度使用volatile,不必要的场景没有必要用volatile修饰变量,尽管这样做程序也不会出什么错。

根据前面的描述,volatile相当于给变量的操作加了“锁”,每次操作都有加锁和释放锁的动作,效率自然会受影响。

volatile不是万能的

对volatile经常有一中误解就是,它可以保证原子操作。

通过上面的例子,我们知道,volatile关键字可以保证内存可见性,指令执行的有序性。但是请一定记住,它没法保证原子性。举个例子你可能比较容易明白。

public class VolatileTest {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final VolatileTest test = new VolatileTest();

        for(int i=0;i<10;i++){
            new Thread(() -> {
                for(int j=0;j<1000;j++)
                    test.increase();
            }).start();
        }
        while(Thread.activeCount()>2)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

执行这段代码,会发现结果每次一般都不同,但是肯定都小于10*1000。这就是volatile不保证原子性的最好证据。那么深层次的原因是什么呢?

事实上,自增操作包括三个步骤:

  1. 读取变量的原始值
  2. 进行加1操作
  3. 写入线程工作内存

既然分了三个步骤,就有可能出现下面这种情况:

假如某个时刻变量inc的值为10。

第一步,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;

第二步, 然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2会直接去主存读取inc的值,此时inc的值时10;

第三步, 线程2进行加1操作,并把11写入工作内存,最后写入主存。

第四步,线程1接着进行加1操作,由于已经读取了inc的值,此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

Finally, two threads were carried out after the first increase since the operation, but an increase of only 1 inc.

After a lot of people will be in the third step and fourth step where there is doubt, thread 2 inc updated values, not the value of the thread 1 will lead to failure of the working memory do?

The answer is no, because in one operation, the value will only read once. This is a core atom and visibility distinction.

The solution is to increase the use of the method using a synchronizedsynchronization lock modification. Specific not started.


reference:

  • "Java concurrent programming of art"
  • https://www.cnblogs.com/dolphin0520/p/3920373.html
Published 158 original articles · won praise 307 · Views 2.12 million +

Guess you like

Origin blog.csdn.net/pony_maggie/article/details/104240562