首页 > 编程知识 正文

无锁算法,heicard com iccid

时间:2023-05-05 04:01:38 阅读:47292 作者:163

来源:

https://blog .喜欢的过去. me/2019/01/head-first-cas.html

在后端开发中,您可能遇到过计数器实现线程安全的需求。 根据经验,您应该知道多线程将实现共享变量的原子性和可见性问题。 因此,摇滚成为了不可避免的话题。 今天讨论与之对应的无锁CAS。 本文从如何来、是什么、如何使用、原理分析、遇到的问题等不同角度带你真正了解CAS。

考虑到为什么不用锁,如何通过多线程来确保安全,首先带出来的一定是锁,从硬件、操作系统方面也或多或少地使用了锁。 钥匙有什么缺点吗? 当然有。 否则,JDK为什么会出现那么多钥匙,是因为每把钥匙都有优缺点。

要使用锁定,必须获取锁定并解除锁定。 CPU必须通过上下文切换和日程管理来执行此操作。 对一个独占锁而言,一个线程保持锁定后,没有运行完毕的其他兄弟必须在外面等待。 直到上一个兄弟完成运行,处理器哥哥才会把锁拿到其他线程上抢(不公平)。 锁定的这个概念基于悲观机制,并且始终认为将修改数据,因此在处理部分代码块之前添加锁定,操作完成后再释放它是安全的。 实际上,在JDK1.5中使用同步就可以了。

但是,上述操作会在多线程的情况下不断切换CPU,非常消耗资源。 我们知道可以使用特定类型的锁来避免某些问题。 那个除了锁门方法以外还有吗? 当然,有人提出了一种无锁算法。 有名的是今天我们说的CAS(compareandswap )。 与锁定不同,这是一种乐观的机制,别人去取数据时认为不会修改,但修改数据时会判断数据此时的状态。 这样,CPU就不会切换。 阅读较多时,性能会大幅提高。 我们现在使用的大多数CPU都有CAS指令,并且在硬件级别支持免锁定,所以可以在开发时调用。

无论是锁定还是未锁定,都有优点和缺点,但我们也举例说明CAS的问题。

什么是CAS前没有钥匙的CAS? 到底什么是CAS呢? 我已经等不及了,看看维基百科的说明吧

并发交换(compare and swap,CAS )是原子操作的一种,可以用于实现多线程编程中不中断的数据交换操作,由于多线程同时重写数据时执行顺序的不确定性和中断的不可预测性而产生此操作通过将内存中的值与指定的数据进行比较,在数值相同时将内存中的数据替换为新值。

CAS在比较替换上完成了原子性,为我们提供了看代码的想法:

1intcas(long*addr,longold,longnew )原子执行)/3if ) addr!=old (4返回0; 5*addr=new; 6返回1; 7 )这是c语言代码,您可以看到有三个参数:

*与*addr:相比的值

old:内存的当前值

new:准备要修改的新值并将其写入存储器

只要当前传递的比较对象的值等于内存中的值,新值就会成功修改。 否则,返回0表示比较失败。 学习数据库的同学知道悲观锁和乐观锁,乐观锁始终认为数据不会被修改。 基于这个假设,CAS的操作也被认为与内存中的值和当前值相等,所以操作总是成功的,我们不用锁定就可以实现多线程的原子操作。

如果在多线程中使用CAS同时更新同一变量,则只有一个线程可以更新变量值,但所有其他线程都将失败。 失败的线程不是被阻止挂起,而是这次修改失败了。 请再试一次。 那样的话,就可以写这样的代码了。

1while (! CAS(addr,old,newValue ) )4//success5printf (' new value=% LD ',addr ); 但我相信这样的代码可能让你看出了其中的蹊跷。 这个稍后分析。 看看CAS在Java中是如何使用的。

Java的CAS还是未来的问题。 用Java的API实现时,可能会考虑锁定(可能是同步或其他类型的锁定)和使用atomic类(如AtomicInteger )。 这一系列类在JDK1.5时出现,我们经常使用

1 executorserviceexecutorservice=executors.newcachedthreadpool (; 2 atomicintegeratomicinteger=newatomicinteger (0; 34for(inti=0; i5000; I ) {5executorservice.execute (自动集成器:3360增量安装); 6 } 78系统. out.println (atomic inte

ger.get());9executorService.shutdown();

这个例子开启了 5000 个线程去进行累加操作,不管你执行多少次答案都是 5000。这么神奇的操作是如何实现的呢?就是依靠 CAS 这种技术来完成的,我们揭开 AtomicInteger 的老底看看它的代码:

1public class AtomicInteger extends Number implements java.io.Serializable { 2    private static final long serialVersionUID = 6214790243416807050L; 3 4    // setup to use Unsafe.compareAndSwapInt for updates 5    private static final Unsafe unsafe = Unsafe.getUnsafe(); 6    private static final long valueOffset; 7 8    static { 9        try {10            valueOffset = unsafe.objectFieldOffset11                (AtomicInteger.class.getDeclaredField("value"));12        } catch (Exception ex) { throw new Error(ex); }13    }1415    private volatile int value;1617    /**18     * Creates a new AtomicInteger with the given initial value.19     *20     * @param initialValue the initial value21     */22    public AtomicInteger(int initialValue) {23        value = initialValue;24    }2526    /**27     * Gets the current value.28     *29     * @return the current value30     */31    public final int get() {32        return value;33    }3435    /**36     * Atomically increments by one the current value.37     *38     * @return the updated value39     */40    public final int incrementAndGet() {41        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;42    }4344}

这里我只帖出了我们前面例子相关的代码,其他都是类似的,可以看到 incrementAndGet 调用了 unsafe.getAndAddInt 方法。Unsafe 这个类是 JDK 提供的一个比较底层的类,它不让我们程序员直接使用,主要是怕操作不当把机器玩坏了。。。(其实可以通过反射的方式获取到这个类的实例)你会在 JDK 源码的很多地方看到这家伙,我们先说说它有什么能力:

内存管理:包括分配内存、释放内存

操作类、对象、变量:通过获取对象和变量偏移量直接修改数据

挂起与恢复:将线程阻塞或者恢复阻塞状态

CAS:调用 CPU 的 CAS 指令进行比较和交换

内存屏障:定义内存屏障,避免指令重排序

这里只是大致提一下常用的操作,具体细节可以在文末的参考链接中查看。下面我们继续看 unsafe 的 getAndAddInt 在做什么。

1public final int getAndAddInt(Object var1, long var2, int var4) { 2    int var5; 3    do { 4        var5 = this.getIntVolatile(var1, var2); 5    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 6 7    return var5; 8} 910public native int getIntVolatile(Object var1, long var2);11public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

其实很简单,先通过 getIntVolatile 获取到内存的当前值,然后进行比较,展开 compareAndSwapInt 方法的几个参数:

var1: 当前要操作的对象(其实就是 AtomicInteger 实例)

var2: 当前要操作的变量偏移量(可以理解为 CAS 中的内存当前值)

var4: 期望内存中的值

var5: 要修改的新值

所以 this.compareAndSwapInt(var1, var2, var5, var5 + var4) 的意思就是,比较一下 var2 和内存当前值 var5 是否相等,如果相等那我就将内存值 var5 修改为 var5 + var4(var4 就是 1,也可以是其他数)。

这里我们还需要解释一下 偏移量 是个啥?你在前面的代码中可能看到这么一段:

1// setup to use Unsafe.compareAndSwapInt for updates 2private static final Unsafe unsafe = Unsafe.getUnsafe(); 3private static final long valueOffset; 4 5static { 6    try { 7        valueOffset = unsafe.objectFieldOffset 8            (AtomicInteger.class.getDeclaredField("value")); 9    } catch (Exception ex) { throw new Error(ex); }10}1112private volatile int value;

可以看出在静态代码块执行的时候将 AtomicInteger 类的 value 这个字段的偏移量获取出来,拿这个 long 数据干嘛呢?在 Unsafe 类里很多地方都需要传入 obj 和偏移量,结合我们说 Unsafe 的诸多能力,其实就是直接通过更底层的方式将对象字段在内存的数据修改掉。

使用上面的方式就可以很好的解决多线程下的原子性和可见性问题。由于代码里使用了 do while 这种循环结构,所以 CPU 不会被挂起,比较失败后重试,就不存在上下文切换了,实现了无锁并发编程。

CAS 存在的问题 自旋的劣势

你留意上面的代码会发现一个问题,while 循环如果在最坏情况下总是失败怎么办?会导致 CPU 在不断处理。像这种 while(!compareAndSwapInt) 的操作我们称之为自旋,CAS 是乐观的,认为大家来并不都是修改数据的,现实可能出现非常多的线程过来都要修改这个数据,此时随着并发量的增加会导致 CAS 操作长时间不成功,CPU 也会有很大的开销。所以我们要清楚,如果是读多写少的情况也就满足乐观,性能是非常好的。

ABA 问题

提到 CAS 不得不说 ABA 问题,它是说假如内存的值原来是 A,被一个线程修改为了 B,此时又有一个线程把它修改为了 A,那么 CAS 肯定是操作成功的。真的这样做的话代码可能就有 bug 了,对于修改数据为 B 的那个线程它应该读取到 B 而不是 A,如果你做过数据库相关的乐观锁机制可能会想到我们在比较的时候使用一个版本号 version 来进行判断就可以搞定。在 JDK 里提供了一个 AtomicStampedReference 类来解决这个问题,来看一个例子:

1int stamp = 10001;23AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(0, stamp);45stampedReference.compareAndSet(0, 10, stamp, stamp + 1);67System.out.println("value: " + stampedReference.getReference());8System.out.println("stamp: " + stampedReference.getStamp());

它的构造函数是 2 个参数,多传入了一个初始 时间戳,用这个戳来给数据加了一个版本,这样的话多个线程来修改如果提供的戳不同。在修改数据的时候除了提供一个新的值之外还要提供一个新的戳,这样在多线程情况下只要数据被修改了那么戳一定会发生改变,另一个线程拿到的是旧的戳所以会修改失败。

尝试应用

既然 CAS 提供了这么好的 API,我们不妨用它来实现一个简易版的独占锁。思路是当某个线程进入 lock 方法就比较锁对象的内存值是否是 false,如果是则代表这把锁它可以获取,获取后将内存之修改为 true,获取不到就自旋。在 unlock 的时候将内存值再修改为 false 即可,代码如下:

1public class SpinLock { 2 3    private AtomicBoolean mutex = new AtomicBoolean(false); 4 5    public void lock() { 6        while (!mutex.compareAndSet(false, true)) { 7            // System.out.println(Thread.currentThread().getName()+ " wait lock release"); 8        } 9    }1011    public void unlock() {12        while (!mutex.compareAndSet(true, false)) {13            // System.out.println(Thread.currentThread().getName()+ " wait lock release");14        }15    }1617}

这里使用了 AtomicBoolean 这个类,当然用 AtomicInteger 也是可以的,因为我们只保存一个状态 boolean 占用比较小就用它了。这个锁的实现比较简单,缺点非常明显,由于 while 循环导致的自旋会让其他线程都在占用 CPU,但是也可以使用,关于锁的优化版本实现我会在后续的文章中进行改进和说明,正因为这些问题我们也会在后续研究 AQS 这把利器的优点。

CAS 源码

看了上面的这些代码和解释相信你对 CAS 已经理解了,下面我们要说的原理是前面的 native 方法中的 C++ 代码写了什么,在 openjdk 的 /hotspot/src/share/vm/prims 目录中有一个 Unsafe.cpp 文件中有这样一段代码:

注意:这里以 hotspot 实现为例

1UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))2  UnsafeWrapper("Unsafe_CompareAndSwapInt");3  oop p = JNIHandles::resolve(obj);4  // 通过偏移量获取对象变量地址5  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);6  // 执行一个原子操作7  // 如果结果和现在不同,就直接返回,因为有其他人修改了;否则会一直尝试去修改。直到成功。8  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;9UNSAFE_END 参考资料

cas wiki

https://zh.wikipedia.org/wiki/%E6%AF%94%E8%BE%83%E5%B9%B6%E4%BA%A4%E6%8D%A2

说一说Java的Unsafe类

https://www.cnblogs.com/pkufork/p/java_unsafe.html

Java Magic. Part 4: sun.misc.Unsafe

http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。