并发的三大特性 -- java面试

原子性

原子性是指在一个操作中,cpu不能在中途暂停然后再调度,即不被中断操作,要么全部执行完成,要么全部不执行。

例子:

private long count = 0;
public void calc() {
    count++;
}
  1. 将count从主内存读到工作内存的副本中
  2. 对工作内存的count+1运算
  3. 将结果写入工作内存
  4. 将工作内存的值写入主内存

count++并不是原子操作,它包含了多个步骤。在多线程中,可能一个线程正在自增操作,另一个线程就已经读取了值,就会导致结果错误。那如果能保证自增操作是一个原子性的操作,那么就能保证其他进程读取的是递增后的数据。

可以使用synchronized解决。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。

当一个线程改变了i的值还没刷新到主内存,线程2又读取了i的值,那么这个i值还是以前的,线程2没有看到线程1对变量的修改,这就是可见性问题。

//线程1
boolean stop = false; 
while(!stop){
    doSomething();
}
//线程2
stop = true;

假如在线程2改变了stop变量的值后,还没来得及写入主内存中就转去做其他事情了,那么线程1因为看不到stop变量的更改,会继续循环。

可以使用volatile、synchronized、final解决。


volatile解决问题:

//线程1
volatile boolean stop = false; 
while(!stop){
    doSomething();
}
//线程2
stop = true;
  1. 使用volatile关键字会强制将修改的值立即写入主内存。

  2. 当线程2进行修改时,会导致线程1的工作内存中的缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)。

  3. 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1会再次会去主内存读取stop变量。这样线程1就可以正常关闭了。

有序性

虚拟机在编译时,对于那些改变顺序不影响结果的代码,虚拟机不一定按照我们编写代码的顺序来执行,有可能对它们进行重排序,这可能会出现线程安全问题。

int a = 0;
bool flag = false;
public void write() {
    a = 2;              //1
    flag = true;        //2
}
public void multiply() {
    if (flag) {         //3
        int ret = a * a;//4
} }

假设线程1执行write()方法,线程2执行multipy()方法

假如在write()方法中对1和2做了重排序,线程1先将flag设置为true,随后线程2执行语句3和4,那么ret就是0,这时候线程1继续运行,这才将a赋值2,很明显迟了一步,与预计结果的ret=4不符合。

可以使用volatile、synchronized解决。

synchronized和volatile

synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。

在某些情况下,volatile的同步机制的性能优于锁(使用synchronized关键字或java.util.concurrent),因为volatile的总开销要比锁低。

判断使用volatile还是加锁的依据是:volatile的语义能否满足使用的场景。

volatile作用

  1. 保证volatile修饰的共享变量对所有线程总是可见,也就是当一个线程修改这个变量,新值总是可以被其他线程立即得知。

  2. 禁止指令重排序。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。

上一篇:EasyCVR打包Linux版本报错NET_DVR_DownFileByName_Stop解决


下一篇:Vue 组件数据懒加载