java8并发编程:volatile关键字详解

首先我们先介绍下java并发编程中2个问题

线程干扰

我们来看下Counter类

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

Counter类每次执行increment方法c的值加1,每次执行decrement方法c的值减1,然而当Counter被多个线程调用的时候,结果可能不是我们所期待的。

当在不同线程中运行作用于相同数据的两个操作发生交错时,就会产生干扰。这意味着这两个操作由多个步骤组成,并且步骤顺序重叠。

Counter实例上的操作似乎不可能交叉,因为c上的两个操作都是单一、简单的语句。然而,即使是简单的语句也可以被虚拟机转换成多个步骤。我们不会检查虚拟机所采取的具体步骤——只要知道单个表达式c++可以分解为三个步骤就足够了:

  1. 检索c的当前值
  2. 当前值加1
  3. 将当前值赋值给c

表达式c–亦可以按照这些步骤分解,只不过第二步是减1

假设线程A调用increment,而线程B调用decrement。如果c的初值为0,它们的交错动作可能遵循以下顺序:

  1. 线程A检索c的值
  2. 线程B检索c的值
  3. 线程A将检索值加1
  4. 线程B将检索值减1
  5. 线程A将检索值赋值给c,c为1
  6. 线程B将检索值赋值给c,c为-1

在不同的情况下,可能是线程B的结果丢失了,或者根本没有错误。由于线程干扰bug是不可预测的,因此很难检测和修复它们。

内存一致性错误

当不同线程对应该是相同数据的内容有不一致的视图时,就会发生内存一致性错误。内存一致性错误的原因非常复杂,幸运的是,我么不需要详细了解这些原因。所需要的只是一种避免它们的策略。

避免内存一致性错误的关键是理解happens-before的关系。这种关系只是保证一个特定语句的内存写入对另一个特定语句是可见的。要了解这一点,请考虑下面的示例。假设定义并初始化了一个简单的int字段:

int counter = 0;

counter字段被线程A和B共享,假设线程A增加counter:

counter++;

然后,不久之后,线程B打印出counter的值:

System.out.println(counter);

如果这两个语句是在同一个线程中执行的,那么可以安全地假设输出的值是“1”。但是如果这两个语句在不同的线程中执行,输出的值很可能是“0”,因为不能保证线程A对counter的更改对线程B是可见的——除非程序员在这两个语句之间建立了happens-before关系。

有多种方式可以创建happens-before关系:

  • 当一条语句执行Thread.start时,‘与该语句具有happens-before关系的每个语句‘和‘新线程执行的每条语句‘也具有happens-before关系。这导致创建新线程的代码的效果对新线程是可见的。
  • 当一个线程A终止时,在另一个线程B中的Thread.join调用会结束,线程A中所有语句与线程B中成功执行完join方法后的所有语句有happens-before关系。线程A代码的执行结果对线程B来说是可见的。

我们再来看下java并发编程中3个重要的概念(下面简单描述下,详细请查看文章最后的引用

原子性

原子行为不能在中间停止:它要么完全发生,要么根本不发生。原子操作的作用在完成之前是不可见的。在java中以下操作都是原子的:

  • 对于引用变量和大多数基本变量(除了long和double之外的所有类型),读写都是原子的。
  • 读写对于所有声明为volatile的变量(包括long变量和double变量)都是原子的。
  • java.concurrent.Atomic包下的原子类

原子操作不能交叉,因此可以使用它们而不用担心线程干扰。然而,这并不意味着原子操作在所有情景下都不需要同步了,因为内存一致性错误仍然是可能的

可见性

可见性指的是在一个线程中修改变量的值,随后另一个线程能立即看到该值。在java中可见性由happens-before关系决定

有序性

有序性指的是代码按照编码的顺序有序执行,之所以会有这个概念是因为JVM和CPU会将指令重排序来优化程序的性能。

volatile

volatile修饰的变量可以保证原子性、可见性和有序性,但是即使能满足这3点也不能保证变量在多线程环境下没有问题,我们来看下面这个例子:

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author RLP
 */
public class TestVolatile {

    private static int threadNum = 10;
    private volatile int a = 0;
    private static AtomicInteger c = new AtomicInteger(0);

    public class R implements Runnable {
        @Override
        public void run() {
        	//循环计数 statement 1
            for (int i = 0; i < 1000; i++) {
                a++;
            }
            c.incrementAndGet();
        }
    }

    public void pre() {
        Thread[] ts = new Thread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            Thread t = new Thread(new R());
            ts[i] = t;
        }
        for (int i = 0; i < threadNum; i++) {
            ts[i].start();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile testAtomic = new TestVolatile();
        testAtomic.pre();
        while (c.get() != threadNum) {
        }
        System.out.println(testAtomic.a);
    }

}

当我们运行上面的代码,控制台打印的值不总是我们期望的10000,因为在多线程下statement 1循环体内的a++操作并不是一个原子操作,我们前面已经描述过a++这种操作可以分解为3个指令,虽然每一条指令本身是原子的,但是3条指令在一起就不是原子操作了,这就会产生我们前面说过的线程干扰问题

https://blog.csdn.net/u012723673/article/details/80682208
https://blog.csdn.net/yjp198713/article/details/78839698
https://www.cnblogs.com/wq3435/p/6220751.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值