CASJAVA一些理解
如果不用锁机制如何实现共享数据访问。(不要用锁,不要 用sychronized 块或者方法,也不要直接使用 jdk 提供的线程安全
的数据结构,需要自己实现一个类来保证多个线程同时读写这个类中的共享数据是线程安全的,怎么 办 ?)
无锁化编程的常用方法 :件 硬件 CPU 同步原语 CAS(Compare a
nd Swap),如无锁栈,无锁队列(ConcurrentLinkedQueue)等等。现在
几乎所有的 CPU 指令都支持 CAS 的原子操作,X86 下对应的是 CMPXCHG 汇
编指令,处理器执行 CMPXCHG 指令是一个原子性操作。有了这个原子操作,
我们就可以用其来实现各种无锁(lock free)的数据结构。
CAS 实现了区别于 sychronized 同步锁的一种乐观锁,当多个线程尝试使
用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线
程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再
次尝试。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改后的新值 B。
当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
其实 CAS 也算是有锁操作,只不过是由 CPU 来触发,比 synchronized 性能
好的多。CAS 的关键点在于,系统 在硬件层面保证了比较并交换操作的原子性,
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操
作。CAS 是非阻塞算法的一种常见实现。
CAS 是非阻塞算法的一种常见实现。
一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作
内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从
主存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该
线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用 CAS
刷新该值的时候,如果发现线程工作内存和主存中不一致了,就会失败,如果
一致,就可以更新成功。
Atomic 包提供了一系列原子类。这些类可以保证多线程环境下,当某个
线程在执行 atomic 的方法时,不会被其他线程打断,而别的线程就像自旋锁一
样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行。
Atomic 类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关
的指令来保证的。
AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对
AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下
的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的
ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID
的现象。Java 并发库中的 AtomicXXX 类均是基于这个原语的实现,拿出
AtomicInteger 来研究在没有锁的情况下是如何做到数据正确性的:
来看看++i 是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在这里采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的
结果进行 CAS 操作,如果成功就返回结果,否则重试直到成功为止。
而 compareAndSet 利用 JNI 来完成 CPU 指令的操作,非阻塞算法。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect,
update);
}
其中,unsafe.compareAndSwapInt()是一个 native 方法,正是调用
CAS 原语完成该操作。
首先假设有一个变量 i,i 的初始值为 0。每个线程都对 i 进行+1 操作。CAS
是这样保证同步的:
假设有两个线程,线程 1 读取内存中的值为 0,current = 0,next = 1,然
后挂起,然后线程 2 对 i 进行操作,将 i 的值变成了 1。线程 2 执行完,回到线
程 1,进入 if 里的 compareAndSet 方法,该方法进行的操作的逻辑是,(1)
如果操作数的值在内存中没有被修改,返回 true,然后 compareAndSet 方法
返回 next 的值(2)如果操作数的值在内存中被修改了,则返回 false,重新
进入下一次循环,重新得到 current 的值为 1,next 的值为 2,然后再比较,
由于这次没有被修改,所以直接返回 2。
那么,为什么自增操作要通过 CAS 来完成呢?仔细观察
incrementAndGet()方法,发现自增操作其实拆成了两步完成的:
int current = get();
int next = current + 1;
由于 volatile 只能保证读取或写入的是最新值,那么可能出现以下情况:
1.A 线程执行 get()操作,获取 current 值(假设为 1)
2.B 线程执行 get()操作,获取 current 值(为 1)
3.B 线程执行 next = current + 1 操作,next = 2
4.A 线程执行 next = current + 1 操作,next = 2
这样的结果明显不是我们想要的,所以,自增操作必须采用 CAS 来完成。
CAS 的优缺点
CAS 由于是在硬件层面保证的原子性,不会锁住当前线程,它的效
率是很高的。
CAS 虽然很高效的实现了原子操作,但是它依然存在三个问题。
1、ABA 问题。CAS 在操作值的时候检查值是否已经变化,没有变化的情况下
才会进行更新。但是如果一个值原来是 A,变成 B,又变成 A,那么 CAS 进行
检查时会认为这个值没有变化,操作成功。ABA 问题的解决方法是使用版本号。
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A
就变成 1A-2B-3A。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类
AtomicStampedReference 来解决 ABA 问题。从 Java1.5 开始 JDK 的
atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这
个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并
且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标
志的值设置为给定的更新值。
CAS 算法实现一个重要前提是需要取出内存中某时刻的数据,而在下一时
刻把取出后的数据和内存中原始数据比较并替换,那么在这个时间差内会导致
数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从
内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数
据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操
作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题
的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
因此前面提到的原子操作
AtomicStampedReference/AtomicMarkableReference 就很有用了。这允
许一对变化的元素进行原子操作。
ABA 问题带来的隐患,各种乐观锁的实现中通常都会用版本
号 version 来对记录或对象标记,避免并发操作带来的问题。在 Java 中,
AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]
的元组来对对象标记版本戳 stamp,从而避免 ABA 问题。
2、循环时间长开销大。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的
执行开销。因此 CAS 不适合竞争十分频繁的场景。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可
以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环
CAS 就无法保证操作的原子性,这个时候就可以用锁。
这里粘贴一个,模拟 CAS 实现的计数器:
public class CASCount implements Runnable {
private SimilatedCAS counter = new SimilatedCAS();
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(this.increment());
}
}
public int increment() {
int oldValue = counter.getValue();
int newValue = oldValue + 1;
while (!counter.compareAndSwap(oldValue, newValue)) { //
如果 CAS 失败,就去拿新值继续执行 CAS
oldValue = counter.getValue();
newValue = oldValue + 1;
}
return newValue;
}
public static void main(String[] args) {
Runnable run = new CASCount();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
}
class SimilatedCAS {
private int value;
public int getValue() {
return value;
}
// 这里只能用 synchronized 了,毕竟无法调用操作系统的 CAS
public synchronized boolean compareAndSwap(int expectedValue,
int newValue) {
if (value == expectedValue) {
value = newValue;
return true;
}
return false;
}
}
CASJAVA一些理解的更多相关文章
- 理解CSS视觉格式化
前面的话 CSS视觉格式化这个词可能比较陌生,但说起盒模型可能就恍然大悟了.实际上,盒模型只是CSS视觉格式化的一部分.视觉格式化分为块级和行内两种处理方式.理解视觉格式化,可以确定得到的效果是应 ...
- 彻底理解AC多模式匹配算法
(本文尤其适合遍览网上的讲解而仍百思不得姐的同学) 一.原理 AC自动机首先将模式组记录为Trie字典树的形式,以节点表示不同状态,边上标以字母表中的字符,表示状态的转移.根节点状态记为0状态,表示起 ...
- 理解加密算法(三)——创建CA机构,签发证书并开始TLS通信
接理解加密算法(一)--加密算法分类.理解加密算法(二)--TLS/SSL 1 不安全的TCP通信 普通的TCP通信数据是明文传输的,所以存在数据泄露和被篡改的风险,我们可以写一段测试代码试验一下. ...
- node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理
一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...
- 如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念
一.前言 DDD(领域驱动设计)的一些介绍网上资料很多,这里就不继续描述了.自己使用领域驱动设计摸滚打爬也有2年多的时间,出于对知识的总结和分享,也是对自我理解的一个公开检验,介于博客园这个平 ...
- 学习AOP之透过Spring的Ioc理解Advisor
花了几天时间来学习Spring,突然明白一个问题,就是看书不能让人理解Spring,一方面要结合使用场景,另一方面要阅读源代码,这种方式理解起来事半功倍.那看书有什么用呢?主要还是扩展视野,毕竟书是别 ...
- ThreadLocal简单理解
在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...
- JS核心系列:理解 new 的运行机制
和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象. 但在 javascript 中,万物皆对象,为什么还要通过 ...
- 深入理解JS 执行细节
javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等 ...
随机推荐
- Python基础(8)迭代器、生成器
一 什么是迭代 1 重复 2 下一次重复是基于上一次的结果 # while True: # cmd=input('>>: ') # print(cmd) # l=['a','b','c', ...
- BZOJ 2063: 我爸是李刚
2063: 我爸是李刚 Time Limit: 10 Sec Memory Limit: 64 MBSubmit: 155 Solved: 82[Submit][Status][Discuss] ...
- python基础代码(猜年龄、从最内层跳出多层循环、简单的购物车程序)
1.猜年龄 , 可以让用户最多猜三次! age = 55 i=0 while i<3: user_guess = int (input ("input your guess:" ...
- 【Linux驱动学习】SD卡规范学习
摘要: 学习SD卡的相关规范,包括定义,硬件特性,数据传输,命令系统等.不涉及代码. 文章针对Linux驱动开发而写,以助于理解SD卡驱动,不会涉及过多硬件内容. 纲要: 1. SD卡介绍 2. SD ...
- django框架实现restful风格的API开发
RESTful风格的要求:https://www.cnblogs.com/chichung/p/9933116.html 利用django原生的框架直接做RESTful的API开发是怎样的呢?感受一下 ...
- elasticsearch SpanNearQuery inOrder参数
一直没有注意还有一个inOrder参数: public SpanNearQuery(SpanQuery[] clauses, int slop, boolean inOrder) When inOrd ...
- java classloader怎么找class?
通过ClassPath http://www.cnblogs.com/xwdreamer/archive/2010/09/08/2297098.html 在eclipse里通过add jar add ...
- Java处理文件BOM头的方式推荐
背景: java普通的文件读取方式对于bom是无法正常识别的. 使用普通的InputStreamReader,如果采用的编码正确,那么可以获得正确的字符,但bom仍然附带在结果中,很容易导致数据处理出 ...
- PHP常用到的功能函数
1.PHP加密解密 PHP加密和解密函数可以用来加密一些有用的字符串存放在数据库里,并且通过可逆解密字符串,该函数使用了base64和MD5加密和解密. 复制代码代码如下: function encr ...
- PHP的文件操作类
<?php class file { function file() { die("Class file can not instantiated!"); } //创建目录 ...