JUC线程进阶篇02:volatile关键字与CAS算法

JUC线程进阶篇02:volatile关键字与CAS算法

标签: 多线程

通过《JUC线程高级篇01:Java内存模型》,知道了主存和线程各自缓存之间的关系,以及并发的三大特点。下面聊聊Volatitle变量

Volatile变量

什么是Volatile

Java语言提供了一种比锁稍弱的同步机制:volatile变量。用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作仪器重排序。volatile变量不会被缓存在计算器或其他存储器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
———— 《Java并发编程实战》

也就是说,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2. 禁止进行指令重排序。

Volatile关键字是一个轻量级的同步机制,是一个比锁要弱,如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会使线程阻塞,引起线程上下文的切换和调度,这也需要很大的开销。

Volatile保证可见性

先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    System.out.println("do something");
}

//线程2
stop = true;
System.out.println("stop = " + stop);

当执行代码的时候,我们发现,我们已经打印了stop = true,但是还是没有执行do somthing

这是因为while执行的速度很快,线程2还没将stop = true写回到内存中,就一直循环了。这就是前一篇说的不可见性。

但是用volatile修饰之后就变得不一样了:

//线程1
volatile boolean stop = false;
while(!stop){
    System.out.println("do something");
}

//线程2
stop = true;
System.out.println("stop = " + stop);

可以从表面上认为,对线程对stop的操作是在主存中进行的,对其他线程是可见的。(底层不是这样的)

Volatile保证有序性

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。这里有两层意思:

  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2. 在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

在以上代码中,由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,保证:语句1和语句2在语句3之前执行,语句4和语句5在语句3之后执行,但语句12、45之间的顺序不能保证,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

即写在volatile变量之前的语句执行完了,才执行在volatile变量,再执行后面的。

volatile不能确保原子性

public class TestAtomicDemo {
    AtomicDemo ad = new AtomicDemo();

    for (int i = 0; i < 10; i++) {
        new Thread(ad).start;
    }
}

class AtomicDemo implements Runnable {
    private volatile int serialNumber = 0;

    public void run() {
        System.out.println(getSerialNumber());
    }

    public int getSerialNumber() {
        return serialNumber++;
    }
}

输出会发现有线程安全问题。volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

i = i++中,操作实际上分三步:

int temp = i;
temp = temp + 1;
i = temp;

自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行。

解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过AtomicInteger。

在java 1.5的java.util.concurrent.atomic包下提供了一些原子变量,对原子变量进行操作都是原子性的。

这些原子变量都用volatile进行修饰,保证内存的可见性,使用CAS算法保证数据的原子性。

CAS算法

什么是CAS算法

CAS(Compare And Swap)算法是硬件对于并发操作共享数据的支持。CAS包含了三个操作数:

  1. 内存值 V
  2. 预估值 A
  3. 更新值 B

当且仅当 V==A 时,B = A,否则将不做任何操作。

  1. 即先从缓存中读取值为 V;
  2. 然后再从缓存读取值为 A ,若 V==A 则立即 B = A,若 V!=A 则什么都不做。

其中1是原子性操作,2是原子性操作。

如果线程1对变量进行了操作,在线程1没改变值之前,线程2获取该值。在线程2获改变值之前,线程1已经改变主存,则 V!=A ,线程2失败。

使用原子变量

关于java.util.concurrent.atomic包,详见:http://ifeve.com/java-atomic/

public class TestAtomicDemo {
    AtomicDemo ad = new AtomicDemo();

    for (int i = 0; i < 10; i++) {
        new Thread(ad).start;
    }
}

class AtomicDemo implements Runnable {
    private AtomicInteger int serialNumber = new AtomicInteger();

    public void run() {
        System.out.println(getSerialNumber());
    }

    public int getSerialNumber() {
        return serialNumber.getAndIncrement;
    }
}

CAS算法的效率比锁高,因为不会阻塞,不会放弃CPU,可以立即再尝试

模拟CAS算法

class CompareAndSwap {

    private int value;

    // 获取内存值
    public synchronized int get() {
        return value;
    }

    // 比较
    public synchronized int compareAndSwap(int expecteValue,int new Value) {
        int oldValue = value;

        if (oldValue == expecteValue) {
            this.value = newValue;
        }
        return oldValue;
    }

    // 设置
    publicsynchronized boolwan compareAndSet(int expecteValue,int new Value) {
        return expectedValue ==  compareAndSwap(expecteValue,new Value);
    }

}

使用模拟的CAS算法

public class TestCompareAndSwap {

    final CompareAndSwap = new CompareAndSwap();

    for (int i - 0 ; i < 10 ; i++) {
        new Thread(new Runnable() {

            public void run() {
                int expectedValue = cas.get();
                boolean b = cas.compareAndSet(expectedValue,(int)(Math.random()*101));
                System.out.println(b);
            }
        }).start();

    }
}
    原文作者:JUC
    原文地址: https://blog.csdn.net/japson_iot/article/details/79983847
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞