JVM CAS与原子类

CAS

CAS是配合volatile使用的重要技术,全称是Compare and Swap,他体现的是一种乐观锁的思想,他也被称之为一种叫无锁并发

之前提到要保护一些共享变量的安全性要使用syncronized加锁,而CAS是根本不加锁,比如用CAS想对多个线程要对一个共享的整型变量执行+1操作:

// 需要不断尝试
while(true) {
	int 旧值 = 共享变量; // 比如拿到了当前值0
	int 结果 = 旧值 + 1; // 在旧值0的基础上增加1,正确结果是1

	/*
	这时候如果别的线程把共享变量改成了5,本县城的正确结果1就作废了,这时候compareAndSwap返回false,重新尝试,直到:
	compareAndSwap返回true,表示我本线程做修改的同时,别的线程没有干扰
	*/
	if (compareAndSwap(旧值,结果)) {
		// 成功,退出循环
	}
}

他首先用的是while死循环,他会首先去把共享变量的值读进来,从主存读到工作内存,赋值给‘旧值’,接下来我想在这个线程内对他进行+1操作,那我就在他的旧值基础上进行+1,赋值给‘结果’,接下来是关键,他会调用compareAndSwap函数,这个方法他就是会尝试把‘结果’赋值给共享变量,但是赋值的同时,他会把这个‘旧值’跟共享变量当前的值做比较,因为他怕我在写入‘结果’的时候,有其他线程已经把共享变量值改了,所以他得拿原先上一次他读到的值(‘旧值’)跟这个共享变量当前的值做比较,如果这两个是一致的,那我这个结果可以成功的写入到这个变量里(网友1:共享变量还是不是旧值,如果是,就赋新值。),但如果共享变量当前的值,与我上一次的‘旧值’不一样了,那说明其他线程把这个共享变量改了(网友2:匹配不上就重试。),那改了的话我这一次操作或这一次尝试就失败了,失败了以后compareAndSwap就会返回false,返回false的话就会重新进行一次while循环,重新一次while循环的话,他就会重新读取共享变量的最新值,来重复刚才的过程。

可以想象一下,比如‘旧值’假设是0,他在‘旧值’基础上+1,得到了最新的‘结果’为1,他想把这个1写回给共享变量,但写的时候‘旧值’是0,但比如共享变量已经被别人给修改成5了,那05相比肯定不一致了,说明其他线程干扰了,那么这一次的修改就作废,重新循环一次,循环时重新获取最新的共享变量的值5,然后在5的基础上再去+1得到了6,得到6以后我再把‘旧值 5’再跟共享变量的当前值做比较,假设没有其他线程干扰,结果这回发现共享变量当前值和‘旧值’都是5,那我就可以安全的把6写入共享变量。这就是CAS的机制,他采用的是一种不断尝试的机制,直到成功的那一天

这里为了读到共享变量的最新结果,比如有可能存在可见性的问题(比如拿到的共享变量不是最新的),所以需要使用volatile修饰共享变量,所以CAS一定要跟volatile结合才能有效,否则你拿到的共享变量值不一定是最新的,那CAS的比较机制就无从谈起了。这种方式可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。

首先竞争不激烈这是一个前提,如果竞争激烈,可以想象比如说我想去用CAS修改,我刚计算好了‘结果’就想把它set给共享变量,结果其他线程就把他改了,那我还得重试,那我又除了一次新值,又想去改,结果别的线程又把他给变动了,那我又得循环一次…所以,若竞争激烈,那重试就会频繁发生,反而会让效率受到一定的影响

第二点是他应该工作在多核的CPU的场景下,因为重试操作他是要使用CPU时间的,他不像原来的synchronized,比如synchronized就是说你在等待的过程中他线程就阻塞了,换句话说他就在休眠了,那就必须等到其他线程把这个锁释放开我这个线程才能恢复运行,而我们的CAS他是要不断重试不断重试的,所以他需要利用CPU时间,如果你CPU就一个,那这个CAS就无从谈起了,因为其他的线程在修改时你想重试也没有CPU的时间可用,所以第二个条件是他必须工作在多核的场景下,所以他是不会陷入阻塞,这也是他效率提升的因素之一,一旦线程阻塞,涉及到线程的上下文切换,所谓的上下文切换就是它得把这个线程当前的运行状态给保存下来,然后让他去一遍阻塞休眠去,所以等到他再次唤醒,他又得把上次的状态又进行恢复,这个是很耗时的,而我们的CAS是由于线程一直在跑,他是不会陷入阻塞了,所以就没有线程的上下文切换,所以他的效率在低竞争的时候其实要比synchronized高很多

CAS底层(实现)依赖于一个Unsafe类来直接调用操作系统底层的CAS指令,下面是直接使用Unsafe对象进行线程安全保护的一个例子,Unsafe我们并不能直接去用,而是要通过反射的方式去拿到这个类,才能调用CAS的相关方法。当然,这个例子并不用去掌握,因为JDK内部他封装了Unsafe这个类的一些方法,底层会用Unsafe去调用我们的CAS指令。他的本意并不是让程序员自己去使用Unsafe:(例子代码省略,因为自己没必要去实现,JDK为我们设计好了,就是原子类

Java里的乐观锁和悲观锁

Java里的悲观锁实际上就是指syncronized,而Java里的乐观锁就是指CAS

  • CAS是基于乐观锁的思想:基于最乐观的估计,不怕别的线程来修改共享变量,计算改了也没关系,我吃亏点再重试呗。
  • synchronized是基于悲观锁的思想:基于最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

原子操作类

JDK5开始,新增了juc(java.util.concurrent Java并发工具包)线程工具类,其中提供了一系列的原子操作类,可以提供线程安全的操作,例如:AtomicIntegerAtomicBoolean等,其中AtomicInteger是原子整数类,他能保证整数操作的一些自增自减的线程安全,他们的底层就是采用CAS技术+volatile来实现的。

// 创建原子整数对象,初始值为0
private static AtomicInteger i = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
	// 主方法里开启了两个线程

	// 线程t1循环5000次,调用原子整数对象的自增方法
	Thread t1 = new Thread(()->{
		for(int j = 0; j < 5000; j ++) {
			i.getAndIncrement();// 获取并且自增,类似于i++
			// i.incrementAndGet(); // 自增并且获取,类似于++i
		}
	});

	// 线程t2循环5000次,自减
	Thread t2 = new Thread(()->{
		for(int j = 0; j < 5000; j ++) {
			i.getAndDecrement();// 获取并且自减i--
		}
	});

	t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(i);// 0
}

他是利用了无锁并发的方式,来保证我的原子整数类中整数信息的线程安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值