关于非阻塞算法CAS。 比较并交换CAS:CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。然后线程可以基于新返回的V值来做对应的操作,可以反复尝试。通常,反复重试是一种合理的策略,但在一些竞争很激烈的情况下,更好的方式是在重试之前首先等待一段时间或者回退,从而避免造成活锁问题。CAS的主要缺点就是,它将使调用者处理竞争问题,而在锁中能自动处理竞争问题。虽然java语言的锁定语句比较简洁,但JVM和操作在管理锁时需要完成的工作却并不简单。在实现锁定时需要遍历JVM中一条非常复杂的代码路径,并可能导致操作系统级的锁定、线程挂起以及上下文却换等动作。在最好的情况下,在锁定时至少需要一次CAS,因此虽然在使用锁时没有用到CAS,但实际上也无法节约任何执行开销。另外,在程序内部执行CAS不需要执行JVM代码、系统调用或线程调度操作。在应用级上看起来越长的代码路径,如果加上JVM和操作系统中的代码调用,那么事实上却变得更短。

在非阻塞算法中不存在死锁和其他活跃性问题。

而在基于锁的算法中,如果一个线程在休眠或自旋的同时持有一个锁,那么其他线程都无法执行下去,而非阻塞算法不会受到单个线程失败的影响。

锁的劣势

许多JVM都对非竞争锁获取和释放操作进行了极大的优化,但如果有多个线程同时请求锁,那么JVM就需要借助操作系统地功能。如果出现了这种情况,那么一些线程将被挂起并且在稍后恢复运行。当线程恢复执行时,必须等待其他线程执行完它们的时间片以后,才能被调度执行。在挂起和恢复线程等过程中存在着很大的开销,并且通常存在着较大时间的中断。如果在基于锁的类中包含细粒度的操作(例如同步器类,在其大多数方法中只包含了少量操作),那么当在锁上存在着激烈的竞争时,调度开销与工作开销的比值会非常高。

另外,当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。如果被阻塞线程的优先级高,而持有锁的线程优先级低,那么将是一个严重的问题。

比较并交换CAS

CAS包含了3个操作数---需要读写的内存位置V,进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子的方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。

CAS的含义:我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少。CAS是一种乐观的态度,它希望能成功地执行更新操作,并且如果有另一个线程在最近一次检查后更新了该变量,那么CAS能检测到这个错误。

  1. /**
  2. * 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败。
  3. * 然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。
  4. * 由于一个线程在竞争CAS时不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也或者不执行任何操作。
  5. */
  6. public class SimulatedCAS {
  7. private int value;
  8.  
  9. public synchronized int get(){
  10. return value;
  11. }
  12.  
  13. public synchronized int compareAndSwap(int expectedValue,int newValue){
  14. int oldValue = value;
  15. if(oldValue == expectedValue){
  16. value = newValue;
  17. }
  18. return oldValue;
  19. }
  20.  
  21. public synchronized boolean compareAndSet(int expectedValue,int newValue){
  22. return (expectedValue == compareAndSwap(expectedValue,newValue));
  23. }
  24. }

CAS的典型使用模式是:首先从V中读取值A,并根据A计算新值B,然后再通过CAS以原子方式将V中的值由A变成B。由于CAS能检测到来自其他线程的干扰,因此即使不使用锁也能够实现原子的读--改--写操作。

非阻塞的计算器

  1. /**
  2. * 通常,反复重试是一种合理的策略,但在一些竞争很激烈的情况下,更好的方式是在重试之前首先等待一段时间或者回退,从而避免造成活锁问题。
  3. *
  4. * 虽然java语言的锁定语句比较简洁,但JVM和操作在管理锁时需要完成的工作却并不简单。
  5. * 在实现锁定时需要遍历JVM中一条非常复杂的代码路径,并可能导致操作系统级的锁定、线程挂起以及上下文却换等动作。
  6. * 在最好的情况下,在锁定时至少需要一次CAS,因此虽然在使用锁时没有用到CAS,但实际上也无法节约任何执行开销。
  7. * 另外,在程序内部执行CAS不需要执行JVM代码、系统调用或线程调度操作。
  8. * 在应用级上看起来越长的代码路径,如果加上JVM和操作系统中的代码调用,那么事实上却变得更短。
  9. * CAS的主要缺点是,它要求调用者处理竞争问题,而在锁中能自动处理竞争问题
  10. */
  11. public class CasCounter {
  12. private SimulatedCAS value;
  13.  
  14. public int getValue(){
  15. return value.get();
  16. }
  17.  
  18. public int increment(){
  19. int v;
  20. do{
  21. v = value.get();
  22. }while(v != value.compareAndSwap(v, v + 1));
  23. return v + 1;
  24. }
  25. }

JVM对CAS的支持

Java5.0中引入了底层的支持,在int,long和对象引用等类型上都公开了CAS操作,并且JVM把它们编译为底层硬件提供的最有效方法。在原子变量类中,使用了这些底层的JVM支持为数字类型和引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接地使用了这些原子变量类。

  • 示例:非阻塞的栈
  1. /**
  2. * 栈是由Node元素构成的一个链表,根节点为栈顶yop,每个元素中都包含了一个值以及指向下一个元素的链接。
  3. * push方法创建一个新的节点,该节点的next域指向当前的栈顶,然后使用CAS把这个新节点放入栈顶。
  4. *
  5. * @param <E>
  6. */
  7. public class ConcurrentStack<E> {
  8.  
  9. AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
  10.  
  11. public void push(E item){
  12. Node<E> newHead = new Node<E>(item);
  13. Node<E> oldHead;
  14. do{
  15. oldHead = top.get();
  16. newHead.next = oldHead;
  17. }while(!top.compareAndSet(oldHead, newHead));
  18. }
  19.  
  20. public E pop(){
  21. Node<E> newHead;
  22. Node<E> oldHead;
  23. do{
  24. oldHead = top.get();
  25. if(oldHead == null){
  26. return null;
  27. }
  28. newHead = oldHead.next;
  29. }while(!top.compareAndSet(oldHead, newHead));
  30. return oldHead.item;
  31. }
  32.  
  33. private static class Node<E>{
  34. public final E item;
  35. public Node<E> next;
  36.  
  37. public Node(E item){
  38. this.item = item;
  39. }
  40. }
  41. }
  • 示例:非阻塞链表

链表队列比栈复杂,它必须支持对头节点和尾节点的快速访问。它需要单独维护头指针和尾指针。

对于尾部的插入,有两个点需要更新:将当前尾节点的next指向要插入的节点,和将尾节点更新为新插入的节点。这两个更新操作需要不同的CAS操作,不好通过原子变量来实现。

需要使用一些策略:

策略一是,即使在一个包含多个步骤的更新操作中,也要确保数据结构总是处于抑制的状态。这样,线程B到达时,如果发现A正在执行更新,那么线程B就可以知道有一个操作已部分完成,并且不能立即执行自己的更新操作。然后B可以等待并直到A完成更新。虽然能使不同的线程轮流访问数据结构,并且不会造成破坏,但如果有一个线程在更新操作中失败了,那么其他的线程都无法再方位队列。

策略二是,如果B到达时发现A正在修改数据结构,那么在数据结构中应该有足够多的信息,使得B能完成A的更新操作。如果B帮助A完成了更新操作,那么B可以执行自己的操作,而不用等待A的操作完成。当A恢复后再试图完成其操作时,会发现B已经替它完成了。

  1. /**
  2. * 实现的关键点在于:
  3. * 当队列处于稳定状态时,未节点的next域将为空,如果队列处于中间状态,那么tail.next将为非空。
  4. * 因此,任何线程都能够通过检查tail.next来获取队列当前的状态。而且,当队列处于中间状态时,可以通过将尾节点向前移动一个节点,
  5. * 从而结束其他线程正在执行的插入元素操作,并使得队列恢复为稳定状态。
  6. *
  7. * @param <E>
  8. */
  9. public class LinkedQueue<E> {
  10.  
  11. private static class Node<E>{
  12. final E item;
  13. final AtomicReference<Node<E>> next;
  14.  
  15. public Node(E item,Node<E> next){
  16. this.item = item;
  17. this.next = new AtomicReference<Node<E>>(next);
  18. }
  19.  
  20. private final Node<E> dummy = new Node<E>(null,null);
  21. private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);
  22. private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);
  23.  
  24. private boolean put(E item){
  25. Node<E> newNode = new Node<E>(item,null);
  26. while(true){
  27. Node<E> curTail = tail.get();
  28. Node<E> tailNext = curTail.next.get();
  29. if(curTail == tail.get()){
  30. if(tailNext != null){
  31. //队列处于中间状态,推进尾节点
  32. tail.compareAndSet(curTail, tailNext);
  33. }else{
  34. //处于稳定状态。尝试插入新节点
  35. if(curTail.next.compareAndSet(null, newNode)){
  36. //插入成功,尝试推进尾节点,这一步如果未来得及完成,可由别的线程帮忙
  37. tail.compareAndSet(curTail, newNode);
  38. return true;
  39. }
  40. }
  41. }
  42. }
  43. }
  44. }
  45. }

ABA问题

在某些算法中,如果V的值首先由A变成B,再由B变成A。

解决办法是:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。AtomicStampedReference以及AtomicMarkableReference支持在两个变量上执行原子的条件更新。

#笔记内容参考 《 java并发编程实战》

Java并发编程实战.笔记十一(非阻塞同步机制)的更多相关文章

  1. 【Java并发编程】9、非阻塞同步算法与CAS(Compare and Swap)无锁算法

    转自:http://www.cnblogs.com/Mainz/p/3546347.html?utm_source=tuicool&utm_medium=referral 锁(lock)的代价 ...

  2. 多线程-java并发编程实战笔记

    线程安全性 编写线程安全的代码实质上就是管理对状态的访问,而且通常都是共享的,可变的状态. 一个对象的状态就是他的数据,存储在状态变量中,比如实例域或静态域.所谓共享是指一个对象可以被多个线程访问:所 ...

  3. Java并发编程实战笔记—— 并发编程1

    1.如何创建并运行java线程 创建一个线程可以继承java的Thread类,或者实现Runnabe接口. public class thread { static class MyThread1 e ...

  4. Java并发编程实战笔记

    如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误.有三种方式可以修复这个问题: i.不在线程之间共享该状态变量 ii.将状态变量修改为不可变的变量 iii.在访问状态变 ...

  5. 《Java并发编程实战》第五章 同步容器类 读书笔记

    一.同步容器类 1. 同步容器类的问题 线程容器类都是线程安全的.可是当在其上进行符合操作则须要而外加锁保护其安全性. 常见符合操作包括: . 迭代 . 跳转(依据指定顺序找到当前元素的下一个元素) ...

  6. JAVA并发编程实战笔记 第二章

    2.1 线程安全性 当多个线程访问某个类时,不论这些线程如何交替执行,这个类始终都能表现出正确的行为,且主调代码中不需要任何额外的同步或协同,则称这个类是线程安全的. 类不变性条件(Invariant ...

  7. java并发编程实战《六》等待-通知机制

    用"等待-通知"机制优化循环等待 前言 在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待. 1 // 一次性申请转出账户和 ...

  8. java并发编程实战笔记---(第五章)基础构建模块

    . 5.1同步容器类 1.同步容器类的问题 复合操作,加容器内置锁 2.迭代器与concurrentModificationException 迭代容器用iterator, 迭代过程中,如果有其他线程 ...

  9. java并发编程实战笔记---(第三章)对象的共享

    3.1 可见性 synchronized 不仅实现了原子性操作或者确定了临界区,而且确保内存可见性. *****必须在同步中才能保证:当一个线程修改了对象状态之后,另一个线程可以看到发生的状态变化. ...

随机推荐

  1. 高级查询语句____ Mysql

    MySQL高级查询 高级查询 关键字书写顺序  关键字执行顺序select:投影结果       1    5 from:定位到表             2    1 where:分组前第一道过滤  ...

  2. CF803D 题解

    题面 正解:一道二分大水题! A:为什么我得不到满分? B : 评测的系统不一样啊! A : 蛤? 正常情况下我们日常练习均使用的是windows系统,在windows下,string 本身是可以存储 ...

  3. codeforces 340 A. The Wall

    水水的一道题,只需要找xy的最小公倍数,然后找a b区间有多少个可以被xy的最小公倍数整除的数,就是答案. //============================================ ...

  4. 初识Apache NiFi

    一. NiFi介绍 Apache NiFi支持功能强大且可扩展的数据路由,转换和系统中介逻辑的有向图. Apache NiFi的一些高级功能和目标包括: 基于Web的用户界面 设计,控制,反馈和监控之 ...

  5. sql server 2008 外键的级联操作

    问题提出:现在我有三张表,学生Student,课程Course,成绩SC 1.  学生表Student,主键是学号Sno 2.  课程Course,主码是课程号Cno 3.  成绩SC,主码是Sno和 ...

  6. Java----面向对象(继承&多态)

    一.继承 什么是继承 ? 让类与类之间产生了子父类关系 ; 继承的好处是: 提高代码的复用性和维护性 java中继承的特点是: 只支持单继承.不支持多继承,但是可以多层继承; 四种权限修饰符是 : p ...

  7. Okhttp3 网络请求框架与 Gson

    Maven环境 : <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>o ...

  8. 同时运行多个 tomcat 修改端口

    修改 tomcat 配置文件,路径: tomcat_home/conf/server.xml  1.HTTP端口,默认8080,如下改为8081 <Connector connectionTim ...

  9. 1、Java小白之路前言

    大二一年准备好好学习Java,养成一个良好的习惯写博客,但是由于各种各样的原因,并没有坚持下来.而正好又赶上大三结束,去实习,发现自己的基础还是有些薄弱,所以决定,重新走上这条Java小白之路. 时隔 ...

  10. Go中的interface学习

    学过Java的同学都知道在Java中接口更像是一种规范,用接口定义了一组方法,下面实现这个接口的类只管按照写好的方法名和返回值去实现就好,内部如何实现是各个方法自己的事情,接口本身不关注. 另外Jav ...