Java实现原子操作的原理

来源:互联网 发布:python实战 编辑:程序博客网 时间:2024/05/17 20:22

原子的定义:

原子(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;@Overridepublic 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(){@Overridepublic 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 blocke.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释放锁。

0 0
原创粉丝点击