您好,登录后才能下订单哦!
这篇文章主要讲解了“java多线程CAS的介绍”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“java多线程CAS的介绍”吧!
1 CAS讲解
在工作中,我们往往需要面对多线程计数的问题,我们第一反应,使用“synchronized”,控制并发。
@Slf4j public class CASDemo extends Thread{ private static int t = 0; @Override public void run() { increment(); log.info("-----------{}------------",t); } private synchronized static void increment() { t++; } public static void main(String[] args){ for (int i = 0 ; i < 100 ; i++){ CASDemo casDemo = new CASDemo(); casDemo.start(); } }
结果如下:
从上面结果来看符合我们的预期,100个线程执行了1次,最后t为100。但是这是最好的实现机制吗?我们知道“synchronized”是同步锁,即使jdk6以后已经对其进行了优化(具体可以见另一篇文章:java多线程基础知识之synchronized原理分析),只是用来计数,是否太大材小用了。有没有一种更加优雅的解决方案?
@Slf4j public class CASDemo extends Thread{ private static AtomicInteger t = new AtomicInteger(0); @Override public void run() { log.info("-----------{}------------",t.incrementAndGet()); } public static void main(String[] args){ for (int i = 0 ; i < 100 ; i++){ CASDemo casDemo = new CASDemo(); casDemo.start(); } } }
结果如下:
从结果上来看,第一种和第二种结果是一样的。为啥推荐第一种方法呢?因为第二种使用了无锁式的“compareAndSwap”即“CAS”,既然“CAS”是无锁的,那么是怎么样保证其实线程安全的。
“compareAndSwap”从字面上看,这一过程肯定包含了比较和替换两个动作,具体步骤如下:
(1)线程从内存中读取 i 的值,假如此时 i 的值为 0,我们把这个值称为 k 吧,即此时 k = 0。
(2)令 j = k + 1。
(3)用 k 的值与内存中i的值相比,如果相等,这意味着没有其他线程修改过 i 的值,我们就把 j(此时为1) 的值写入内存;如果不相等(意味着i的值被其他线程修改过),我们就不把j的值写入内存,而是重新跳回步骤 1,继续这三个操作。具体源码如下:
同时,整个"CAS"是原子的,对应操作系统的一条硬件操作指令,尽管看似有很多操作在里面,但操作系统能够保证它是原子执行的。
通过上面的流程讲解,我们可以发现其不可能从内存中同时取到相同的K值,并且分别+1然后提交到内存中,从而保证的线程安全。
但是我们仍然面临一个问题:谁偷偷更改了我的值。
举个例子,当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,之后又马上把 i 的值减 1,然后,线程 A 执行第三步,这个时候线程 A 是认为并没有人修改过 i 的值,因为 i 的值并没有发生改变。而这,就是我们平常说的ABA问题。对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。
怎么解决这个问题呢?——版本控制(参考乐观锁)。
例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不同,这样,我们就可以预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就可以进行版本控制了。
给个demo.
//构造方法, 传入引用和戳 public AtomicStampedReference(V initialRef, int initialStamp) //返回引用 public V getReference() //返回版本戳 public int getStamp() //如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) //如果当前引用 等于 预期引用, 将更新新的版本戳到内存 public boolean attemptStamp(V expectedReference, int newStamp) //设置当前引用的新引用和版本戳 public void set(V newReference, int newStamp)
public static void main(String[] args) { String str1 = "aaa"; String str2 = "bbb"; AtomicStampedReference<String> reference = new AtomicStampedReference<String>(str1,1); reference.compareAndSet(str1,str2,reference.getStamp(),reference.getStamp()+1); System.out.println("reference.getReference() = " + reference.getReference()); boolean b = reference.attemptStamp(str2, reference.getStamp() + 1); System.out.println("b: "+b); System.out.println("reference.getStamp() = "+reference.getStamp()); boolean c = reference.weakCompareAndSet(str2,"ccc",4, reference.getStamp()+1); System.out.println("reference.getReference() = "+reference.getReference()); System.out.println("c = " + c); } 输出: reference.getReference() = bbb b: true reference.getStamp() = 3 reference.getReference() = bbb c = false c为什么输出false呢, 因为版本戳不一致啦
2jdk8对CAS的优化
由于采用这种 CAS 机制是没有对方法进行加锁的,所以,所有的线程都可以进入 increment() 这个方法,假如进入这个方法的线程太多,就会出现一个问题:每次有线程要执行第三个步骤的时候,i 的值老是被修改了,所以线程又到回到第一步继续重头再来。
而这就会导致一个问题:由于线程太密集了,太多人想要修改 i 的值了,进而大部分人都会修改不成功,白白着在那里循环消耗资源。
为了解决这个问题,Java8 引入了一个 cell[] 数组,它的工作机制是这样的:假如有 5 个线程要对 i 进行自增操作,由于 5 个线程的话,不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。但是,如果有 100 个线程要对 i 进行自增操作的话,这个时候,冲突就会大大增加,系统就会把这些线程分配到不同的 cell 数组元素去,假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分成 10 组,每一组对 cell 数组其中的一个元素做自增操作,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而得到 100,二这,就等价于 100 个线程对 i 进行了 100 次自增操作。
总之,jdk8对于高并发的情况下,采用了类似减少锁粒度方法来提高性能。
感谢各位的阅读,以上就是“java多线程CAS的介绍”的内容了,经过本文的学习后,相信大家对java多线程CAS的介绍这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是亿速云,小编将为大家推送更多相关知识点的文章,欢迎关注!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。