并发之无锁技术归纳

并发之AtomicBoolean/AtomicBooleanArray/AtomicBooleanUpdateFeild

1 和前面的AtomicInteger很相似或者原理基本一致的;原理就是使用了CAS算法实行循环重试的方式来保证一组操作是原子性的操作;

2 同样的也是一个无锁技术的应用;

3 在源码内部,使用1表示true,使用0表示false;

同样的boolean类型的变量在并发情况下也是不安全的,因此使用了AtomicBoolean来保障原子性的操作;

AtomicReference案例:

和AtomicInteger、AtomicLong原大同小异;只是AtomicLong而言,32位的数分为高16位和低16位分别并行计算;

AtomicReference是一种模板类,可以用来封装任何类型的数据;

public class AtomicReference<V> implements java.io.Serializable
package automic;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicRefrenceTest {
     public final static AtomicReference<String> atomicString = new AtomicReference<String>("gosaint");
     /**
      * 创建了10个线程,同时去修改atomicString的值,但是在并发状态下只有一个可以修改成功!
      * @param args
      */
     public static void main(String[] args) {
         // 开启10个线程
         for (int i = 0; i < 10; i++) {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                       try {
                            Thread.sleep(Math.abs((int) Math.random() * 100));
                       } catch (Exception e) {
                            e.printStackTrace();
                       }
                       if (atomicString.compareAndSet("gosaint", "mrc")) {
                            System.out.println(Thread.currentThread().getId() + "Change value");
                       } else {
                            System.out.println(Thread.currentThread().getId() + "Failed");
                       }
                  }
              }).start();
         }
     }
}
 

看运行结果:只有一个线程对值进行了修改;

10Change value
9Failed
11Failed
13Failed
14Failed
15Failed
12Failed
16Failed
18Failed
17Failed

CAS的缺点:

其实这个之前说过,为了深刻理解,这里再说明一下;比如在内存的初始值是3;现在存在两个线程去修改这个值;根据JVM内存模型;每一个线程都存在这个变量的副本;假设线程1去修改这个值;发现内存中的这个值是3;想要修改为4;但是此时线程2也去修改了内存的值;并且鲜牛该为2,再修改为3,此时线程1发现还是内存还是3,进行了修改;我们看到,虽然对于线程1来说,修改完成了,但是内存中的3确是经过修改2修改过的,这个值得状态已经发生了变化;如果我们对于某个值的状态很关注并且这个状态很重要;那么这个漏洞必须要避免;这也就是传说中的“ABA”问题;在JAVA中,通常是对内存中值得每一次修改添加一个时间戳作为版本号,再比较值的时候同样的时间戳的版本号也要比较;

在AtomicStampedReference类中,存在一个内部类Pair来封装值和时间戳;

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

上面的源码在比较值得时候时间戳的信息同样的也进行比较;

package automic;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicTest {
     /**
      * AtomicStampedReference(V initialRef, int initialStamp)
      *   创建具有给定初始值的新 AtomicStampedReference。
      * @param args
      */
     //创建AtomicStampedReference,用户的账户是19,版本号是0
     static AtomicStampedReference<Integer> asr=new AtomicStampedReference<Integer>(19,0);
     public static void main(String[] args) {
         //创建3个线程给用户充话费
         for(int i=0;i<3;i++){
              final int expectedStamp = asr.getStamp();//获取时间戳
              new Thread(new Runnable() {

                  @Override
                  public void run() {
                       while(true){
                            while(true){
                                //获取值
                                Integer expectedReference = asr.getReference();
                                /**
                                 * 小于20元话费,那么充值20
                                 */
                                 if(expectedReference<20){
                                     if(asr.compareAndSet(expectedReference, expectedReference+20, expectedStamp, expectedStamp+1)){
                                          System.out.println("充值成功,余额为:"+asr.getReference());
                                          break;
                                     }
                                }else{
                                     break;
                                }
                            }
                       }
                  }
              }).start();
         }
         //启动100个线程消费
         new Thread(new Runnable() {

              @Override
              public void run() {
                  for(int i=0;i<100;i++){
                       while(true){
                            int stamp = asr.getStamp();
                            Integer reference = asr.getReference();
                            if(reference>10){
                                 if(asr.compareAndSet(reference, reference-10, stamp, stamp+1)){
                                      System.out.println("消费10元,余额:"+ asr.getReference());
                             break;
                                }
                            }else{
                                break;
                            }
                       }
                       try{
                             Thread.sleep(100);
                       }catch(Exception e){

                       }
                  }

              }
         }).start();
     }
}

解释下代码,有3个线程在给用户充值,当用户余额少于20时,就给用户充值20元。有100个线程在消费,每次消费10元。用户初始有9元,当使用AtomicStampedReference来实现时,只会给用户充值一次,因为每次操作使得时间戳+1。运行结果:

充值成功,余额为:39
消费10元,余额:29
消费10元,余额:19
消费10元,余额:9

  

代码修改:修改为AtomicReference类

package automic;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicTest2 {
     /**
      * AtomicStampedReference(V initialRef, int initialStamp)
      *   创建具有给定初始值的新 AtomicStampedReference。
      * @param args
      */
     //创建AtomicStampedReference,用户的账户是19,版本号是0
     static AtomicReference<Integer> asr=new AtomicReference<Integer>(19);
     public static void main(String[] args) {
         //创建3个线程给用户充话费
         for(int i=0;i<3;i++){
              new Thread(new Runnable() {

                  @Override
                  public void run() {
                       while(true){
                            while(true){
                                //获取值
                                Integer expectedReference = asr.get();
                                /**
                                 * 小于20元话费,那么充值20
                                 */
                                 if(expectedReference<20){
                                     if(asr.compareAndSet(expectedReference, expectedReference+20)){
                                          System.out.println("充值成功,余额为:"+asr.get());
                                          break;
                                     }
                                }else{
                                     break;
                                }
                            }
                       }
                  }
              }).start();
         }
         //启动100个线程消费
         new Thread(new Runnable() {

              @Override
              public void run() {
                  for(int i=0;i<100;i++){
                       while(true){
                            Integer reference = asr.get();
                            if(reference>10){
                                 if(asr.compareAndSet(reference, reference-10)){
                                      System.out.println("消费10元,余额:"+ asr.get());
                             break;
                                }
                            }else{
                                break;
                            }
                       }
                       try{
                             Thread.sleep(100);
                       }catch(Exception e){

                       }
                  }

              }
         }).start();
     }
}

运行结果:出现了多次充值的现象:

充值成功,余额为:39
消费10元,余额:29
消费10元,余额:19
充值成功,余额为:39
消费10元,余额:29
消费10元,余额:19
充值成功,余额为:39
消费10元,余额:29
充值成功,余额为:39
消费10元,余额:39
消费10元,余额:29
充值成功,余额为:39
消费10元,余额:39

原文地址:https://www.cnblogs.com/gosaint/p/9068195.html

时间: 2024-10-02 08:48:16

并发之无锁技术归纳的相关文章

Java高并发之无锁与Atomic源码分析

目录 CAS原理 AtomicInteger Unsafe AtomicReference AtomicStampedReference AtomicIntegerArray AtomicIntegerFieldUpdater 无锁的Vector 无锁即无障碍的运行, 所有线程都可以到达临界区, 接近于无等待. 无锁采用CAS(compare and swap)算法来处理线程冲突, 其原理如下 CAS原理 CAS包含3个参数CAS(V,E,N).V表示要更新的变量, E表示预期值, N表示新值.

加锁并发算法 vs 无锁并发算法

Heinz Kabutz 在上周举办了一次成功 JCrete研讨会,我在会上参加了对一种新的 StampedLock(于JSR166中 引入) 进行的评审.StampedLock (邮戳锁) 旨在解决系统中共享资源的争用问题.在一个系统中,如果多个需要读写某一共享状态的程序并发访问这个共享对象时,争用问题就产生了.在设计 上,StampedLock 试图通过一种“乐观读取”的方式来减小系统开销,从而提供比 ReentrantReadWriteLock(重入读写锁) 更好的性能. 在评审过程中,我

使用RCU技术实现读写线程无锁

在一个系统中有一个写线程和若干个读线程,读写线程通过一个指针共用了一个数据结构,写线程改写这个结构,读线程读取该结构.在写线程改写这个数据结构的过程中,加锁情况下读线程由于等待锁耗时会增加. 可以利用RCU (Read Copy Update What is rcu)的思想来去除这个锁.本文提到的主要实现代码:gist RCU RCU可以说是一种替代读写锁的方法.其基于一个事实:当写线程在改变一个指针时,读线程获取这个指针,要么获取到老的值,要么获取到新的值.RCU的基本思想其实很简单,参考Wh

一个无锁消息队列引发的血案(六)——RingQueue(中) 休眠的艺术 [续]

目录 (一)起因 (二)混合自旋锁 (三)q3.h 与 RingBuffer (四)RingQueue(上) 自旋锁 (五)RingQueue(中) 休眠的艺术 (六)RingQueue(中) 休眠的艺术 [续] 开篇 这是第五篇的后续,这部分的内容同时会更新和添加在 第五篇:RingQueue(中) 休眠的艺术 一文的末尾. 归纳 紧接上一篇的末尾,我们把 Windows 和 Linux 下的休眠策略归纳总结一下,如下图: 我们可以看到,Linux 下的 sched_yield() 虽然包括了

并发无锁队列

1.前言 队列在计算机中非常重要的一种数据结构,尤其在操作系统中.队列典型的特征是先进先出(FIFO),符合流水线业务流程.在进程间通信.网络通信之间经常采用队列做缓存,缓解数据处理压力.结合自己在工作中遇到的队列问题,总结一下对不同场景下的队列实现.根据操作队列的场景分为:单生产者--单消费者.多生产者--单消费者.单生产者--多消费者.多生产者--多消费者四大模型.其实后面三种的队列,可以归纳为一种多对多.根据队列中数据分为:队列中的数据是定长的.队列中的数据是变长的. 2.队列操作模型 (

无锁机制实现并发访问

对于并发控制而言, 锁是一种悲观的策略.它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼.如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行. 而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的.既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行.那遇到冲突怎么办呢?无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲

基于循环数组的无锁队列

在之前的两篇博客(线程安全的无锁RingBuffer的实现,多个写线程一个读线程的无锁队列实现)中,分别写了在只有一个读线程.一个写线程的情况下,以及只有一个写线程.两个读线程的情况下,不采用加锁技术,甚至原子运算的循环队列的实现.但是,在其他的情况下,我们也需要尽可能高效的线程安全的队列的实现.本文实现了一种基于循环数组和原子运算的无锁队列.采用原子运算(compare and swap)而不是加锁同步,可以很大的提高运行效率.之所以用循环数组,是因为这样在使用过程中不需要反复开辟内存空间,可

生产者消费者模式下的并发无锁环形缓冲区

上一篇记录了几种环形缓冲区的设计方法和环形缓冲区在生产者消费者模式下的使用(并发有锁),这一篇主要看看怎么实现并发无锁. 0.简单的说明 首先对环形缓冲区做下说明: 环形缓冲区使用改进的数组版本,缓冲区容量为2的幂 缓冲区满阻塞生产者,消费者进行消费后,缓冲区又有可用资源,由消费者唤醒生产者 缓冲区空阻塞消费者,生产者进程生产后,缓冲区又有可用资源,由生产者唤醒消费者 然后对涉及到的几个技术做下说明: ⑴CAS,Compare & Set,X86下对应的是CMPXCHG 汇编指令,原子操作,基本

谈谈存储软件的无锁设计

面向磁盘设计的存储软件不需要考虑竞争锁带来的性能影响.磁盘存储软件的性能瓶颈点在于磁盘,磁盘抖动会引入极大的性能损耗.因此,传统存储软件的设计不会特别在意处理器的使用效率.曾经对一个存储虚拟化软件进行性能调优,在锁竞争方面做了大量优化,最后也没有达到性能提升的效果,原因就在于存储虚拟化的性能瓶颈点在于磁盘,而不在于处理器的使用效率.正因为如此,在面向磁盘设计的软件中,很多都采用单线程.单队列处理的方式,一定程度上还可以避免由于并发所引入的磁盘抖动问题. 在面向NVMe SSD设计的存储软件中,这