读书笔记:Java并发实战第15章 原子变量与非阻塞同步机制
2016-01-12 14:13
861 查看
原子变量和非阻塞的同步机制在并发的环境中比synchronized机制提供更高的性能和可伸缩性
非阻塞同步算法用底层的原子机器指令代替锁来确保数据在并发访问中的一致性,非阻塞算法不存在死锁的问题
Java提供了原子变量类:Atomic***可以用来来构建高效的非阻塞算法,或者当做一种更好的volatile变量来使用,因为原子变量和volatile变量具有相同的内存语义,同时还提供了很多原子操作
锁的局限
1)竞争锁带来的调度开销
非竞争锁:在任一时刻,只有一个线程尝试获取锁,JVM可以对非竞争锁进行优化以提高性能
竞争锁:在任一时刻,有多个线程同时请求锁,JVM无法对其优化,只能借助操作系统来处理
竞争锁会造成线程的挂起和恢复,这些调度动作存在很大的性能开销,例如在同步容器中,大部分的方法只包含了少量的操作,这样,锁的竞争就会很激烈,调度的开销会占很大的比例,锁对于细粒度的操作来说,是一种高开销的机制
2)锁等待
锁等待的过程中,线程不能做任何其他事情,如果持有锁的线程一直不释放锁,那么其他线程将无法执行下去
volatile的局限
volatile是一种轻量级的同步机制,使用volatile变量不会发生上下文切换和线程调度,但volatile变量的缺点在于不能提供原子性的复合操作
CAS操作
CAS是底层处理器在并发环境下提供的原子操作,是一种乐观锁,中文是:比较并交换
CAS包含3个操作数:读写的变量V,期望值A,更新值B
当V的值等于A时,CAS才会用原子操作将V赋值为B,最后返回变量V执行本次CAS操作之前的值
CAS的写入和volatile变量的写入有相同的内存语义
CAS相对锁的好处是:当线程竞争锁失败时,它不会被挂起或阻塞,可以根据业务逻辑来决定是否重试,有些情况下,当CAS失败时不执行任何操作是一种明智的做法,说明其他线程已经完成了你想要执行的操作。如果使用自旋的方式重试的话,在高竞争的条件下仍需要防止活锁的发生
锁和CAS的性能比较
锁保证了只有一个线程能够使用共享资源,而CAS不能保证这一点,要通过自旋的方式来不断的尝试原子操作,直到成功
在竞争不高的情况下,CAS操作失败的概率非常小,而获取无竞争的锁,至少需要一次CAS操作和其他锁相关的操作,因此,在中度的锁竞争情况下,CAS的性能是远远高于锁的
在高度竞争的条件下,锁的性能有可能稍微超过CAS,因为锁至少保证有一个线程能执行操作,而CAS可能使得大部分线程都在不断的重试,从而导致更激烈的竞争
总体而言,CAS在可伸缩性上要高于锁,在应对常见的竞争程度上,CAS的效率更高
原子变量的使用案例:
因为setMax和setMin中存在"先判断后执行",因此多线程的情况下,要保证max > min这个不变性约束,需要在两个方法中使用锁,或者像上面的例子,使用原子变量
非阻塞算法
首先看什么是阻塞算法:线程在请求锁时,如果锁被占用,那么该线程会被挂起,这就是阻塞算法
非阻塞算法:线程在请求锁时,无论是否请求成功,都会立即返回,这就是非阻塞算法
因为非阻塞算法需要自行实现失败处理,因此要比基于锁的算法复杂得多,但是能够提供更高的可伸缩性
实现多个原子变量的安全更新
原子变量将执行原子修改的范围缩小到单个变量上以提高效率,如果多个变量需要原子更新,那该如何做?
如果在一个原子更新操作中涉及到多个CAS操作,在并发的条件下要保持一致的状态,难点是:如何保持多个CAS的原子性的同时保证非阻塞
一般采用的方法是:
当线程B试图进行原子更新时,发现线程A正在进行更新,那么线程B会帮助线程A完成一部分更新操作,然后执行自己的更新操作,当线程A完成一部分更新操作,希望完成剩下的更新时,会发现线程B已经替他完成了
例如在并发队列中,当添加一个新的节点要做两个CAS操作:1、把新节点加入到队尾,即原队尾节点的next指向新节点 2、更新尾节点tail指针,指向新节点
下面是并发队列的实现的主要代码
tail指向当前队列的尾节点指针,tailNext是尾节点的next指针,newNode是新增节点
下面分析各种竞争条件
1、单线程执行, 这是没有竞争的情况,步骤A判断为false,因为尾节点的next指针必定指向null,因此执行else块的代码:把新节点添加到队尾,然后更新tail指针,最后返回true
2、线程A执行完步骤C时,线程B开始执行步骤A,会发现步骤A判断为true,因为尾节点的next指针已经被线程A修改为新节点了,然后线程B会执行步骤B(和步骤D是一样的东西),然后线程A在执行步骤D时会失败(线程B帮它完成了),线程A就返回true,但此时线程B还没完成,继续执行步骤A,按照上面的逻辑一直到返回true
这样就保证了更新的完整性
原子的域更新器
在并发链表ConcurrentLinkedQueue中使用的也是这样的算法,但在CAS操作上并不是使用原子变量,而是使用更加高效的原子的域更新器
在Node类中的next属性并不是原子引用,而是普通的volatile,通过基于反射的AtomicReferenceFieldUpdater可以进行volatile域的原子更新,这样可以避免每个Node的AtomicReference的创建过程,AtomicReferenceFieldUpdater的使用如下:
说明几点:
1、如果Node不是Queue的内部类,那么volatile域不能为private
2、volatile域不能使用static或final修饰
3、newUpdater方法接受的三个参数分别是:对象类型,字段类型、字段名称
ABA问题
这是一个需要详细讨论的问题:参考下一篇博客:ABA问题及避免:http://blog.csdn.net/li954644351/article/details/50511879
非阻塞同步算法用底层的原子机器指令代替锁来确保数据在并发访问中的一致性,非阻塞算法不存在死锁的问题
Java提供了原子变量类:Atomic***可以用来来构建高效的非阻塞算法,或者当做一种更好的volatile变量来使用,因为原子变量和volatile变量具有相同的内存语义,同时还提供了很多原子操作
锁的局限
1)竞争锁带来的调度开销
非竞争锁:在任一时刻,只有一个线程尝试获取锁,JVM可以对非竞争锁进行优化以提高性能
竞争锁:在任一时刻,有多个线程同时请求锁,JVM无法对其优化,只能借助操作系统来处理
竞争锁会造成线程的挂起和恢复,这些调度动作存在很大的性能开销,例如在同步容器中,大部分的方法只包含了少量的操作,这样,锁的竞争就会很激烈,调度的开销会占很大的比例,锁对于细粒度的操作来说,是一种高开销的机制
2)锁等待
锁等待的过程中,线程不能做任何其他事情,如果持有锁的线程一直不释放锁,那么其他线程将无法执行下去
volatile的局限
volatile是一种轻量级的同步机制,使用volatile变量不会发生上下文切换和线程调度,但volatile变量的缺点在于不能提供原子性的复合操作
CAS操作
CAS是底层处理器在并发环境下提供的原子操作,是一种乐观锁,中文是:比较并交换
CAS包含3个操作数:读写的变量V,期望值A,更新值B
当V的值等于A时,CAS才会用原子操作将V赋值为B,最后返回变量V执行本次CAS操作之前的值
CAS的写入和volatile变量的写入有相同的内存语义
CAS相对锁的好处是:当线程竞争锁失败时,它不会被挂起或阻塞,可以根据业务逻辑来决定是否重试,有些情况下,当CAS失败时不执行任何操作是一种明智的做法,说明其他线程已经完成了你想要执行的操作。如果使用自旋的方式重试的话,在高竞争的条件下仍需要防止活锁的发生
锁和CAS的性能比较
锁保证了只有一个线程能够使用共享资源,而CAS不能保证这一点,要通过自旋的方式来不断的尝试原子操作,直到成功
在竞争不高的情况下,CAS操作失败的概率非常小,而获取无竞争的锁,至少需要一次CAS操作和其他锁相关的操作,因此,在中度的锁竞争情况下,CAS的性能是远远高于锁的
在高度竞争的条件下,锁的性能有可能稍微超过CAS,因为锁至少保证有一个线程能执行操作,而CAS可能使得大部分线程都在不断的重试,从而导致更激烈的竞争
总体而言,CAS在可伸缩性上要高于锁,在应对常见的竞争程度上,CAS的效率更高
原子变量的使用案例:
/** * * 数值范围类,其中max必须大于min * */ public class Range { private int max; private int min; public Range(int min,int max){ this.max = max; this.min= min; } //getter & setter方法 }
public class SetRange { private final AtomicReference<Range> range = new AtomicReference<Range>(); public SetRange(){ range.set(new Range(Integer.MIN_VALUE,Integer.MAX_VALUE)); } public void setMax(int i){ while(true){ Range old_ref = range.get(); if(i < old_ref.getMin()){ throw new IllegalArgumentException(); } Range new_ref = new Range(old_ref.getMin(),i); if(range.compareAndSet(old_ref, new_ref)){ return; } } } }
因为setMax和setMin中存在"先判断后执行",因此多线程的情况下,要保证max > min这个不变性约束,需要在两个方法中使用锁,或者像上面的例子,使用原子变量
非阻塞算法
首先看什么是阻塞算法:线程在请求锁时,如果锁被占用,那么该线程会被挂起,这就是阻塞算法
非阻塞算法:线程在请求锁时,无论是否请求成功,都会立即返回,这就是非阻塞算法
因为非阻塞算法需要自行实现失败处理,因此要比基于锁的算法复杂得多,但是能够提供更高的可伸缩性
实现多个原子变量的安全更新
原子变量将执行原子修改的范围缩小到单个变量上以提高效率,如果多个变量需要原子更新,那该如何做?
如果在一个原子更新操作中涉及到多个CAS操作,在并发的条件下要保持一致的状态,难点是:如何保持多个CAS的原子性的同时保证非阻塞
一般采用的方法是:
当线程B试图进行原子更新时,发现线程A正在进行更新,那么线程B会帮助线程A完成一部分更新操作,然后执行自己的更新操作,当线程A完成一部分更新操作,希望完成剩下的更新时,会发现线程B已经替他完成了
例如在并发队列中,当添加一个新的节点要做两个CAS操作:1、把新节点加入到队尾,即原队尾节点的next指向新节点 2、更新尾节点tail指针,指向新节点
下面是并发队列的实现的主要代码
public 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); } }
public class Queue<E> { private final Node<E> dummy = new Node<E>(null,null);//空节点 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(); Node<E> tailNext = curTail.next.get(); if(curTail == tail.get()){ if(tailNext != null){ //步骤A tail.compareAndSet(curTail, tailNext); //步骤B }else{ if(curTail.next.compareAndSet(null, newNode)){ //步骤C tail.compareAndSet(curTail, newNode); //步骤D return true; } } } } } }
tail指向当前队列的尾节点指针,tailNext是尾节点的next指针,newNode是新增节点
下面分析各种竞争条件
1、单线程执行, 这是没有竞争的情况,步骤A判断为false,因为尾节点的next指针必定指向null,因此执行else块的代码:把新节点添加到队尾,然后更新tail指针,最后返回true
2、线程A执行完步骤C时,线程B开始执行步骤A,会发现步骤A判断为true,因为尾节点的next指针已经被线程A修改为新节点了,然后线程B会执行步骤B(和步骤D是一样的东西),然后线程A在执行步骤D时会失败(线程B帮它完成了),线程A就返回true,但此时线程B还没完成,继续执行步骤A,按照上面的逻辑一直到返回true
这样就保证了更新的完整性
原子的域更新器
在并发链表ConcurrentLinkedQueue中使用的也是这样的算法,但在CAS操作上并不是使用原子变量,而是使用更加高效的原子的域更新器
在Node类中的next属性并不是原子引用,而是普通的volatile,通过基于反射的AtomicReferenceFieldUpdater可以进行volatile域的原子更新,这样可以避免每个Node的AtomicReference的创建过程,AtomicReferenceFieldUpdater的使用如下:
public class Node { volatile Node next; //... }
public class Queue{ public boolean put(Node newNode){ //... AtomicReferenceFieldUpdater updater=AtomicReferenceFieldUpdater.newUpdater(Node.class,Node.class,"next"); updater.compareAndSet(tail,tail.next,newNode) ; //... } }
说明几点:
1、如果Node不是Queue的内部类,那么volatile域不能为private
2、volatile域不能使用static或final修饰
3、newUpdater方法接受的三个参数分别是:对象类型,字段类型、字段名称
ABA问题
这是一个需要详细讨论的问题:参考下一篇博客:ABA问题及避免:http://blog.csdn.net/li954644351/article/details/50511879
相关文章推荐
- Java环境变量(二)
- java中的static
- Java环境变量的配置(一)
- 【第九章】 Spring的事务 之 9.1 数据库事务概述 ——跟我学spring3
- eclipse打war包
- Spring MVC 整理
- 从Eclipse转到Android Studio的注意事项
- Eclipse快捷键 10个最有用的快捷键
- java流与文件——文本输入输出
- 在Eclipse中Debug 一直source not found
- spring web项目 maven依赖包
- Eclipse加载maven依赖包失败
- Spring+Mybatis实现动态SQL查询
- Spring定时任务的几种实现
- java流与文件——流
- java垃圾回收
- spring data jpa 构建查询
- springmvc 下资源访问不到,报404错误
- java中使用base64笔记
- Spring3.0 AOP 详解