引入的原因:
- 线程间通信可以使用共享变量的方式(一块公共内存)
- 现代计算机都是多cpu的,并且为了提高效率,cpu都是带缓存的,并且设计成分层的,有一级缓存、二级缓存、三级缓存(L1,L2,L3)。每个cpu有自己的cache
L1响应时间1ns,3个时钟周期,大小32K
L2响应时间3ns,大小256K
L3响应时间12ns,大小8M
直接访问主内存的响应时间65ns
为什么设计这么复杂,还分层?
就是考虑成本的问题,最近也看到新闻说内存涨价了,如果不考虑成本那就简单了,直接都怼上最贵的L1cache,就不需要开发人员考虑这么多细节了。
实际上一些应用程序最热的数据也就是那么几K,分层的目的也就是这个原因。
有以上是背景,在两个线程访问的时候,如果是不同的cpu执行这两个线程,就会出现缓存不一致问题。
volatile的作用
- 被volatile修饰的变量,jvm会做一些底层的工作,保证写入的操作,把更改后的内容立刻从cpu缓存回写到主内存。
- 保证了读的可见性,当一个线程读到被volatile修饰的变量的时候,会抛弃cpu缓存的值,从主内存复制一份到cpu缓存内,读取。保证读取得是最新的值。
synchronized和volatile作用的异同
当使用synchronized加锁的方式,也可以实现多线程下可见性。还能保证写入的原子性。
但是volatile不能保证多线程下操作的原子性。如下面例子
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
要保证操作的原子性,处理synchronized关键字和lock外,还可以使用concurrent包下的AtomicInteger原子操作类,原来是利用硬件的原子操作指令cap。
volatile使用场景
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
不使用volatile,会有概率出错。
没有使用锁是因为,volatile性能更高。