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

面试官:说说Atomic原子类的实现原理

时间:2023-03-22 12:58:43 科技观察

面试官:说说Atomic原子类的实现原理有时候,你肯定会想到Atomic类。是的,Atomic相关的类都是线程安全的。在讲Atomic类之前,我想先讲讲“线程安全”的概念。线程安全真的是线程安全吗?乍一看,“线程安全”这个词很容易就按字面意思来理解,这不就是线程安全吗?其实不然,线程本身没有好坏之分,没有“安全线程”和“不安全线程”之分,俗话说:人性本善,线程本性纯善。真正让线程变坏的是它们访问的变量。变量对于操作系统来说其实就是内存块,所以绕过了这么大的圈子,把线程安全称为“内存安全”可能更合适。总之,线程访问的内存决定了线程是否安全。变量大致可以分为局部变量和共享变量。局部变量是JVM的栈空间。每个人都记住了刻板印象。栈是线程私有的,非共享的,自然是内存安全的;共享变量用于JVM。一般存在于堆上,堆上的东西是所有线程共享的。没有任何限制自然是不安全的。因为线程安全的概念已经深入人心,所以后面我们会用线程安全来表达内存安全的含义。那么如何解决这种不安全感呢?方法有很多,比如:加锁,Atomic原子类等。那么,今天我们就来看看Atomic类吧。什么是原子?Java从JDK1.5开始提供了java.util.concurrent.atomic包,其中包含多个原子操作类。原子操作类提供了一种简单、高效且安全的方法来更新变量。Atomic包下有很多原子操作类,大致可以分为四种:原子操作基本类型原子操作数组类型原子操作引用类型原子操作更新属性Atomic原子操作类使用源码中的Unsafe类,以及Unsafe类提供了硬件级的原子操作,可以直接安全地操作内存变量。后面在讲解源码的时候会详细介绍。实现一个计数器如果在业务代码中需要实现一个计数器的功能,点击一下,我们很快就会编写如下代码:当代码提交库代码审查时,啪的一声,很快就收到了审查意见(严重级别):如果是多线程场景,可能是你的计数器有问题。大一的时候老师说count++是非原子的,其实包括三个操作:读取数据,加一,写回数据。再次修改代码。多行访问increase方法会有问题。然后给它加一把锁。如果修改了count变量,其他线程可能无法立即看到它。然后给变量加一个volatile。杭驰杭驰,代码如下:publicclassLockCounter{privatevolatileintcount;publicsynchronizedvoidincrease(){count++;}}操作猛如虎,再次提交代码后,依旧收到审稿意见(建议级):locking会影响效率,可以考虑使用原子操作那种。原子操作?“黑问号脸”,莫非老大知道我晚上有约会故意惩罚我,不想合并到代码里。抱着怀疑的态度,我打开了百度和谷歌。事实证明,AtomicInteger可以轻松解决这个问题。一顿复制粘贴代码的忙碌后,终于可以下班了。publicclassAtomicCounter{privateAtomicIntegercount=newAtomicInteger(0);publicvoidincrease(){count.incrementAndGet();}}AtomicInteger源码分析调用AtomicInteger类的incrementAndGet方法可以实现无锁安全自增。这真太了不起了。下面我带大家分析一下源码。完成了,等不及了,等不及了。打开源码,可以看到定义的incrementAndGet方法:/***在当前值的基础上自动加1**@return更新后的值*/publicfinalintincrementAndGet(){returnunsafe.getAndAddInt(this,valueOffset,1)+1;}通过源码可以看出,其实调用了一个unsafe的方法。不安全的在后面。我们看一下getAndAddInt方法的参数:第一个参数this是对当前对象的引用;第二个参数valueOffset用于记录value在内存中的偏移地址,第三个参数为常量1;在AtomicInteger中定义了一个常量valueOffset和一个变量成员变量value:privatestaticfinalUnsafeunsafe=Unsafe.getUnsafe();privatestaticfinallongvalueOffset;static{try{valueOffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));}catch(Exceptionex){thrownewError(ex);}}privatevolatileintvalue;value变量保存的是当前对象的值,valueOffset是变量的内存偏移地址,也是调用unsafe方法得到的。publicfinalclassUnsafe{//...省略其他方法publicnativelongobjectFieldOffset(Fieldf);}这里说说Unsafe类,顾名思义:不安全类。打开Unsafe类,你会看到大部分方法都标有native,也就是说这些都是本地方法。native方法对操作系统平台有很强的依赖性,一般用C/C++语言编写。当调用Unsafe类的native方法时,实际会执行这些方法,熟悉C/C++的可以下载源码进行研究。好吧,我们回到最开始,调用Unsafe类的getAndAddInt方法:publicfinalclassUnsafe{//...省略其他方法publicfinalintgetAndAddInt(Objecto,longoffset,intdelta){intv;do{v=getIntVolatile(o,offset);//循环CAS操作}while(!compareAndSwapInt(o,offset,v,v+delta));returnv;}//根据内存偏移地址获取当前值publicnativeintgetIntVolatile(Objecto,longoffset);//CAS操作publicfinalnativebooleancompareAndSwapInt(Objecto,longoffset,intexpected,intx);}通过getIntVolatile方法获取当前AtomicInteger对象的值,是一个本地方法。然后调用compareAndSwapInt进行CAS原子操作,尝试将当前值加1,如果CAS失败,会循环重试。因此,compareAndSwapInt方法是核心。具体实现可以自行查找源码。下面我们看一下方法的参数。一共有四个参数:o指的是当前对象;offset是指当前对象值的内存偏移地址;expected是期望值;x是修改后的值;compareAndSwapInt方法的思路是获取对象o和offset后,会取回对象的实际值。如果当前值与之前得到的期望值一致,则认为该值没有被修改,直接将value的值更新为x,从而完成一次CAS操作。操作系统保证CAS操作是原子的。如果当前值与期望值不一致,则说明该值已被修改,将重试CAS操作,直至成功。AtomicInteger类中还有很多其他的方法,比如:decrementAndGet()getAndDecrement()getAndIncrement()accumulateAndGet()//...省略这些方法,实现原理大同小异,希望大家对其他方法举一反三了解其他方法。此外,还有一些其他类,如:AtomicLong、AtomicReference、AtomicIntegerArray等,这里不再赘述,原理类似。谁更好,AtomicLong还是LongAdder?Java在jdk1.8中引入了LongAdder类。和AtomicLong一样,可以实现加减自增自减等线程安全的操作。但在高并发、竞争激烈的场景下,LongAdder效率更高。一步到位,后续有单独的文章介绍。说了半天,可能有些朋友还是一头雾水。Atomic类是如何实现线程安全的?在语言级别,Atomic类不执行任何同步操作。查看源代码方法没有任何锁。其实最大的功劳还是在CAS上。CAS利用操作系统的硬件特性实现原子性,利用CPU的多核能力实现硬件级阻塞。只有CAS的原子性保证一定是线程安全的?当然不是,通过源码发现value变量也用volatile修饰,保证了线程可见性。那么有的朋友可能要问了,锁了没有用吗?不是的,虽然基于CAS的线程安全机制很好很高效,但是只对一些比较细粒度的需求有效。如果遇到非常复杂的业务逻辑,还是需要加锁操作。你学会了吗?

最新推荐
猜你喜欢