原子的定义:
原子(atomic)本意是"不能被进一步分割的最小粒子”,而原子操作描述为:“不可被中断的一个或一系列操作“。在多核处理器上实现原子操作就会变得复杂了许多。
原子操作的实现:
1.术语定义
术语名称 | 英文 | 解释 |
缓存行 | Cache line | 缓存的最小单位 |
比较并交换 | Compare and Swap | CAS操作需要输入两个数值,一个旧值(期望操作 前的值),一个新值,在操作期间先比较旧值有没有 发生变化,如果没有发生变化才交换成新值,发 生了变化则不交换。 |
CPU流水线 | CPU pilelineCPU | 流水线的工作方式就像工业生产上的装配流水线,在CPU中由5 5-6个不同功能的电路单元组成一条指令处理流水线,,然后将一条 X86指令分成5-6步后再由这些电路单元分别执行。这样就能实现 在一个CPU时钟周期完成一条指令,因此提高 CPU的运算速度 |
内存顺序冲突 | Memory order violation | 内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓冲行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线 |
2.处理器如何实现原子操作
(1)使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读写操作就不是原子的,操作完之后共享变量的值会和期望的不一样。
public class Test6 {
public static void main(String[] args) {
Count count=new Count();
Count count2=new Count();
count.start();
count2.start();
}
}
class Count extends Thread{
private static int i=1;
@Override
public void run() {
i++;
System.out.println(i);
super.run();
}
}
这里我们期望打印出2和3,但是结果有可能会出现2,2和3,3;原因可能多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存中。想要保证都改写共享变量的操作都是原子的,就必须保持CPU1(线程1)读改写变量i的时候,CPU2(线程2)不能操作缓存了该共享变量内存地址的缓存。
处理器的总线锁就是解决这个问题的。所谓总线锁就是使用处理器提供一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的轻轻将被阻塞,那么该处理器可以共享内存。
(2).使用缓存锁保证原子性
第二个机制是通过缓存锁来保证原子性。在同一时刻,我们只需要保证对某个内存的操作是原子性即可。总线锁的开销很大,目前处理器会在某些场合使用缓存锁代替总线锁来进行优化。
有两种情况下不能使用缓存锁
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line)时,则处理器会调用总线锁定;
第二种情况:有些处理器不支持缓存锁定。对于Intel486和Pentium处理器,就是有缓存行也会调用总线锁定;
Java如何实现原子操作:
(1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的,自旋的CAS实现的基本思路就是循环进行CAS操作直到成功为止
/*/
* 计数器
*/
public class Counter {
private AtomicInteger atomic=new AtomicInteger(0);
private int i=0;
public static void main(String[] args) {
final Counter counter=new Counter();
List<Thread> list=new ArrayList<Thread>(); //创建线程集合
long start=System.currentTimeMillis(); //记录下开始时间
for(int j=0;j<100;j++){
Thread t=new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<1000;i++){
counter.count(); //非线程安全计数器
counter.safeCount(); //线程安全
}
}
}); //以匿名内部类的方式创建线程
list.add(t);
}
for(Thread t:list){
t.start();//启动所有线程
}
//等待所有线程执行完毕
for(Thread t:list){
try {
t.join(); //得到上一个线程的锁
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(counter.i);
System.out.println(counter.atomic.get());
System.out.println(System.currentTimeMillis()-start);
}
/*/
* 非线程安全计数器
*/
private void count(){
i++;
}
/*
* 使用cas实现线程安全计数器
*/
private void safeCount(){
for(;;){
int i=atomic.get();
boolean bl=atomic.compareAndSet(i, ++i);
if(bl){
break;
}
}
}
}
上面这个类实现了一个线程安全的计数器和一个非线程安全的。在JDK1.5之后提供了一些并发包来进行原子操作,如AtomicInteger(用原子的方式更新int值),AtomicBoolean等。这些类里面还提供了自增和自减等操作的方法;
(2)CAS实现原子操作的三大问题
1.ABA问题
因为CAS需要在操作值得时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值有没有变化,但是实际上却变化了。那么ABA问题的解决思路就是使用版本号,在变量前面加上版本号,每次变量更新的时候把版本号加1,那么A-B-C就会变成1A-2B-3A。
2.循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供pause指令,那么效率会有一定的提示。pause指令的两个作用,第一,它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本。第二,它可以避免在推出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提高CPU的效率;
3.只能保证一个共享变量的原子操作
当对一个共享变量操作时,我们可以使用CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作,这个时候就可以用锁。
(3)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很大锁机制,有偏向锁,轻量级锁和互斥锁有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程向进入同步块的时候使用CAS的方式来获取锁,当它退出同步块的是很好使用循环CAS释放锁。