如果不用锁机制如何实现共享数据访问。(不要用锁,不要 用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一些理解的更多相关文章

  1. 理解CSS视觉格式化

    前面的话   CSS视觉格式化这个词可能比较陌生,但说起盒模型可能就恍然大悟了.实际上,盒模型只是CSS视觉格式化的一部分.视觉格式化分为块级和行内两种处理方式.理解视觉格式化,可以确定得到的效果是应 ...

  2. 彻底理解AC多模式匹配算法

    (本文尤其适合遍览网上的讲解而仍百思不得姐的同学) 一.原理 AC自动机首先将模式组记录为Trie字典树的形式,以节点表示不同状态,边上标以字母表中的字符,表示状态的转移.根节点状态记为0状态,表示起 ...

  3. 理解加密算法(三)——创建CA机构,签发证书并开始TLS通信

    接理解加密算法(一)--加密算法分类.理解加密算法(二)--TLS/SSL 1 不安全的TCP通信 普通的TCP通信数据是明文传输的,所以存在数据泄露和被篡改的风险,我们可以写一段测试代码试验一下. ...

  4. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  5. 如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念

    一.前言     DDD(领域驱动设计)的一些介绍网上资料很多,这里就不继续描述了.自己使用领域驱动设计摸滚打爬也有2年多的时间,出于对知识的总结和分享,也是对自我理解的一个公开检验,介于博客园这个平 ...

  6. 学习AOP之透过Spring的Ioc理解Advisor

    花了几天时间来学习Spring,突然明白一个问题,就是看书不能让人理解Spring,一方面要结合使用场景,另一方面要阅读源代码,这种方式理解起来事半功倍.那看书有什么用呢?主要还是扩展视野,毕竟书是别 ...

  7. ThreadLocal简单理解

    在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...

  8. JS核心系列:理解 new 的运行机制

    和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象. 但在 javascript 中,万物皆对象,为什么还要通过 ...

  9. 深入理解JS 执行细节

    javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等 ...

随机推荐

  1. 通过 CLI 搭建 ghost

    参考: ghost 官网 系统架构说明 架构 架构说明 本实践将 web 接入, nodejs 服务, 数据库分离, 适合生产环境场景. nginx 接入请求, 反向代理后端 nodejs 服务 no ...

  2. 利用ES6的Promise.all实现至少请求多长时间

    1.背景 我们都知道ajax请求可以加个timeout,就是最多请求多少时间,如果超过这个时间直接就报错. 这个是最多请求多长时间,我现在要做的是,最少要请求多长时间,然后才能执行后续的逻辑. 比如, ...

  3. 关于vscode的个人配置

      vs code官方下载地址 : https://code.visualstudio.com/Download   下载好的vs code相当是一款纯文本编辑器,接下来开始进行对其配置:   页面设 ...

  4. Entity Framework 6.1.2 offset row fetch next 错误解决办法

    本地测试环境用的SqlServer2012,生产环境2008R2.然后在查询分页数据时生产环境悲剧的报错了. 原因是EF6.1.2以上版本在编译SQL时使用了新的语法对低版本的SqlServer不兼容 ...

  5. codevs 1018 [noip 2000 提高] 单词接龙

    题目链接:http://codevs.cn/problem/1018/ 题目描述 Description 单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母, ...

  6. 4.flume实战(一)

    需求:从指定网络端口采集数据输出到控制台 使用flume的关键就是写配置文件 a)配置source b)配置channel c)配置sink d)把以上三个组件串起来 我们看一下官网给的配置文件 # ...

  7. spring FieldRetrievingFactoryBean

    Spring : 基于XML Schema的配置(一): http://www.tuicool.com/articles/mMjY3uI http://www.cnblogs.com/jifeng/a ...

  8. win32 读取文本文件,并进行字符串分割和存储

    //分割字符      char *p;//p存放临时行指针 q存放临时每一行的列指针      char *hang[100]={0};//存储第一行      char *lie[300]={0} ...

  9. docker从零开始 存储(一)存储概述

    管理Docker中的数据 默认情况下,在容器内创建的所有文件都存储在可写容器层中.这意味着: 当该容器不再运行时,数据不会持久存在,如果另一个进程需要,则可能很难从容器中获取数据. 容器的可写层紧密耦 ...

  10. 网关协议:CGI、FastCGI、WSGI

    CGI就像是一座桥,把网页和WEB服务器中的执行程序连接起来,它把HTML接收的指令传递给服务器的执行程序,再把服务器执行程序的结果返还给HTML页. CGI CGI即通用网关接口(Common Ga ...