java并发性能(三)之非阻塞操作(上)

小孩子 我们都是小青蛙 2018-05-31

本人水平有限,如果在文中发现任何问题或疑惑,请留言,如果留言写不清的话,可以加我微信 xiaohaizi4919 ,我的位置是西北工业大学友谊校区图书馆3楼北区科技典藏阅览室,如果想找我随时在哈(木有周末,木有假期),相互学习,相互进步哈~


本篇文章不是java语法的基本教程!在阅读之前,请保证你有面向对象的编程基础,熟悉封装、继承、多态,否则的话,你不适合阅读本篇文章,先学一下基础吧~

6. 本公众号的文章都是需要被系统性学习的,这篇文章是建立在之前的几篇文章之上的,如果你没有读过,务必读后再看本篇,要不然可能会有阅读不畅的体验:

锁的劣势

为了引出新的主题,我不得不再扒一扒锁不好的地方,不管是内置锁还是显式锁,一个线程在获取锁之后,其他等待获取这个锁的线程都会发生阻塞从而发生线程的上下文切换,并且被阻塞的线程可能被操作系统挂起,也就是从内存中放到硬盘里。

与锁相比,多线程对volatile变量的读写就不会发生线程阻塞,自然不会发生线程切换以及挂起的事情,但是volatile变量只能保证对变量的读写操作是具有内存可见性的,不能保证某些操作是以原子的方式执行的,典型的例子就是i++操作,它是由3个独立的原子操作所构成,到目前为止,我们保证这几个操作具有原子性的方式就只有加锁了。如果多个线程竞争一个锁的程度很激烈,而且每个线程持有锁的时间又很短,这很可能导致线程在因为阻塞导致的上下文切换和挂起浪费的时间已经大大超过了执行操作的时间;而且如果持有锁的线程因为操作系统的一些问题(比如调度的比较迟,内存分配的比较慢)而延迟执行的时候,其他等待这个锁的线程同样需要延迟执行;另外,如果一个低优先级的线程持有锁,高优先级的线程因为获取不到锁而长时间阻塞,这就是所谓的优先级反转问题。

正是因为有这么多问题,所以我们希望有一种机制即可以保证某些操作的原子性,其他线程又可以不用发生阻塞。幸运的是,现代的处理器上对某些复合操作已经提供原子性保证了,我们接着往下看。

CompareAndSwap

世界上有悲观的人也有乐观的人,悲观的人觉得世界不美好,不好的事情一定会发生,乐观的人觉得世界很奇妙,凡事都是先试试嘛,有不好的事儿等到发生再考虑怎么办。

对于并发操作来说,以i++为例,我们之前使用的锁,包括内置锁和ReentrantLock都属于一种悲观的技术,也就是说使用锁来保护操作的话总是认为竞争一定会发生,也就是认为一定会有多个线程同时执行i++的操作,所以对该操作加锁保护之后,同一时刻只能有一个线程执行该操作,其他线程只能排队等待。而一种乐观的技术就觉得在同一时刻很可能多个线程并不会同时执行该操作,所以可以直接上去就执行操作,但是如果真发生冲突的时候咋办呢?需要借助一种冲突检查机制来判断操作过程中是否收到了其他线程的干扰,如果没有干扰,则执行完操作返回true,如果受到了干扰那就什么都不做直接返回false,比如两个线程同时执行i++操作,同时读到i的值为5,第一个线程先执行更新操作,这样i的值就为6了,这个过程中没有收到别的线程干扰,所以进行的很顺利,第二个线程再执行更新操作,发现i的值已经不是之前读到的5了,所以放弃本次操作,等待下次重试。

那第二个线程如何监测到现在的i值已经不是之前读到的5了呢,这个就是所谓的冲突检查机制,底层的处理器已经帮我们实现了在更新一个值的时候比较一下实际值和给定值是否一致,如果一致则更新并返回true,不一致则不更新并返回false的操作。请注意, 先比较再更新 这个操作的原子性是由底层硬件保证的,我们学会利用这个原子性操作来实现乐观的并发操作就可以啦。

我不知道你有没有纠结为什么`先比较再更新`的操作会是一个原子操作呢?明明是两个操作嘛,`先比较``再更新`嘛!
其实换个思路再想一下,所有的指令都会被放到处理器里执行,处理器把`先比较再更新`当成是一个指令,我们可以分成两种情况来看处理器是如何保证该操作是原子性的:
1. 如果在单个处理器里,那么只要处理器被设计成在只要这个指令没有被执行完那么就不允许线程切换,就可以保证这个操作的原子性了。
2. 如果在多个处理器,一般情况下各个处理器执行指令是互不相关的,但是在对某个变量执行`先比较再更新`指令的时候,先执行这个指令的处理器会采取一些措施,比方说把各个处理器和内存通信的总线给锁住,或者把要更新的变量的锁在的缓存给锁住,保证完整执行完这个指令其他的处理器才能继续执行对该变量的`先比较再更新`操作。
喔,如果你看不懂这里头在说啥,那就别看了,毕竟是系统底层的东西,如果想了解的更透彻,可以去找一本计算机体系结构的书读一下,我们是在唠叨java语法,所以你只需要知道在处理器层面可以保证`先比较再更新`的原子性的,我们可以利用这个操作的原子性来进行并发编程的乐观操作就行了。

先比较再更新的英文原话是CompareAndSwap,简称是CAS,其实也可以翻译成比较并交换,这个操作需要3个参数:

  1. 你要更新的变量V

  2. 原来的值A

  3. 即将更新的新值B

操作过程就是当V的值等于A时,将新值B赋值给V并且返回true,否则什么都不做,返回false。所以当多个线程使用CAS同时更新一个变量时,只有一个线程可以成功的更新,其他线程都将失败,但是失败的线程并不会被阻塞甚至挂起,而是被立即告知失败了,然后可以再次尝试更新。

原子变量

不过上边只是说了CAS操作的大致原理,设计java的大叔们根据这个原理定义了许多的原子变量类,每个原子变量的对象都维护一个对应的数据类型的值,这些类提供一些方法,可以用原子的方式去更新某种类型的变量,所以我们可以在构建我们自己的并发程序时使用这些类来替代之前的锁。他们定义了好多个原子操作类,可以分为4种类型,我们下来详细看一下。

原子更新基本类型类

可以用原子的方式更新基本数据类型数据,定义了下边这么3个类:

  • AtomicBoolean,原子方式更新bool类型变量

  • AtomicInteger,原子方式更新int类型变量

  • AtomicLong,原子方式更新long类型变量

这些类内部都维护了一个volatile的变量,比如AtomicBoolean内部维护一个boolean类型的volatile变量,AtomicInteger内部维护一个int类型的volatile变量,而AtomicLong内部维护一个long类型的volatile变量。

它们定义的方法基本一致,我们以AtomicInteger为例来看一下这些类提供的构造方法:

以及一些重要方法:

小贴士:

这里只是举了一部分方法作为例子,具体全部的方法可以参考java文档或者源代码。

我们来写段代码来测试一下上边的一些方法:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {

   private static AtomicInteger atomicInteger = new AtomicInteger(1);

   public static void main(String[] args) {
       System.out.println("atomicInteger代表的int值是:" + atomicInteger.get());
       System.out.println("atomicInteger自增1之后的值是:" + atomicInteger.incrementAndGet());

       boolean result = atomicInteger.compareAndSet(2, 3);
       System.out.println("atomicInteger比较并交换的执行成功了么:" + result + " 结果是:" + atomicInteger.get());

   }
}

执行结果是:

atomicInteger代表的int值是:1
atomicInteger自增1之后的值是:2
atomicInteger比较并交换的执行成功了么:true 结果是:3

这个类看着是挺好用的,它的compareAndSet是怎么实现的呢?我们来看看源代码:

public final boolean compareAndSet(int expect, int update) {
   return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

原来是调用了一个叫unsafe对象的方法,再点开这个方法看看:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int int x);

我们发现这是一个所谓的native方法,说明这个方法是调用了更底层的某些指令,就是靠底层的这些指令来保证这个比较并交换操作的原子性,这些指令我们java程序员就不用管怎么实现的啦!

好啦,compareAndSet直接调用了底层指令我们知道啦,再看看别的方法是咋实现的,比如getAndIncrement这个方法:

public final int getAndIncrement() {
   for (;;) {
       int current = get();
       int next = current + 1;
       if (compareAndSet(current, next)) {
           return current;
       }
   }
}

也就是说将加1后的值直接调用compareAndSet,如果调用失败返回false,就意味着别的线程先于本线程修改了int值,所以直接重试,直到操作成功了为止,就是这么暴力。别的方法的实现和这个方法的实现方案是一样的,都是如果失败了就不断重试,直到成功了为止

原子更新数组

我们也可以通过原子的方式更新某个数组里的某个元素,设计java的大叔们提供了下边几个类:

  • AtomicIntegerArray,原子方式更新int类型数组中的元素

  • AtomicLongArray,原子方式更新long类型数组中的元素

  • AtomicReferenceArray,原子方式更新引用类型数组中的元素

这些类内部都维护了一个数组,比如AtomicIntegerArray内部维护一个int类型数组,AtomicIntegerArray内部维护一个long类型的数组,而AtomicReferenceArray内部维护一个Object类型的数组。

这几个类提供的方法几乎是一样的,我们这里以AtomicIntegerArray来举例来看它们提供的构造方法:

再看一下提供的方法:

如需查看更多方法,请查看java文档或源代码

下边我们来使用一下这个类:

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayDemo {

   private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1, 3, 5, 7, 9});

   public static void main(String[] args) {
       System.out.println("atomicIntegerArray代表的int数组第1号元素是:" + atomicIntegerArray.get(1));
       System.out.println("atomicIntegerArray第1号元素自增1之后的值是:" + atomicIntegerArray.incrementAndGet(1));

       boolean result = atomicIntegerArray.compareAndSet(1, 4, 5);
       System.out.println("atomicIntegerArray对第1号元素的比较并交换的执行成功了么:" + result + " 结果是:" + atomicIntegerArray.get(1));
   }
}

执行结果是:

atomicIntegerArray代表的int数组第1号元素是:3
atomicIntegerArray第1号元素自增1之后的值是:4
atomicIntegerArray对第1号元素的比较并交换的执行成功了么:true 结果是:5

其实这个数组类型和基本类型的原理是一样一样的,这里就不多赘述了哈。

原子更新引用类型

  • AtomicReference,原子方式更新引用类型变量

  • AtomicStampedReference,原子方式更新带有版本号的引用类型

  • AtomicMarkableReference,原子方式更新带有标记位的引用类型

其实各种原子变量的方法都挺像的,我这就节省点篇幅,直接写个例子展示下用法就好了哦:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {

   private static class MyObj {
       int i;

       public MyObj(int i) {
           this.i = i;
       }
   }

   private static AtomicReference atomicReference = new AtomicReference<>();

   public static void main(String[] args) {
       MyObj myObj = new MyObj(5);
       atomicReference.set(myObj);
       boolean result = atomicReference.compareAndSet(myObj, new MyObj(6));
       System.out.println("atomicReference的比较并交换成功了么:" + result + " 结果是:" + atomicReference.get().i);
   }
}

执行结果是:

atomicReference的比较并交换成功了么:true 结果是:6

AtomicStampedReferenceAtomicMarkableReference的用法就不说了,等用到了再说,反正你知道底层用的是CAS机制就好了。

原子更新字段类

  • AtomicIntegerFieldUpdater,原子方式更新某个对象的int型字段

  • AtomicLongFieldUpdater,原子方式更新某个对象的long型字段

  • AtomicReferenceFieldUpdater,原子方式更新引用类型指向对象里的字段

这些类都是抽象类,每次使用的时候必须用newUpdater()静态方法创建一个对象,调用这个方法的时候需要更新的类和字段名,需要注意的是,更新的字段必须使用public volatile修饰。我们以AtomicIntegerFieldUpdater为例来看一下:

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterDemo {

   private static class MyObj {
       public volatile int i;

       public MyObj(int i) {
           this.i = i;
       }
   }

   private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(MyObj.class, "i");

   public static void main(String[] args) {
       MyObj myObj = new MyObj(5);
       System.out.println("更新后的值是:" +  updater.incrementAndGet(myObj));
   }
}

执行的结果是:

更新后的值是:6

题外话

写文章挺累的,有时候你觉得阅读挺流畅的,那其实是背后无数次修改的结果。如果你觉得不错请帮忙转发一下,万分感谢~


    本站仅按申请收录文章,版权归原作者所有
    如若侵权,请联系本站删除
    觉得不错,分享给更多人看到
    我们都是小青蛙 热门文章:

    java并发编程之原子性操作    阅读/点赞 : 0/0

    我们都是小青蛙,呱呱呱呱呱    阅读/点赞 : 0/0

    活跃性(死锁、饥饿、活锁)    阅读/点赞 : 0/0

    InnoDB空间文件布局的基础知识    阅读/点赞 : 0/0

    指令重排序    阅读/点赞 : 0/0

    InnoDB的Buffer Pool简介    阅读/点赞 : 0/0

    一些比较重要的数字电路模块    阅读/点赞 : 0/0

    《UNIX环境高级编程》书籍推荐    阅读/点赞 : 0/0

    InnoDB中的B+树索引结构    阅读/点赞 : 0/0

    InnoDB索引页面的物理结构    阅读/点赞 : 0/0

    我们都是小青蛙 微信二维码

    我们都是小青蛙 微信二维码