并发编程(1)- volatile关键字
聊到并发,我们首先想到的应该是变量共享、锁。我们便从变量共享以及锁作为切入点开始我们的并发编程之路。
变量共享:是Java内存模型规定,对于多个线程共享的变量,存储在主内存(JVM表现形式为线程共享区域)当中,每个线程都有自己独立的工作内存(JVM中表现形式为线程独占区域),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。
为了保证多线程协同操作主内存的完整性,JAVA内存模型定义了8种原子操作来实现主内存与工作内存之间的交互。
lock: 锁定,锁定共享变量,使其变成线程独占状态,其他线程不可访问。
unlock: 解锁,和lock相对应将变量从线程独占状态解锁为可访问状态。
read:读取,将变量从主内存中读取到工作内存中。
load:载入,将读取到的变量赋值给工作内存中变量副本。
use:使用,在执行字节码的时候将变量传递给解释引擎进行使用。
assign: 赋值,将变量从解释引擎中赋值给工作内存中的变量。
store:存储,将工作内存中的变量的值传送到主内存。
write:写入,将传入的主内存的值写入主内存变量中。
上面是主内存与工作内存之间的交互,上面通过文字描述每个原子操作相信读者已经比较清楚了。下面用图像更加形象的表示出来。
上图将8个原子操作中的6个表现了出来。
其中A到A'为线程主内存中将A的值read进工作内存然后在load进A(副本变量),在虚拟机栈帧进行解释的时候use该变量。B到B'是工作线程向主线程写入值的过程首先将值复制到B变量,然后存储到主内存,主内存获取到值后将其写入到B变量中。
另外两个原子操作就是加锁和解锁,通过上面介绍共享变量的读取,应该很容易理解加锁以及解锁。就是在一个线程完成对某个变量更改完成其他线程不能获取到它。这块不做详细解释,直接贴一张图
下面引入今天的主角 volatile,这个关键字解决变量在程序运行过程中的可见性问题。具体怎么理解呢。下面讲解。
从上面工作内存获取主内存中的值的过程可以发现在use的时候可能主内存的值已经改变这个时候工作内存里面的值实际上是旧的。这是由于read、load、use三个没有在一个操作中。
volatile的规则是:
read、load、use动作必须连续出现。
assign、store、write动作必须连续出现。
这样就可以解决在工作内存使用的时候不会出现工作内存的值和主内存中的值不一致问题。因为在使用的时候是先去读取内存的值这时是会有刷新操作,读取,加载、使用是连续出现的。反之写入的时候也是连续出现的保证了主内存和工作内存进行值交换的时候的可见性。
但是volatile并不能保证变量的原子性,因为读取、和写入是分开的。下面举例子说明
public class VolatileTest {
private volatile int a = 0;
public void increase() {
a++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
test.increase();
}
}
}.start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(test.a);
}
}
上面代码执行的值并不一定会等于1000000。这也证明了volatile关键字不能保证原子性,原子性应该是6个动作都做完其他线程才能访问。解决该问题可以使用synchronized关键字、原子变量、Lock来解决该问题。
volatile的第二个特性就是禁止指令重排。
在计算机处理的时候会将需要执行的指令进行重排优先处理那些耗时短的指令,可以提高处理效率。比如 a=1;b=1;c=a+b;三条语句中只有第三条依赖前面两条,但是前面两条的执行顺序是没有关系的,所以可以乱序赋值。
volatile保证每次写或者读操作的时候设置一个内存屏障保证,保证3个原子操作不会被重排。这块怎么理解呢?
int a =3;
没有添加volatile的时候两个线程去使用它可能指令会是read a(线程1),read a(线程2),load a(线程1),use a(线程1),load a(线程2),use a(线程2)。在进行读写操作的时候会存在多线程同步进行。
添加了volatile关键字之后,执行可能是这样 read a、load a、use a(线程1),read a、load a、use a(线程2)。这便是内存屏障的作用。
看到这里你可能已经蒙了。没错、蒙了就对了,这玩意有啥用处呢。不能当锁用。没有存在的必要吧。我们可以用一个例子来解释指令重排带来的问题。单例模式下初始化对象常使用的就是DCL(双重校验机制)。
public class TEST{
private static TEST instance;
public static TEST getInstance() {
if (instance == null) {
synchronized (TEST.class) {
if (instance == null) //检查
instance = new TEST(); //创建
}
}
return instance;
}
}
在创建对象的时候有很多指令,我们简化为三步
1、申请内存空间
2、通过构造方法对空间进行初始化
3、将初始化的空间复制给变量
由于指令重排可能在使用的时候获取到的是一个没有进行初始化的内存空间。或者说还在进行3的时候还没有完成,另外一个线程就获取到了这个对象去做操作。也会异常。volatile关键字保证了在另外一个线程判断instance是都为空的时候第3步是已经完成了并且写入了内存中的。
扫一扫上方的二维码关注微信公众号有更多好文推荐给大家哦!