概念
不可被中断的一个或一系列操作
术语定义
术语名称 | 英文 | 解释 |
---|---|---|
缓存行 | cache line | 缓存的最小操作单位 |
比较并交换 | Compare And Swap | CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有变化,才交换成新值,发生了变化则不交换 |
CPU流水线 | CPU pipeline | CPU流水线的工作方式就像工厂装配流水线,在CPU中有5-6个不用功能的电路单元组成一个指令处理流水线,然后将一条x86指令分成5-6步后在由这些电路单元分别执行,这样就能实现一个CPU时钟周期完成一条指令,因此提供CPU的运算速度 |
内存顺序冲突 | Memory Order Violation | 内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改用一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现内存顺序冲突时,CPU必须清空流水线 |
处理器如何实现原子操作
- 使用总线锁保证原子性
总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么处理器独占共享内存 - 使用缓存锁保证原子性
处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性会阻止同时修改由两个以上处理器缓存的内存区域数据,当其它处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
Java如何实现原子性
Java中通过锁和循环CAS的方式实现原子操作
循环CAS
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
public static void main (String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for(int j = 0; j < 100 ; j++){
Thread t = new Thread(new Runable(){
public void run(){
for(int i = 0; i < 10000 ; i++ ){
cas.count();
cas.safecount();
}
}
});
ts.add(t);
}
for(Thread t : ts)
t.start();
//等待所有的线程执行完成
for(Thread t : ts){
try{
t.join();
}catch(Exception e){
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cae.automicI.get());
System.out.println(System.currentTimeMillis()-start);
}
private void safeCount(){
for(;;){
int i = automicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if(suc)
break;
}
}
/**非线程安全*/
private void count(){
i++;
}
}
JDK1.5之后,并发包提供一些类来进行原子操作,如AtomicBoolean,AutomicInteger,AtomicLog等
CAS原子操作的三大问题
ABA问题
CAS需要在操作值的时候,检查有没有变化,如果没有发生变化则更新。如果一个值原来时A,改成B,又改成A。那么使用CAS会发现它的值没有变化。如果要解决ABA问题,可以在变量前加上版本号,1A,2B,3A来区分,如JDK提供AtomicStampedReference来解决,这个类的compareAndSet方法会先检查当前引用是否等于预期引起,并检查当前标志是否等于预期标志,如果都相等,则以原子的形式将该引用和标志设置为新的值。循环时间长开销大
CAS长时间不成功会给CPU带来非常大的开销只能保证一个共享变量的原子操作
对于一个共享变量我们可以通过CAS保证原子性,但是多个共享变量,要保证原子性就只能用锁。
锁机制实现原子操作
锁机制保证只有获得锁的线程才能操作锁定的内存区域,JVM内部有很多锁机制,偏向锁,轻量锁和互斥锁。除了偏向锁,JVM实现锁的方式都用了循环CAS(自旋),即当一个线程想进入同步块的时候使用自旋的方式获取锁,当它退出同步块时,采用自旋释放锁。