文章结构

前言

想要读懂 Java 中的并发包,就是要先读懂 CAS 机制,因为 CAS 是并发包的底层实现原理。本文主要讨论

  1.  CAS 是如何保证操作的原子性的
  2.  Java8 对 CAS 进行了哪些优化

synchronized:大材小用

我们先来看几行代码:

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

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

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

100个线程同时调用 increment()进行 i++ 操作,为什么得不到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 这个方法的时候,拿不到锁的方法是会被阻塞在方法外面的,最后再来唤醒他们,而阻塞/唤醒这些操作,是非常消耗时间的。

CAS :这种小事交给我

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

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

CAS原理

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

CAS原理翻译成代码示例

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:谁偷偷更改了我的值

什么是ABA?

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

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

如何解决ABA?

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

Java8 对 CAS 的优化

Java8之前CAS存在的问题

由于采用这种 CAS 机制是没有对方法进行加锁的,所以,所有的线程都可以进入 increment() 这个方法,假如进入这个方法的线程太多,就会出现一个问题每次有线程要执行第三个步骤的时候,i 的值老是被修改了,所以线程又到回到第一步继续重头再来

而这就会导致一个问题由于线程太密集了,太多人想要修改 i 的值了,进而大部分人都会修改不成功,白白着在那里循环消耗资源

Java8对CAS的优化方法

为了解决这个问题,Java8 引入了一个 cell[] 数组,它的工作机制是这样的:

  • 假如有 5 个线程要对 i 进行自增操作,由于 5 个线程的话,不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。
  • 如果有 100 个线程要对 i 进行自增操作的话,这个时候,冲突就会大大增加,系统就会把这些线程分配到不同的 cell 数组元素去。假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分成 10 组,每一组对 cell 数组其中的一个元素做自增操作,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而得到 100,二这,就等价于 100 个线程对 i 进行了 100 次自增操作。

【*】CAS 是什么,Java8是如何优化 CAS 的的更多相关文章

  1. 并发的核心:CAS 是什么?Java8是如何优化 CAS 的?

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

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

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

  3. java8 如何优化CAS的性能

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

  4. 并发系列2-大白话聊聊Java并发面试问题之Java 8如何优化CAS性能?【石杉的架构笔记】

  5. nginx反向代理cas server之1:多个cas server负载均衡配置以及ssl配置

    系统环境采用centOS7 由于cas server不支持session持久化方式的共享,所以请用其他方式代替,例如:组播复制. 为什么不支持session持久化:http://blog.csdn.n ...

  6. 利用Idea重构功能及Java8语法特性——优化深层嵌套代码

    当遇到深层嵌套代码,如for,if,lambda表达式或内部类及这些代码的组合,这时我们可以通过Java 8的语法特性来进行优化. 下面的代码是一个嵌套循环的示例. public MappedFiel ...

  7. 什么是CAS机制?(转)

    围绕下面四个点展开叙述: 一:什么是CAS机制? 二:Java当中CAS的底层实现 三:CAS的ABA问题和解决方法 四:java8对CAS的优化 一:什么是CAS机制? 我们先看一段代码: 启动两个 ...

  8. JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法

    JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...

  9. No.1 CAS 之LDAP认证服务端集群配置

    建档日期:   2016/08/31 最后修改日期:   2016/12/09   1 概述 本文描述了CAS单点登录服务端配置的大概流程,希望抛砖引玉,帮助你完成CAS服务端的配置. 本文采用apa ...

随机推荐

  1. KEYCODE_DPAD_CENTER 和 KEYCODE_ENTER

    KEYCODE_DPAD_CENTER或者是KEYCODE_ENTER时,会在view优先处理isConfirmKey,而在activity中的onKeyDown和up中接收不到该按键消息

  2. Broadcast的类型

    两种发送方法 1.无序广播 对于多个接收者来说是完全异步的,通常每个接收者都无需等待即可以接收到广播,接收者相互之间不会有影响.对于这种广播,接收者无法终止广播,即无法阻止其他接收者的 接收动作. 消 ...

  3. Ajax文件上传并添加Bootstrap进度条

    1.项目中需要用到文件上传和显示进度,网上各种插件搞得头晕,决定自己实现一个 三个步骤:Ajax上传文件,获取上传进度,显示进度 html: <!DOCTYPE HTML> <htm ...

  4. c++动态库封装及调用(3、windows下动态库调用)

    1.DLL的隐式调用 隐式链接采用静态加载的方式,比较简单,需要.h..lib..dll三件套.新建“控制台应用程序”或“空项目”.配置如下: 项目->属性->配置属性->VC++ ...

  5. 【Shell】获取当前路径

    bathpath=$(cd dirname $0 ; pwd)

  6. web服务器tomcat入门实战

    一.tomcat介绍1.1 引入tomcat Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache.Su ...

  7. php时间戳与日期转换

    日期转换为时间戳 PHP 提供了函数可以方便的将各种形式的日期转换为时间戳,该类函数主要是: strtotime():将任何英文文本的日期时间描述解析为时间戳. mktime():从日期取得时间戳. ...

  8. NodeJs>------->>第三章:Node.js基础知识

    第三章:Node.js基础知识 一:Node.js中的控制台 1:console.log.console.info  方法 console.log(" node app1.js 1> ...

  9. [翻译] 一个kubernetes网络简明教程[Part 1]

    一个kubernetes网络简明教程[Part 1] 翻译: icebug 所有我学到的关于kubernetes网络的事情 你可能已经在kubernetes集群当中跑了一堆服务并且正在享受其带来的好处 ...

  10. 性能测试三十九:Jprofiler分析CPU过高和响应时间长的问题

    使用Jprofiler监控分析案例 一.cpu负载过高:http://localhost:8080/PerfTeach/CpuTopServlet?id=1 cpu消耗高的可能原因1.使用了复杂的算法 ...