Java实现原子操作的原理

原子的定义:

原子(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释放锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值