大家可能都听说说 Java 中的并发包,如果想要读懂 Java 中的并发包,其核心就是要先读懂 CAS 机制,因为 CAS 可以说是并发包的底层实现原理。

今天就带大家读懂 CAS 是如何保证操作的原子性的,以及 Java8 对 CAS 进行了哪些优化。

synchronized:大材小用

我们先来看几行代码:

public class CASTest {
static int i = 0; public static void increment() {
i++;
}
}

假如有100个线程同时调用 increment() 方法对 i 进行自增操作,i 的结果会是 100 吗?

学会多线程的同学应该都知道,这个方法是线程不安全的,由于 i++ 不是一个原子操作,所以是很难得到 100 的。

这里稍微解释下为啥会得不到 100(知道的可直接跳过), i++ 这个操作,计算机需要分成三步来执行。

1、读取 i 的值。

2、把 i 加 1.

3、把 最终 i 的结果写入内存之中。所以,假如线程 A 读取了 i 的值为 i = 0,这个时候线程 B 也读取了 i 的值 i = 0。接着 A把 i 加 1,然后写入内存,此时 i = 1。紧接着,B也把 i 加 1,此时线程B中的 i = 1,然后线程 B 把 i 写入内存,此时内存中的 i = 1。也就是说,线程 A, B 都对 i 进行了自增,但最终的结果却是 1,不是 2.

那该怎么办呢?解决的策略一般都是给这个方法加个锁,如下

public class CASTest {
static int i = 0; public synchronized static void increment() {
i++;
}
}

加了 synchronized 之后,就最多只能有一个线程能够进入这个 increment() 方法了。这样,就不会出现线程不安全了。不懂 synchronized 的可以看我这篇文章:彻底搞懂synchronized(从偏向锁到重量级锁)

然而,一个简简单单的自增操作,就加了 synchronized 进行同步,好像有点大材小用的感觉,加了 synchronized 关键词之后,当有很多线程去竞争 increment 这个方法的时候,拿不到锁的方法是会被阻塞在方法外面的,最后再来唤醒他们,而阻塞/唤醒这些操作,是非常消耗时间的。

这里可能有人会说,synchronized 到了JDK1.6之后不是做了很多优化吗?是的,确实做了很多优化,增加了偏向锁、轻量级锁等, 但是,就算增加了这些,当很多线程来竞争的时候,开销依然很多,不信你看我另外一篇文章的介绍:彻底搞懂synchronized(从偏向锁到重量级锁)

CAS :这种小事交给我

那有没有其他方法来代替 synchronized 对方法的加锁,并且保证 increment() 方法是线程安全呢?

大家看一下,如果我采用下面这种方式,能否保证 increment 是线程安全的呢?步骤如下:

1、线程从内存中读取 i 的值,假如此时 i 的值为 0,我们把这个值称为 k 吧,即此时 k = 0。

2、令 j = k + 1。

3、用 k 的值与内存中i的值相比,如果相等,这意味着没有其他线程修改过 i 的值,我们就把 j(此时为1) 的值写入内存;如果不相等(意味着i的值被其他线程修改过),我们就不把j的值写入内存,而是重新跳回步骤 1,继续这三个操作。

翻译成代码的话就是这样:

public static void increment() {
do{
int k = i;
int j = k + 1;
}while (compareAndSet(i, k, j))
}

如果你去模拟一下,就会发现,这样写是线程安全的。

这里可能有人会说,第三步的 compareAndSet 这个操作不仅要读取内存,还干了比较、写入内存等操作,,,这一步本身就是线程不安全的啊?

如果你能想到这个,说明你是真的有去思考、模拟这个过程,不过我想要告诉你的是,这个 compareAndSet 操作,他其实只对应操作系统的一条硬件操作指令,尽管看似有很多操作在里面,但操作系统能够保证他是原子执行的。

对于一条英文单词很长的指令,我们都喜欢用它的简称来称呼他,所以,我们就把 compareAndSet 称为 CAS 吧。

所以,采用 CAS 这种机制的写法也是线程安全的,通过这种方式,可以说是不存在锁的竞争,也不存在阻塞等事情的发生,可以让程序执行的更好。

在 Java 中,也是提供了这种 CAS 的原子类,例如:

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicLong
  4. AtomicReference

具体如何使用呢?我就以上面那个例子进行改版吧,代码如下:

public class CASTest {
static AtomicInteger i = new AtomicInteger(0); public static void increment() {
// 自增 1并返回之后的结果
i.incrementAndGet();
}
}

CAS:谁偷偷更改了我的值

虽然这种 CAS 的机制能够保证increment() 方法,但依然有一些问题,例如,当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,之后又马上把 i 的值减 1,然后,线程 A 执行第三步,这个时候线程 A 是认为并没有人修改过 i 的值,因为 i 的值并没有发生改变。而这,就是我们平常说的ABA问题

对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。

来个版本控制吧

为了解决这个 ABA 的问题,我们可以引入版本控制,例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不同,这样,我们就可以预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就可以进行版本控制了。

Java8 对 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 次自增操作。

当然,我这里只是举个例子来说明 Java8 对 CAS 优化的大致原理,具体的大家有兴趣可以去看源码,或者去搜索对应的文章哦。

总结

理解 CAS 的原理还是非常重要的,它是 AQS 的基石,而 AQS 又是并发框架的基石,接下来有时间的话,还会写一篇 AQS 的文章。

最后推广下我的公众号:苦逼的码农戳我即可关注,文章都会首发于我的公众号,期待各路英雄的关注交流。

并发的核心:CAS 是什么?Java8是如何优化 CAS 的?的更多相关文章

  1. 【*】CAS 是什么,Java8是如何优化 CAS 的

    文章结构 前言 想要读懂 Java 中的并发包,就是要先读懂 CAS 机制,因为 CAS 是并发包的底层实现原理.本文主要讨论  CAS 是如何保证操作的原子性的  Java8 对 CAS 进行了哪些 ...

  2. 并发的核心:CAS 与synchronized, Java8是如何优化 CAS 的?

    大家可能都听说说 Java 中的并发包,如果想要读懂 Java 中的并发包,其核心就是要先读懂 CAS 机制,因为 CAS 可以说是并发包的底层实现原理. 今天就带大家读懂 CAS 是如何保证操作的原 ...

  3. Java并发编程核心知识体系精讲

    第1章 开宗明义[不看错过一个亿]本章一连串设问:为什么学并发编程?学并发编程痛点?谁适合学习本课?本课程包含内容和亮点?首先4大个理由告诉你为什么要学,其实源于JD岗位要求就不得不服了.其次5个痛点 ...

  4. Java 并发机制底层实现 —— volatile 原理、synchronize 锁优化机制

    本书部分摘自<Java 并发编程的艺术> 概述 相信大家都很熟悉如何使用 Java 编写处理并发的代码,也知道 Java 代码在编译后变成 Class 字节码,字节码被类加载器加载到 JV ...

  5. 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

    package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...

  6. 高并发场景下System.currentTimeMillis()的性能问题的优化

    高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...

  7. Java并发编程核心概念一览

    作者博客地址 https://muggle.javaboy.org. 并行相关概念 同步和异步 同步和异步通常来形容一次方法的调用.同步方法一旦开始,调用者必须等到方法结束才能执行后续动作:异步方法则 ...

  8. java8 如何优化CAS的性能

    场景引入 经常都会有下面这段代码,多个线程同时修改一个变量,造成线程不安全,代码如下: public class ThreadCASDemo implements Runnable { static ...

  9. Java并发编程核心方法与框架-TheadPoolExecutor的使用

    类ThreadPoolExecutor最常使用的构造方法是 ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAli ...

随机推荐

  1. txtbook简单HTML可读化改造

    一般来讲下载的txtbook在notepad或者之类的文本编辑器下的阅读体验不是很好,PC上面专门装个txt阅读器好像有点杀鸡用牛刀,可以用HTML简单处理一下就可以放在浏览器下爽快的阅读了,这个操作 ...

  2. bootstrap响应式设计简单实践。

    首先需要熟悉Boostrap提供的响应式设施:http://getbootstrap.com/css/#responsive-utilities,BootStrap的响应式设施主要是利用媒体查询对元素 ...

  3. centos安装nginx(针对一哥们的博客进行的详细补充(用红色字体标出了补充部分))

    centos安装nginx 原文地址:http://www.cnblogs.com/leechenxiang/p/5402960.html 一.什么是nginx 是一个C语言开发的HTTP反向代理服务 ...

  4. 第三方支付设计——账户体系

    第三方支付架构设计之-帐户体系 一,      什么是第三方支付?         什么是第三方支付?相信很多人对这个名字很熟悉,不管是从各种媒体等都经常听到,可以说是耳熟能熟.但,如果非得给这个名词 ...

  5. 前后台分离部署时,Niginx上的部署

    upstream bowenpay_backend { server 127.0.0.1:9002; } server { listen 80; server_name wx.bowenpay.com ...

  6. 从数据库读取数据并动态生成easyui tree构结

    一. 数据库表结构 二.从后台读取数据库生成easyui tree结构的树 1.TreeNode树结点类(每个结点都包含easyui tree 的基本属性信息) import java.io.Seri ...

  7. [ Java面试题 ] 框架篇

    1.谈谈你对Struts的理解. 1. struts是一个按MVC模式设计的Web层框架,其实它就是一个Servlet,这个Servlet名为ActionServlet,或是ActionServlet ...

  8. 简明4步,让Python的好朋友Pycharm变得更加雅观!

    跟着人工智能/机械学习的兴起,Python再次取得广泛程序员的关注.而JetBrains出品的PyCharm无疑是最好用的Python IDE之一. 把本身的IDE装备安排得既有逼格又雅观,这是每个P ...

  9. HTML知识点总结

    HTML知识点总结 一.需要熟悉的基本快捷键 ctrl+c            复制 ctrl+v            粘贴 ctrl+x            剪切 ctrl+tab       ...

  10. 计算机协议、标准以及OSI模型的简单介绍

    由概念启发学习,引导学习.本篇文章中包含了一些最基本的概念和底层知识.虽然零碎,但是这是基础. 一.协议和标准 协议指的是一组控制数据通信的规则.协议有三要素:语法(syntax),语义(semant ...