原子性是指在一个操作中,cpu不能在中途暂停然后再调度,即不被中断操作,要么全部执行完成,要么全部不执行。
例子:
private long count = 0; public void calc() { count++; }
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;
使用volatile关键字会强制将修改的值立即写入主内存。
当线程2进行修改时,会导致线程1的工作内存中的缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)。
由于线程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关键字不满足原子性。
在某些情况下,volatile的同步机制的性能优于锁(使用synchronized关键字或java.util.concurrent),因为volatile的总开销要比锁低。
判断使用volatile还是加锁的依据是:volatile的语义能否满足使用的场景。
保证volatile修饰的共享变量对所有线程总是可见,也就是当一个线程修改这个变量,新值总是可以被其他线程立即得知。
禁止指令重排序。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。