当前位置: 首页 > 科技观察

告诉你一个AtomicInteger的大秘密!

时间:2023-03-15 10:57:03 科技观察

i++不是线程安全操作,因为它不是原子操作。那么,如果我想实现类似i++的效果,应该使用哪些集合或者工具类呢?在JDK1.5之前,为了保证多线程下对一个基本数据类型或引用数据类型的操作的原子性,必须依赖于外部关键字synchronized,但这种情况在JDK1.5之后有所改变。当然,你仍然可以使用synchronized来保证原子性。我们这里说的一种线程安全的方式是原子工具类,比如“AtomicInteger,AtomicBoolean”等,这些原子类都是线程安全的工具类,它们也是Lock-Free的。让我们来看看这些工具以及Lock-Free是什么。认识AtomicIntegerAtomicInteger是JDK1.5中新加入的工具类。我们先看它的继承关系,和int包装类Integer一样,都是继承自Number类。Number类是基本数据类型的包装类,与数据类型相关的对象一般都继承自Number类。它的继承系统非常简单。让我们看一下它的基本属性和方法。AtomicInteger的基本属性具有三个基本属性。一些本机方法保证操作的原子性。Unsafe的objectFieldOffset方法可以得到成员属性在内存中的地址相对于对象内存地址的偏移量。简单点说就是找到这个变量在内存中的地址,这样后面就可以直接通过内存地址来操作了。这个值就是value,后面会详细说,value就是AtomicIneger的值。AtomicInteger的构造方法继续往下看。AtomicInteger的构造方法只有两种。一种是不带参数的构造方法。不带参数的构造方法默认值初始值为0,带参数的构造方法可以指定初始值。AtomicInteger中的方法我们来谈谈AtomicInteger中的方法。Get和Set先来看最简单的get和set方法:get():获取当前AtomicInteger的值set():设置当前AtomicInteger的值get()可以原子读取AtomicInteger中的数据,set()可以原子地设置当前值,因为get()和set()最终都是作用于value变量,而value是通过volatile修改的,所以get和set相当于读取和设置内存。如下图所示,上面我们提到了i++和i++的非原子操作,我们说可以使用AtomicInteger中的方法来代替。增量操作AtomicInteger中的Incremental相关方法可以满足我们的需求getAndIncrement():原子地增加当前值并返回结果。相当于i++的运算。为了验证是否线程安全,我们使用下面的例子来测试publicclassTAtomicTestimplementsRunnable{AtomicIntegeratomicInteger=newAtomicInteger();@Overridepublicvoidrun(){for(inti=0;i<10000;i++){System.out.println(atomicInteger.getAndIncrement());}}publicstaticvoidmain(String[]args){TAtomicTesttAtomicTest=newTAtomicTest();Threadt1=newThread(tAtomicTest);Threadt2=newThread(tAtomicTest);t1.start();t2.start();}}通过输出结果,你会发现这是一个线程安全的操作。可以修改i的值,但最终结果还是i-1,因为是先取值,再+1。其原理图如下。incrementAndGet则相反,先进行+1操作,然后返回自增后的结果。这种操作方式可以保证对值的原子操作。如下图,Decremental操作与此相反,x--或x=x-1等自减操作也是原子的。我们仍然可以使用AtomicInteger中的方法来代替getAndDecrement:返回当前类型的int值,然后对value进行自减。下面是测试代码String[]args){TAtomicTestDecrementtAtomicTest=newTAtomicTestDecrement();Threadt1=newThread(tAtomicTest);Threadt2=newThread(tAtomicTest);t1.start();t2.start();}}下面是getAndDecrementdecrementAndGet的示意图:同理,decrementAndGet方法是先进行自减操作,然后获取value的值。示意图如下。LazySet方法volatile有内存屏障,你知道吗?什么是内存屏障?内存屏障,又称内存屏障、内存屏障、屏障指令等,是一类同步屏障指令,是CPU或编译器随机访问操作中的一个同步点,使得在该点之前的所有读写操作可以在该点之后的操作之前执行。它也是一种使CPU处理单元中的内存状态对其他处理单元可见的技术。CPU用了很多优化,缓存,指令重排等等,最终目的还是为了性能。也就是说,当一个程序执行时,只要最终结果相同,指令是否重新排列都无所谓。所以指令的执行时序并不是按顺序执行的,而是乱序的,这样会造成很多问题,这也促使了内存屏障的出现。从语义上讲,内存屏障之前的所有写操作都必须写入内存;内存屏障之后的读操作可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,可以在写操作之后和读操作之前插入内存屏障。内存屏障的开销很轻,但是再小也是有开销的。LazySet就是这样做的。它以普通变量的形式读写变量。也可以说:“懒得设置障碍”GetAndSet方法自动设置为给定值并返回旧值。它的源码是在unsafe中调用getAndSetInt方法。如下图,先循环再调用getIntVolatile方法。我在cpp中没有找到这个方法。如果你找到了,记得及时告诉我学习。循环直到compareAndSwapInt返回false,这意味着使用CAS还没有更新到新值,所以var5返回最新的内存值。CAS方法我们一直说CAS其实就是CompareAndSet方法。顾名思义,该方法的意思是“比较和更新”。更新。上面给出了CASJava级别的源码,JDK官方对其的解释是“如果当前值等于expect的值,则以原子方式将当前值设置为update的给定值”。该方法会返回一个boolean类型,如果为true则表示比较更新成功,否则表示失败。CAS也是一种无锁并发机制,也叫LockFree,所以你觉得LockFree很高吗?不。下面我们构建一个CASLockclassCASLock{AtomicIntegeratomicInteger=newAtomicInteger();ThreadcurrentThread=null;publicvoidtryLock()throwsException{booleanisLock=atomicInteger.compareAndSet(0,1);if(!isLock){thrownewException("Lockingfailure");}currentThread=Thread.currentThread();System.out.println(currentThread+"tryLock");}publicvoidunlock(){intlockValue=atomicInteger.get();if(lockValue==0){return;}if(currentThread==Thread.currentThread()){atomicInteger.compareAndSet(1,0);System.out.println(currentThread+"unlock");}}publicstaticvoidmain(String[]args){CASLockcasLock=newCASLock();for(inti=0;i<5;i++){newThread(()->{try{casLock.tryLock();Thread.sleep(10000);}catch(Exceptione){e.printStackTrace();}finally{casLock.unlock();}}).start();}}}在上面的代码中,我们构建了一个CASLock。在tryLock方法中,我们先使用CAS方法进行更新。如果更新不成功,则抛出异常并将当前线程设置为锁定线程。在unLock方法中,我们先判断当前值是否为0,如果为0,就是我们要看到的结果,直接返回。否则为1,表示当前线程仍处于锁定状态。我们来判断当前线程是否为加锁线程,如果是则执行解锁操作。那么我们上面提到的compareAndSet其实可以解析成如下操作//伪代码//当前值intv=0;inta=0;intb=1;if(compare(0,0)==true){set(0,1);}else{//继续向下执行}也可以以生活场景中的买票为例。进入景区必须要有门票。如果你有假票或者不符合景区门票的门票,肯定可以鉴别。如果没有门票,是肯定进不了景区的。废话少说,这里是compareAndSetweakCompareAndSet的原理图:靠,我很仔细的看了好几遍,发现JDK1.8的这个方法和compareAndSet方法一模一样,骗我。..但事实真的如此吗?不会,JDK源码博大精深,不会设计重复的方法。仔细想想,JDK团队不会犯这么低级的团队,但这是什么原因呢?《Java 高并发详解》这本书给了我们答案。AddAndGetAddAndGet和getAndIncrement、getAndAdd、incrementAndGet等方法都使用了do...while+CAS操作,其实相当于一个自旋锁。如果CAS修改成功,会一直循环下去,修改失败。将返回。深入AtomicInteger的示意图如下。上面我们讨论了AtomicInteger的具体使用。同时,我们知道AtomicInteger是依赖volatile和CAS来保证原子性的。那么我们来分析一下为什么CAS可以保证原子性。它的底层是什么?AtomicInteger和乐观锁有什么关系?AtomicInteger的底层实现原理我们来看看这个可爱的compareAndSetL(CAS)方法。这两行代码为什么要保证原子性呢?我们可以看到这个CAS方法相当于调用了unsafe中的compareAndSwapInt方法,只有进入unsafe才能看到具体的实现。compareAndSwapInt是sun.misc中的一个方法。这个方法是一个native方法,它的底层是用C/C++实现的,所以需要看C/C++的源码。你知道C/C++的牛逼吗?用Java是玩应用和架构,C/C++是玩服务器和底层。compareAndSwapInt的源码在jdk8u-dev/hotspot/src/share/vm/prims/unsafe.app路径下,其源码实现是Unsafe_CompareAndSwapInt方法。我们找到了这个方法。C/C++源码看不懂,但这并不妨碍我们找到关键代码Atomic::cmpxchg。cmpxchg是x86CPU架构的汇编指令。它的主要功能是比较和交换操作数。我们继续往下找这条指令的定义。我们会发现,对应不同的os,其底层实现方式是不同的。我们找到Windows的实现方法如下。我们继续往下看。它实际上定义了第216行的代码。当我们找到它时,我们需要汇编指令和寄存器。知识。上面os::is-MP()是多进程操作系统的接口,下面是__asm,这是一个C/C++关键字,用来调用内联汇编器。__asm中的代码是汇编程序。一般来说,dest、exchange_value、compare_value的值都是放在寄存器中的。下面LOCK_IF_MP中代码的大致意思是,如果是多处理器,会执行lock,然后进行比较操作。.其中cmp表示比较,mp表示MultiProcess,je表示相等跳转,L0表示标识位。我们回到上面的汇编指令,我们可以看到CAS的底层是cmpxchg指令。乐观锁,你有没有这个疑问,为什么AtomicInteger可以拿到当前值,那为什么会出现expectValue和value不一致的情况呢?因为AtomicInteger只是一个原子工具类,它是不排他的,它不像synchronized或者Lock一样有互斥和排他性。还记得AtomicInteger中有get和set两个方法吗?它们只是用volatile修饰的,而volatile不是原子的,所以可能会出现expectValue和value当前值不一致的情况。因此可能会出现重复修订。针对上述情况有两种解决方案。一种是使用synchronized、lock等类似的锁机制。这个锁是独占的,也就是说同一时间只有一个线程可以修改它。可以保证原子性,但是相对开销比较大。这种锁也称为悲观锁。另一种解决方案是使用版本号或CAS方法。“版本号”版本号机制是通过在数据表中增加一个版本字段来实现的,表示数据被修改的次数。当执行写操作并且写入成功时,version=version+1。当线程A要更新数据时,读取数据时,version值也会同时被读取。提交更新时,如果刚刚读取的版本值等于当前数据库中的版本值,则进行更新。否则,重试更新操作,直到更新成功。“CAS方法”的另一种方式是CAS。上面我们花了很多篇幅介绍了CAS方法,相信大家现在已经对它的运行机制有了一定的了解,我们就不再细说它的运行机制了。.任何事物都有优点和缺点。软件行业没有完美的解决方案,只有最优的解决方案,所以乐观锁也有它的弱点和缺陷,这就是ABA问题。ABA问题ABA问题是如果第一次读取一个变量的值为A,准备写入A的时候,发现值还是A,那么这种情况下,可以认为A的值没有改变。它改变了吗?可以是A->B->A,但是AtomicInteger不这么认为,它只相信它所看到的,它所看到的就是它所看到的。比如现在有一个单向链表,如下图,A.next=B,B.next=null,此时两个线程T1和T2分别从单向链表中取出A,由于某些特殊原因,T2将A更改为B,然后又更改为A。这时T1执行CAS方法,发现单链表还是A,于是执行CAS方法。虽然结果是正确的,但是这个操作会导致一些潜在的问题。这个时候还是单链表。两个线程T1和T2分别从单链表中取出A,然后T1将链表改成ACD。如下图,T2发现内存值还是A,尝试将A的值替换为B,因为B的引用为null,会导致C和D处于空闲状态。JDK1.5之后的AtomicStampedReference类提供了这个能力,compareAndSet方法就是先检查当前值是否等于期望值。判断标准即当前引文和邮戳分别等于预期引文和邮戳,如果都相等,则自动设置为给定的更新值。好了,以上就是Java代码流程。看到native,就知道又要用cpp了。Kailu简单的解释说UnsafeWrapper只是一个wrapper,只是换了个名字而已。然后经过一些JNI处理,因为compareAndSwapOject比较引用,所以需要经过C++面向对象的转换。最重要的方法是atomic_compare_exchange_oop。可以看到,熟悉的词汇cmpxchg又出现了,也就是说compareAndSwapOject还是使用了cmpxchg原子指令,只是经过了一系列的转换。后记提出一个问题,CAS能保证变量之间的可见性吗?为什么?还有一个问题,getIntVolatile方法的cpp源码在哪里?如何找到它?按照下面的二维码。如需转载本文,请联系Java开发者公众号。