非阻塞算法:使用底层的原子机器指令(例如比较并交换指令)代替锁来确保数据在并发访问中的一致性

  • 应用于在操作系统和JVM中实现线程 / 进程调度机制、垃圾回收机制以及锁和其他并发数据结构
  • 可伸缩性和活跃性上拥有巨大的优势,不存在死锁

原子变量:提供了与volatile类型变量相同的内存语义,并支持原子的更新操作,比基于锁的方法提供更高的可伸缩性

一、锁的劣势

锁:独占方式访问共享变量,对变量的操作对其他获得同一个锁的线程可见

劣势:

  • 请求锁失败,一些线程将被挂起并且在稍后恢复运行
  • 恢复执行时必须等其他的线程的执行时间片用完
  • 挂起和恢复的开销很大
  • 锁竞争存在开销
  • 优先级反转:被阻塞线程的优先级较高,而持有锁的线程优先级较低

Volatile更轻量级,保证可见性,不会发生山下文切换和线程调度,但没有办法完成原子操作

二、硬件对并发的支持

乐观的解决方法:借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,并且可以重试(也可以不重试)

针对多处理器操作而设计的处理器中提供了一些特殊指令:支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-Increment)以及交换(Swap)等指令

1、比较并交换CAS——乐观技术

CAS的含义是:“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”

 1 public class SimulatedCAS {//模拟CAS操作
2 @GuardedBy("this") private int value;
3
4 public synchronized int get() { return value; }
5
6 public synchronized int compareAndSwap(int expectedValue,
7 int newValue) {//相当于处理器的CAS原子化操作
8 int oldValue = value;
9 if (oldValue == expectedValue)
10 value = newValue;
11 return oldValue;
12 }
13
14 public synchronized boolean compareAndSet(int expectedValue,
15 int newValue) {
16 return (expectedValue
17 == compareAndSwap(expectedValue, newValue));
18 }
19 }

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都将失败(而不是挂起

竞争中失败,并且可以再次尝试,也可以放弃

由于CAS能检测到来自其他线程的干扰(干扰结果为失败),因此即使不使用锁也能够实现原子的读-改-写操作序列

2、非阻塞的计数器

  • 1 @ThreadSafe
  • 2 public class CasCounter {
  • 3 private SimulatedCAS value;
  • 4
  • 5    public int getValue() {
  • 6       return value.get();
  • 7     }
  • 8
  • 9     public int increment() {
  • 10       int v;
  • 11       do {
  • 12          v = value.get();
  • 13       } while (v != value.compareAndSwap(v, v + 1));
  • 14
  • 15    return v + 1;
  • 16    }
  • 17 }
  • CasCount不会发生阻塞,如果多个线程同时更新计数器,那么可能产生失败,并执行多次重试操作
  • 为避免活锁,每次失败时,可以等一段时间再执行
  • 当竞争程度不高时,CasCount性能远高于基于锁的计数器

3、JVM对CAS的支持

  在原子变量类(例如java.util.concurrent.atomic中的AutomicXxx)中使用了这些底层的JVM支持为数字类型和引用类型提供了一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时则直接或间接地使用了这些原子变量类

三、原子变量类

  原子变量比锁的粒度更细,量级更轻,直接利用硬件对并发的支持,性能比锁更好。原子变量相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作

  共有12个原子变量类,可分为4组:标量类(Scalar)、更新器类、数组类以及复合变量类

    • 标量类(AtomicInteger、AutomicLong、AutomicBoolean以及AtomicReference):模拟其他基本类型的原子变量,可以将short或byte等类型与int类型进行转换,以及使用floatToIntBits或doubleToLongBits来转换浮点数
    • 原子数组类(AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray<E>):volatile类型的数组仅在数组引用上具有volatile语义,而在其元素则没有

  基本类型的包装类是不可修改的,而原子变量类是可修改的

1、原子变量是一种“更好的volatile”

 通过CAS来维持包含多个变量的不变性条件
public class CasNumberRange {
private static class IntPair{
// 不变性条件: lower <= upper
final int lower;
final int upper; public IntPair(int lower, int upper) {
this.lower = lower;
this.upper = upper;
}
} private AtomicReference<IntPair> values = new AtomicReference<>(); public int getLower(){
return values.get().lower;
} public int getUpper(){
return values.get().upper;
} public void setLower(int i){
while (true){
IntPair oldv = values.get();
if (i > oldv.upper){
throw new IllegalArgumentException("lower can't > upper");
}
IntPair newv = new IntPair(i, oldv.upper);
if (values.compareAndSet(oldv, newv)){
return;
}
}
}
}

实现对多变量的非阻塞的先检查在执行操作

2、性能比较:锁与原子变量

 基于锁的随机数生成器
public class ReentrantLockPseudoRandom extends PseudoRandom {
private final Lock lock = new ReentrantLock(false);
private int seed; ReentrantLockPseudoRandom(int seed) {
this.seed = seed;
} public int nextInt(int n) {
lock.lock();
try {
int s = seed;
seed = calculateNext(s);
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
} finally {
lock.unlock();
}
}
}
 基于AtomicInteger的随机数生成器
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed; AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
} public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
}

如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈。如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低

在高度竞争的情况下,锁的性能将超过原子变量的性能(失败重试导致更激烈的竞争),但在更真实的情况下,原子变量的性能将超过锁的性能

四、非阻塞算法——一个线程的失败或挂起不会导致其他线程也失败或挂起

无锁算法——算法的每个步骤中都存在某个线程能够执行下去

在非阻塞算法中通常不会出现死锁和优先级反转问题(但可能会出现饥饿和活锁问题,因为在算法中会反复地重试)

  创建非阻塞算法的关键在于,找出如何将原子修改的范围缩小到单个变量上,同时还要维护数据的一致性

  非阻塞算法的特性:某项工作的完成具有不确定性,必须重新执行

  非阻塞算法中能确保线程安全性,因为compareAndSet像锁定机制一样,既能提供原子性,又能提供可见性

1、非阻塞的栈——Treiber算法

 1 public class ConcurrentStack<E> {//非阻塞栈
2 //栈顶元素,永远指向栈顶,入栈与出栈都只能从栈顶开始
3 AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();
4
5 //非阻塞的入栈操作
6 public void push(E item) {
7 //创建新的元素
8 Node<E> newHead = new Node<E>(item);
9 Node<E> oldHead;
10 do {
11 //当前栈顶元素,也即这次操作的基准点,操作期间不能改变
12 oldHead = top.get();
13 newHead.next = oldHead;//让新元素成为栈顶
14 //如果基准点被其他线程修改后就会失败,失败后再重试
15 } while (!top.compareAndSet(oldHead, newHead));
16 }
17
18 //非阻塞的出栈操作
19 public E pop() {
20 Node<E> oldHead;
21 Node<E> newHead;
22 do {
23 oldHead = top.get();//取栈顶元素,即基准点
24 if (oldHead == null)
25 return null;
26 newHead = oldHead.next;
27 //如果基准点没有变化,则成功
28 } while (!top.compareAndSet(oldHead, newHead));
29 return oldHead.item;//返回栈顶元素值
30 }
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 }

2、非阻塞的链表

  一个链表队列比栈更加复杂,因为它需要支持首尾(从尾插入,从首取出)的快速访问,为了实现,它会维护独立的队首指针和队尾指针。

  有两个指针指向位于尾部的节点:当前最后一个元素的next指针,以及尾节点。当成功地插入一个新元素时,这两个指针都需要采用原子操作来更新。  

 Michael-Scott算法

 public class LinkedQueue<E> {
private static class Node<E> {
final E item;
final AtomicReference<Node<E>> next; public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
} //哑元,用于区分队首与队尾,特别是在循环队列中
private final Node<E> dummy = new Node<E>(null, null);
private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(
dummy);//头指针,出队时用
private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(
dummy);//尾指针,入队时用 public boolean put(E item) {//入队
Node<E> newNode = new Node<E>(item, null);
while (true) {//在除尾插入新的元素直到成功
//当前队尾元素
Node<E> curTail = tail.get();
/*
* 当前队尾元素的next域,一般为null,但有可能不为null,
* 因为有可能其他线程已经上一语句与下一语句间添加了新
* 的元素,即此时队列处于中间状态
*/
Node<E> tailNext = curTail.next.get();
/*
* 再一次检查上面两行语句的操作还是否有效,因为很有可在此刻尾指针已经
* 向后移动了(比如其他线程已经执行了B 或 D 处语句),所以下面的操作都
* 是要基于尾节点是curTail才可以。(想了一下,其实这里不需要这个判断
* 也是可以的,因为下面执行到 B 或 C 时自然会失败,这样做只是为了提高
* 成功的效率)
*/
if (curTail == tail.get()) { if (tailNext != null) {// A
/*
* 队列处于中间状态,尝试调整队尾指针,这里
* 需要使用compareAndSet原子操作来进行,因为
* 有可以在进行时 D 处已经调整完成
*/
tail.compareAndSet(curTail, tailNext);// B
} else {
// 队列处于稳定状态,尝试在队尾插入新的节点
if (curTail.next.compareAndSet(null, newNode)) {// C
/*
* 插入尝试成功,再开始尝试调整队尾指针,这里完全
* 有可能不需要再调整了,因为上面 B 行已经帮这里调
* 整过了
*/
tail.compareAndSet(curTail, newNode);// D
return true;
}
}
}
}
}
}

原理:对队列处于稳定状态时,尾节点的next域将为空,如果队列处于中间状态,那么tail.next将为非空。

LinkedQueue.put方法在插入新元素之前,将首先检查队列是否处于中间状态(步骤A),如果是,那么有另一个线程正在插入元素(在步骤C和D之间)。此时线程不会等待其他线程执行完成,而是帮助它完成操作,并将尾节点向前推进一个节点(步骤B)。然后,它将重复执行这种检查,以免另一个线程已经开始插入新元素,并继续推进尾节点,直到它发现队列处于稳定状态后,才会开始执行自己的插入操作。

由于步骤C中的CAS将把新节点链接到队列尾部,因此如果两个线程同时插入元素,那么这个CAS将失败。在这样的情况下,并不会造成破坏:不会发生任何变化,并且当前的线程只需重新读取尾节点并再次重试。如果步骤C成功了,那么插入操作将生效,第二个CAS(步骤D)被认为是一个“清理操作”因为它既可以由执行插入操作的线程来执行,也可以由其他任何线程来执行。如果步骤D失败,那么执行插入操作的线程将返回,而不是重新执行CAS,因为不再需要重试——另一个线程已经在步骤B中完成了这个工作。这种方式能够工作,因为在任何线程尝试将一个新节点插入到队列之前,都会首先通过检查tail.next是否非空来判断是否需要清理队列。如果是,它首先会推进为尾节点(可能需要执行多次),知道队列处于稳定状态。

3、原子的域更新器

  原子的域更新器类表示现有volatile域的一种基于反射的“视图”,从而能够在已有的volatile域上使用CAS

 private static AtomicReferenceFieldUpdater<Node, Node> nextUpdater  //用来对next字段进行更新
= AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");

4、ABA问题

在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然应该被认为是发生了变化,并需要重新执行算法中的某些步骤

解决方案:不只更新某个引用的值,而是更新两个值,包括一个引用和一个版本号

java并发编程实战:第十五章----原子变量与非阻塞机制的更多相关文章

  1. 《java并发编程实战》读书笔记12--原子变量,非阻塞算法,CAS

    第15章 原子变量与非阻塞同步机制 近年来,在并发算法领域的大多数研究都侧重于非阻塞算法,这种算法用底层的原子机器指令(例如比较并交换指令)代替锁老确保数据在并发访问中的一致性. 15.1 锁的劣势 ...

  2. 《Java并发编程实战》第十五章 原子变量与非堵塞同步机制 读书笔记

    一.锁的劣势 锁定后假设未释放.再次请求锁时会造成堵塞.多线程调度通常遇到堵塞会进行上下文切换,造成很多其它的开销. 在挂起与恢复线程等过程中存在着非常大的开销,而且通常存在着较长时间的中断. 锁可能 ...

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

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

  4. java并发编程实战:第五章----基础构建模块

    委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可. 一.同步容器类 1.同步容器类的问题 同步容器类都是线程安全的,容器本身内置的复合操作能够保证原子性,但是当在其上进 ...

  5. 【java并发编程实战】第五章:基础构建模块

    1.同步容器类 它们是线程安全的 1.1 vector和hashtable. 和Collections.synchronizeXxx()一样.实现方式就是在每个方法里面加入synchronize代码块 ...

  6. 《Java并发编程实战》笔记-锁与原子变量性能比较

    如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈.如果线程本地的计算量较多,那么在锁和原子变量上的竞争会降低,因为在线程中访问锁和原子变量的频率将降低. 在高度竞争的情况下,锁的性能将超 ...

  7. 《Java并发编程实战》第三章 对象的共享 读书笔记

    一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见 ...

  8. 《Java并发编程实战》第十一章 性能与可伸缩性 读书笔记

    造成开销的操作包含: 1. 线程之间的协调(比如:锁.触发信号以及内存同步等) 2. 添加�的上下文切换 3. 线程的创建和销毁 4. 线程的调度 一.对性能的思考 1 性能与可伸缩性 执行速度涉及下 ...

  9. 《Java并发编程实战》第六章 任务运行 读书笔记

    一. 在线程中运行任务 无限制创建线程的不足 .线程生命周期的开销很高 .资源消耗 .稳定性 二.Executor框架 Executor基于生产者-消费者模式.提交任务的操作相当于生产者.运行任务的线 ...

随机推荐

  1. 【解决Jira】Chrome提示Java插件因过期而遭到阻止(JIRA上传截屏截图)

    最近经常被这个问题所困扰:用Chrome访问JIRA上传截屏截图时,地址栏下面弹出通知,提示JAVA插件已过期.但是由于公司要求统一开发环境和设置,不能更新到最新版,就像这样: 结果网页上的Java就 ...

  2. unity代码添加动画,并传参数

    测试界面 button一个 sprite一个 测试代码 public class BgObject : MonoBehaviour { void Start() { List<string> ...

  3. POJ 2395 Out of Hay(求最小生成树的最长边+kruskal)

    Out of Hay Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 18472   Accepted: 7318 Descr ...

  4. Hive任务优化--控制hive任务中的map数和reduce数

    一.    控制hive任务中的map数: 1.    通常情况下,作业会通过input的目录产生一个或者多个map任务.主要的决定因素有: input的文件总个数,input的文件大小,集群设置的文 ...

  5. 031:Cetus sharding

    目录 一.主机环境 二.搭建环境 1.准备环境 三.Cetus安装 1.下载包和安装依赖包 2.Cetus安装 1.安装说明 2.安装实施 四.Cetus配置 1.创建和修改配置文件 2.启动cetu ...

  6. 深入浅出HTTPS工作原理(转载)

    转载自: https://blog.csdn.net/wangtaomtk/article/details/80917081 深入浅出HTTPS工作原理 HTTP协议由于是明文传送,所以存在三大风险: ...

  7. 第十九课 golang中的下划线

    在 Golang 里, _ (下划线)是个特殊的标识符. 用在 import 在导包的时候,常见这个用法: 1 2 import _ "net/http/pprof" import ...

  8. Linux安装imagick扩展出现错误:configure: error: not found. Please provide a path to MagickWand-config or Wand-config program.

    在Linux(CentOS)上安装imagick扩展时,遇到如下错误: checking ImageMagick MagickWand API configuration program... che ...

  9. 496. Next Greater Element I + 503. Next Greater Element II + 556. Next Greater Element III

    ▶ 给定一个数组与它的一个子列,对于数组中的一个元素,定义它右边第一个比他大的元素称为他的后继,求所给子列的后继构成的数组 ▶ 第 496 题,规定数组最后一个元素即数组最大元素的后继均为 -1 ● ...

  10. C#串口编程测试收发

    原文:http://www.cnblogs.com/vsdot/archive/2013/04/23/3263348.html   基本传递方法:RS232传输要有1位起始位,8位数据位.1位校验位( ...