温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

非阻塞同步怎么在Java中应用

发布时间:2021-06-11 14:03:16 来源:亿速云 阅读:147 作者:Leah 栏目:开发技术

非阻塞同步怎么在Java中应用?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

一、从硬件原语上理解同步(非特指Java)

同步机制是多处理机系统的重要组成部分,其实现方式除了关系到计算的正确性之外还有效率的问题。同步机制的实现通常是在硬件提供的同步指令的基础上,在通过用户级别软件例程实现的。上面说到的乐观策略实际上就是建立在硬件指令集的基础上的(我们需要实际操作和冲突检测是原子性的),一般有下面的常用指令:测试并设置(test_and_set)、获取并增加(fetch_and_increment)、原子交换(Atomic_Exchange)、比较并交换(CAS)、加载连接条件存储(LL/SC),下面我们会讲到这些以及通过这些硬件同步原语实现的旋转锁和栅栏同步。

1.1、基本硬件原语

在多处理机中实现同步,所需的主要功能是一组能以原子操作读出并修改存储单元的硬件原语。如果没有这种操作,建立基本的同步原语的代价会非常大。基本硬件原语有几种形式提供选择,他们都能以原子操作的方式读改存储单元,并指出进行的操作是否能以原子形式进行,这些原语作为基本构建提供构造各种各样的用户及同步操作。

一个典型的例子就是原子交换(Atomic Exchange),他的功能是将一个存储单元中的值和一个寄存器的值进行交换。我们看看这个原语怎样构造一个我们通常意义上说的简单的锁。

假设现在我们构造这样一个简单的锁:其值为0表示锁是开的(锁可用),为1表示上锁(不可用)。当处理器要给该锁上锁的时候,将对应于该锁的存储单元的值与存放在某个寄存器中的1进行交换。如果别的处理器已经上了锁,那么交换指令返回的值为1否则为0。返回0的时候,因为是原子交换,锁的值就会从0变为1表示上锁成功;返回1,原子交换锁的值还是1,但是返回1表示已经被上了锁。我们考虑使用这个锁:假设两个处理器同时进行交换操作(原子交换),竞争的结果就是,只有一个处理器会先执行成功而得到返回值0,而另一个得到的返回值为1表示已经被上锁。从这些我们可以看出,采用原子交换指令是实现同步的关键:这个原子交换操作的不可再分的,两个交换操作将由写顺序机制确定先后顺序,这也保证了两个线程不能同时获取同步变量锁。

除此之外,还有别的原语可以实现同步(关键都在于能以原子的方式读-改-写存储单元的值)。例如:测试并置定(test_and_set)(先测试一个存储单元的值,如果符合条件就修改其值),另一个同步原语是读取并加1(fetch_and_increment))(返回存储单元的值并自动增加该值)。

那么,上面的基本原语操作又是怎样实现的呢,这在一条指令中完成上述操作显然是困难的(在一条不可中断的指令中完成一次存储器的读改写,而且要求不允许其他的访存操作还要避免死锁)。现在的计算机上采用一对指令来实现上述的同步原语。该指令对由两条特殊的指令组成,一条是特殊的load指令(LL指令),另一条是特殊的store指令(SC)。指令的执行顺序是:如果LL指令指明的存储单元的值在SC对其进行写之前被其他的指令改写过,则第二条指令执行失败,如果在两条指令之间进行切换也会导致执行SC失败,而SC指令将通过返回一个值来指出该指令操作是否成功(如果返回的1表示执行成功,返回0表示失败)。为什么说这对指令相当于原子操作呢,这指的是是所有其他处理器进行的操作或者在这对指令之前执行或者在其后执行,不存在两条指令之间进行,所以在这一对指令之间不存在任何其他处理器改变相应存储单元的值。

下面是一段实现对R1指出的存储单元进行的原子交换操作

try:OR    R3,R4,R0 //R4中为交换值,将该值送入R3

    LL    R2,0(R1) //将0(R1)中的值取到R2

    SC    R3,0(R1) //若0(R1)中的值与R3中的值相同,则置R3的值为1,否则为0

    BEQZ R3,try //R3的值为0表示存失败,转移重新尝试

    MOV R4,R2 //成功,将取出的值送往R4     

最终R4和由R1指向的存储单元值进行了原子交换,在LL和SC之间如果有别的处理器插入并且修改了存储单元的值则SC都会返回0并存入R3中从而重新执行交换操作。下面是实现各个讲到的读取并加1(fetch_and_increment)原语的实现

try:LL    R2,0(R1) //将0(R1)中的值送入R2

    DADDIU    R2,R2,#1 //加1操作(R2+1->R2)

    SC    R2,0(R1) //如果0(R1)中的值和R2中的值相同就置R2的值为1,否则为0

    BEQZ    R2,try //R2的值为0表示存失败,转移到开始出重新执行

上面的指令的执行需要跟踪地址,通常LL指令指定一个寄存器,该寄存器中存放着目的存储单元的地址,这个寄存器称为连接寄存器,如果发生中断切换或者与连接寄存器中的地址匹配的cache块被作废(被别的SC指令访问),则将连接寄存器清零,SC指令则检查它的存储地址和连接寄存器汇中的内容是够匹配,如果匹配则SC指令继续执行,否则执行失败。

1.2、用一致性实现锁

我们现在用上面的原子交换的同步原语实现自旋锁(spin lock)(处理器不停请求获得锁的试用权,围绕该锁反复执行循环程序,直到获得锁)。自旋锁适用于这样的场景:锁被占用时间少,在获得锁之后加锁的过程延迟小。

下面我们考虑使用一种简单的方法实现:将锁变量保存在存储器中,处理器可以不断通过原子交换操作来请求其使用权,比如使用原子交换操作获得其返回值从而直达锁变量的使用情况。释放锁的时候,处理器只需要将说置为0。如下面的程序:使用原子交换操作堆自旋锁进行加锁,其中R1中存放的是自旋锁变量的地址

        DADDIU R2,R0,#1

lockit: EXCH R2,0(R1) //原子交换,获得自旋锁的值并在下面比较自旋锁的值为1还是0,为1表示已经上锁

        BNEZ R2,lockit //若R2的内容不为0,则表示已经有其他程序获得了锁变量,就继续旋转等待

下面我们对这个简单的自旋锁实现进行一些改进(下面说到的可类比JMM内存模型理解)如果计算机支持Cache一致性,就可以将锁调入Cache中(类比本地内存),并通过一致性保证使得锁的值保持和存储器中的值一致(类比内存可见性和本地内存主内存的值一致同步)。这样做有下面的好处:①使得环绕自旋锁的线程(自旋请求锁变量)只对本地Cache中的锁(主存中的副本)进行操作,而不用再每次请求占用锁时候进行一次全局的访存操作(访问主内存存储器中存放的锁的值) ②利用访问锁的程序局部性原理(处理器最近使用的锁可能不久后还会使用),这种情况就可以使得锁驻留在对应的Cache中,大大减少了获得锁所需要的时间(处于性能考虑,需要减少全局访存操作)。

在改进之前,我们应该知道,在上面的简单实现的基础上(上面的每次循环交换均需要一次写操作,因为有多个处理器会同时请求加锁,这就会导致一个处理器请求成功后,其他处理器都会写不命中),需要对这个程序进行改进,使得它只对本地副本中的锁变量进行读取和检测,直到发现锁已经被释放。发现释放之后,立刻去进行交换操作跟别的处理器竞争锁变量。所有这些进程还是以原子交换的方式获得锁,也只有一个进程可以获得成功(获得锁变量成功的进程交换后看到的锁变量值为0,交换之后的锁变量值为1表示上锁成功;而获得失败的进程虽然也交换了锁变量的值,但是因为交换后自己看到的锁变量的值已经是1,就表示自己进程失败了),其他的需要继续旋转等待。当获得锁的进程使用完之后,将锁变量置为0表示释放锁由其他需要获取的进程去竞争它(其他进程会在自己的Cache中发现锁变量的值发生变化,这是上面所说的Cache一致性)。下面是修改后的旋转锁程序

lockit: LD    R2,0(R1) //取得锁的值

        BNEZ R2,lockit //如果锁还没有释放(R2的值还是1)

        DADDIU    R2,R0,#1 //将R2值置为1(这里面可以这样想:上面BNEZ执行失败表示R2值为0,那么这个时候就+1)

        EXCH R2,0(R1) //将R2中的值和0(R1)中的锁变量进行原子交换

        BNEZ R2,lockit //上面第一次判断是当前进程首先发现主存中的锁变量值发生变化;

                       //进行原子交换结果判断和上面一样,如果狡猾后返回值为0表示成功,为1表示失败就继续旋转等待获取

1.3、使用上面的旋转锁实现我们一个同步原语——栅栏同步

首先解释一下什么叫栅栏同步(barrier)。假设有一个类似于栅栏的东西,它会强制所有到达栅栏的进程进行等待,直到全部的进程都到达之后释放所有到达的进程继续往下执行,从而形成同步。下面我们就通过上面说的旋转锁来简单模拟实现这样的一个同步原语

使用两个旋转锁,一个表示计数器,记录已经到达该栅栏的进程数;另一个用来封锁进程知道最后一个进程到达该栅栏。为了实现栅栏,我们需要一个变量,到达并阻塞住的进程需要在这个变量上自旋等待知道满足它需要的条件(都到达栅栏然后才能往下执行)。我们使用spin表示这个条件condition。如下的程序所示,其中lock和unlock提供基本的旋转锁,变量count记录已经到达栅栏的进程数,total表示已经到达栅栏的进程总数,对counterlock加锁保证了增量操作的原子性,release用来封锁最后一个到达栅栏的进程。spin(release==1)表示需要全部进程都到达栅栏。

lock(counterlock); //确保更新的原子性
if(count == 0) release = 0; //第一个进程到达,这时候重置release为0表示在其值变为1之前后续到达的进程都需要等待
count = count + 1; //记录到达的进程数
unlock(counterlock); //释放锁
if(count == total) { //进程全部到达
    count = 0; //重置计数器count
    release = 1; //将release置为1表示释放所欲到达的进程
} else { //进程还没有全部到达
    spin(release == 1); //已经到达的进程旋转等待知道所有的进程到达(言外之意就是release=1)
}

但是上面的这种简单实现还是存在问题的,我们考虑下面这种可能发生的情况:当栅栏的使用在循环当中时候,这时候所有释放的进程在运行一段时间之后还会到达栅栏,假设其中一个进程在上次释放的时候还没有来得及离开栅栏,而是依旧停留在旋转操作上(可能操作系统重新进行进程调度导致那个进程没有来得及离开栅栏)。如果第二次栅栏使用的时候,一个执行较快的进程到达栅栏(这个快的意思是,当他到达栅栏之后上次那个还没有离开栅栏的进程还在旋转操作上),这个快的进程会发现count=0,那么他就会将release置为0,这时候就会导致那个还在旋转等待的进程发现release值为0,然后那就更不会再退出这个旋转操作了,就相当于被捆绑在栅栏上出不去(这个问题会导致后续的count计数少了一个进程到达,而总是小于total),那这样的话,由于count总是小于total那不是所有到达栅栏的进程都在spin上一直自旋了吗。那怎么解决这个问题呢,一种方法就是在进程离开栅栏的时候也进行计数,在上次使用栅栏的进程全部离开栅栏之前不允许执行快的进程再次使用并初始化栅栏的一些变量值。还有一种方法是使用sense_reversing栅栏,即每个进程只用一个本地私有变量local_sense并初始化为1,用它和release判断进程是否需要自旋等待。

二、Java中的原子性操作概述

所谓原子操作,就是指执行一系列操作的时候,要么全部执行要么全部不执行,不存在只执行一部分的情况。在设置计数器的时候一般是读取当前的值,然后+1在更新(读-改-写的过程),如果不能保证这这几个操作的过程的原子性就可能出现线程安全问题,比如下面的代码示例,++value在没有任何额外保证的前提下不是原子操作。

public class ThreadUnSafe{
    private Long value;

    public Long getValue() {return value;}

    public void increment() {++value;}
}

使用Javap -c XX.class查看汇编代码如下

非阻塞同步怎么在Java中应用

这是个复合操作,是不具备原子性的。而保证这个操作原子性的方法最简单的就是加上synchronized关键字,使用synchronized可以实现线程安全性,但是这是个独占锁,没有获取内部锁的线程会被阻塞住(即便是这里的getValue操作,多线程访问也会阻塞住),这对于并发性能的提高是不好的(而这里也不能简单的去掉getValue上的synchronized,因为读操作需要保证value的读一致性,即需要获得主内存中的值而不是线程工作内存中的可能是旧的副本值)。那么除了加锁之外其他安全的方法?后面讲到的原子类(使用CAS实现)就可以作为一个选择。

三、Java中的CAS操作概述

Java中提供非阻塞的volatile关键字解决保证共享变量的可见性问题,但是不能解决部分符合操作不具备原子性的问题(比如自增运算)。CAS即CompareAndSwap是JDK提供的非阻塞原子操作,通过硬件保证比较更新的原子性。我们通过compareAndSwapLong来简单介绍CAS:

compareAndSwapLong(Object obj, long valueOffset, long expect, long update),该方法中compareAndSwap表示比较并交换,方法中有四个操作数,其中obj表示对象内存的位置,valueOffset表示对象中存储变量的偏移量,expect表示变量的预期值,update表示更新值。操作含义就是,若果对象obj中内存偏移量为valueOffset的变量值为expect则使用心得update值替换旧的值expect,这是处理器提供的一个原子指令。这些方法有sun.misc.Unsafe类提供。后面我们会说到Unsafe类

在此之前我们先说一下CAS操作的一个经典的ABA问题:假如线程1 使用CAS修改初始值为A的变量X,那么线程1会首先回去当前变量X的值(A),然后使用CAS操作尝试修改X的值为B,如果使用CAS修改成功了,那么程序一定执行正确了吗?在往下的假设看,如果线程I在获取变量X的值A后,在执行CAS之前线程II使用CAS修改变量X的值为B然后由修改回了A。这时候虽然线程I执行CAS时候X的值依旧是A但是这个A已经不是线程I获取时候的A了,这就是ABA问题。ABA产生的原因是变量的状态值产生了环形转换,即变量值从A->B,然后又从B->A。jdk中提供了带有标记的原子类AtomicStampedReference(时间戳原子引用)通过控制变量的版本保证CAS的正确性。如下所做的测试ABA问题以及使用AtomicStampedReference来解决这个问题

3.1、模拟ABA问题

下面的程序输出结果会是这样的

非阻塞同步怎么在Java中应用

package test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class TestAtomicStampedReference {

    static AtomicReference<Integer> atomicReference = new AtomicReference<>(1);

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicReference.compareAndSet(1,2);
                atomicReference.compareAndSet(2,1);
                System.out.println(Thread.currentThread() + "线程修改后的变量值" + atomicReference.get());
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //sleep 1秒,保证线程t1完成1->2->1的模拟ABA操作
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicReference.compareAndSet(1,3);
                System.out.println(Thread.currentThread() + "线程修改后的变量值" + atomicReference.get());
            }
        });

        t1.start();
        t2.start();
    }
}

3.2、使用AtomicStampedReference重新实现

下面是运行结果

非阻塞同步怎么在Java中应用

package test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class TestAtomicStampedReference {

    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1); //定义初始值和初始版本号
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程1获得初始版本号并sleep1秒
                int version = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread() + "当前线程获得的版本号" + version);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "修改变量结果true/false?:" +
                        atomicStampedReference.compareAndSet(10,11,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)
                        + "修改后的结果:" + atomicStampedReference.getReference());
                System.out.println(Thread.currentThread() + "修改变量结果true/false?:" +
                        atomicStampedReference.compareAndSet(11,10,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)
                        + "修改后的结果:" + atomicStampedReference.getReference());
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //首先获得初始版本号,sleep2秒让线程1完成10->11->10的模拟ABA操作
                int version = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread() + "当前线程获得的版本号" + version);
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "修改变量结果true/false?:" +
                        atomicStampedReference.compareAndSet(10,20,version,atomicStampedReference.getStamp()+1)
                        + "修改后的结果:" + atomicStampedReference.getReference());
            }
        });

        t1.start();
        t2.start();
    }
}

四、Java中的Unsafe类

JDK中的rt.jar包中的Unsafe类提供了硬件级别的原子性操作

非阻塞同步怎么在Java中应用

Unsafe类中许多方法都是native方法,他们使用JNI的方式访问本地C++中的实现库。下面我们了解一下Unsafe类提供的几个主要的方法以及如何使用unsafe类进行一些编程操作。

4.1、Unsafe类中的重要方法介绍

(1)public native long objectFieldOffset(Field var1):返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时候使用。如下使用Unsafe类获取变量value在Atomic对象中的内存偏移量

 非阻塞同步怎么在Java中应用

(2)public native int arrayBaseOffset(Class<?> var1):获取数组中第一个元素的地址

(3)public native int arrayIndexScale(Class<?> var1):获取数组中一个元素占用的字节

(4)public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5):比较对象var1中的偏移量为var2的变量的值是否与var4相同,相同则使用var6的值更新,并返回true,否则返回false。

(5)public native long getLongVolatile(Object var1, long var2):获取对象var1中偏移量为offset的变量对应volatile语义的值。

(6)public native void putLongVolatile(Object var1, long var2, long var4):设置var1对象中offset偏移类型为long的值为var4,支持volatile语义

(7)public native void putOrderedLong(Object var1, long var2, long var4):设置对象obj中offset偏移地址对应的long型的field的值为value。这是一个有延迟的putLongVolatile方法,并且不保证对应的值类型的修改对其他线程可见,只有变量在只用volatile修饰并且预计会被意外修改的时候才会使用该方法、

(8)public native void park(boolean var1, long var2):阻塞当前线程,其中参数var1等于false且var2等于0表示一直阻塞,var2大于0表示等待指定的时间后阻塞线程会被唤醒。这个var的值是相对的,为一个增量值,也就是相当当前时间累加事假后当前线程就会被唤醒。如果var1位true,并且var2大于0,则表示阻塞的线程到指定的时间点后就会被唤醒,这里的时间var2是个绝对时间,是某个时间点换算为ms后的值。

(9)public native void unpark(Object var1):唤醒调用park方法之后的线程。

下面是jdk8之后新增加的,我们列出Long类型的方法

(10)getAndSetLong()方法:获取当前对象var1中偏移量为var2的变量volatile语义的当前值,并设置变量volatile语义的值为var4。

首先使用getLongVolatile获取当前变量的值,然后使用CAS原子操作设置新的值。这里使用while是当CAS失败时候进行重试。

 非阻塞同步怎么在Java中应用

(11)getAndAddLong()方法:获取对象var1中偏移量为var2变量的volatile语义的值,设置变量值为原始值+var4

 非阻塞同步怎么在Java中应用

4.2、Unsafe类的使用

考虑编写出下面的程序,并在自己的IDE中运行下面的程序,观察结果。

package test;

import sun.misc.Unsafe;

public class TestUnsafe {

    //获取Unsafe的实例
    static Unsafe unsafe = Unsafe.getUnsafe();

    //记录变量value在TestUnsafe中的偏移量
    static long valueState;

    //变量
    private volatile long value;

    static {
        try {
            //获取value变量在TestUnsafe类中的偏移量
            valueState = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("value"));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public static void main(String[] args) {
        TestUnsafe testUnsafe = new TestUnsafe();
        System.out.println(unsafe.compareAndSwapInt(testUnsafe,valueState,0,1));
    }
}

上面的程序中首先获取Unsafe的一个实例,然后使用unsafe的objectFieldOffset方法获取TestUnsafe类中value变量,计算在TestUnsafe类中value变量的内存偏移地址并保存到valueState中。main中调用unsafe的compareAndSwapInt方法设置testUnsafe对象的value变量的值为1(如果是0的话)。value初始默认是0,我们希望代码能输出true(即compareAndSwapInt能够执行成功),但是最终运行时下面的结果

非阻塞同步怎么在Java中应用

我们看到上面的异常报错在getUnsafe方法位置,下来我们看一看getUnsafe方法

public static Unsafe getUnsafe() {
    //(1)获取调用getUnsafe类的这个Class类,按照上面的程序中的TestUnsafa类
    Class var0 = Reflection.getCallerClass();
    //(2)看下面的那个方法
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
/**
 * (3)判断是不是启动类加载器加载的类,即看看是不是由BootStrapClassLoader加载的TestUnsafe.class,
 *    由于我们这是一个简单测试类,是由应用程序类加载器AppClassLoader加载的,所以直接报出SecurityException异常
 */
public static boolean isSystemDomainLoader(ClassLoader var0) {
    return var0 == null;
}

由于Unsafe类rt.jar包提供的,该包下面的类都是通过Bootstrap类加载器加载的,而我们使用的main方法所在的类是由AppClassLoader加载的,所以在main方法中加载Unsafe类的时候根据双亲委派机制会委托给Bootstrap加载。那么如果想要使用Unsafe类应该怎样使用呢,《深入理解java虚拟机》中这一块告诉我们可以使用反射来使用,下面我们来试一下

package test;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class TestUnsafe2 {

    static Unsafe unsafe;

    static long valueOffset;

    private volatile long value = 0;

    static {
        try {
            //使用反射获取Unsafe的成员变量theUnsafe
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            //设置为课存取
            field.setAccessible(true);
            //设置该变量的值
            unsafe = (Unsafe) field.get(null);
            //获取value偏移量
            valueOffset = unsafe.objectFieldOffset(TestUnsafe2.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TestUnsafe2 test = new TestUnsafe2();
        System.out.println("修改变量结果true/false?:" +
                unsafe.compareAndSwapInt(test,valueOffset,0,1)
                + "修改后的结果:" + test.value);
    }
}

得到下面的结果:

非阻塞同步怎么在Java中应用

五、JUC中原子操作类AtomicLong的原理探究

5.1、原操作类概述

JUC包中提供了很多原子操作类,这些类都是通过上面说到的非阻塞CAS算法来实现的,相比较使用锁来实现原子性操作CAS在性能上有很大提高。由于原子操作类的原理都大致相同,所以下面分析AtomicLong类的实现原理来进一步了解原子操作类。

5.2、AtomicLong的源码

下面是AtomicLong原子类的部分源码,其中主要包含其成员变量以及一些静态代码块和构造方法

public class AtomicLong extends Number implements java.io.Serializable {

    //(1)获取Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    //(2)保存value值的偏移量
    private static final long valueOffset;

    //(3)判断当前JVM是否支持Long类型的无锁CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
    private static native boolean VMSupportsCS8();

    static {
        try {
            //(4)获取value值在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //(5)实际存的变量值value
    private volatile long value;

    //构造方法
    public AtomicLong(long initialValue) {
        value = initialValue;
    }
}

在上面的部分代码中,代码(1)通过Unsafe.getUnsafe()方法获取到Unsafe类的实例(AtomicLong类也是rt.jar包下面的,所以AtomicLong也是通过启动类加载器进行类加载的)。(2)(4)两处是计算并保存AtomicLong类中存储的变量value的偏移量。(5)中的value被声明为volatile的,这是为了在多线程下保证内存的可见性,而value就是具体存放计数的变量。下面我们看看AtomicLong中的主要几个函数

(1)递增和递减的源码

//使用unsafe的方法,原子性的设置value值为原始值+1,返回值为递增之后的值
public final long getAndIncrement() {
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}
public final long incrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
//使用unsafe的方法,原子性的设置value值为原始值-1,返回值为递减之后的值
public final long getAndDecrement() {
    return unsafe.getAndAddLong(this, valueOffset, -1L);
}
public final long decrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}

在上面的代码中都是通过调用Unsafe类的getAndAddLong方法来实现操作的,我们来看看这个方法,这个方法是个原子性操作:其中的第一个参数是AtomicLong实例的引用,第二个参数是value变量在AtomicLong中的偏移量,第三个参数是要设置为第二个变量的值。下面就是getAndAddLong方法的实现,以及一些分析

public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        //public native long getLongVolatile(Object var1, long var2);
        //该方法就是获取var1引用指向的内存地址中偏移量为var2位置的值,然后赋给var6
        var6 = this.getLongVolatile(var1, var2);
    /**public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
     * var1:AtomicXXX类型的一个引用,指向堆内存中的一块地址
     * var2:AtomicXXX源码中的valueOffset,表示AtomicXXX源码中实际存储的值value在原子类型内存中的地址偏移量
     * var4:要比较的目标值expectValue,如果从内存指定地址处(var1和var2决定的那块地址)的值和该值相等,则CAS成功
     * var6:CAS成功后向该内存中写进的新值
     */
    //该方法就是使用CAS的方式,比较指定内存地址处(var1指向的内存地址块中偏移量为var2处)的值和上面同一块地址处取出的var6是否相等,
    //相等就将var6+var4(这里可以看成var6+1)和指定内存地址处(var2引用指向的地址块中偏移量为var2处)的值交换,并返回true,然后就会结束循环
    //CAS失败返回false,然后继续执行循环体内部的代码,直到成功(也就是自增运算成功就会跳出循环并返回自增后的值)
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

    return var6;
}

(2)CompareAndSet方法

下面是compaerAndSet方法的实现,主要还是调用unsafe类的compareAndSwapLong方法,其原理和上面分析的差不多,都是通过CAS的方式进行比较交换值。

public final boolean compareAndSet(long expect, long update) {
    //public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

(3)扩展,下面是compareAndSwapInt的底层实现,实际上是通过硬件同步原语来实现的CAS,下面的cmpxchg就是基于硬件原语实现的

UNSAFE_ENTRY(jboolean,Usafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UsafeWrapper("Usafe_CompareAndSwapInt");
oop p = JNIHasdles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x,addr,e)) == e;
UNSAFE_END

(4)下面是一个例子,使用AtomicLong来进行技术运算

package test;

import java.util.concurrent.atomic.AtomicLong;

public class TestAtomic1 {

    //创建AtomicLong类型的计数器
    private static AtomicLong atomicLong = new AtomicLong();
//    private static Long atomicLong = 0L;
    //创建两个数组,计算数组中的0的个数
    private static Integer[] arr1 = {0,1,2,3,0,5,6,0,56,0};
    private static Integer[] arr2 = {10,1,2,3,0,5,6,0,56,0};

    public static void main(String[] args) throws InterruptedException {

        //线程1统计arr1中0的个数
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                int size = arr1.length;
                for (int i = 0; i < size; i++) {
                    if(arr1[i].intValue() == 0) {
//                        atomicLong.getAndIncrement();
                        atomicLong++;
                    }
                }
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                int size = arr2.length;
                for (int i = 0; i < size; i++) {
                    if(arr2[i].intValue() == 0) {
//                        atomicLong.getAndIncrement();
                        atomicLong++;
                    }
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("两个数组中0出现的次数为: " + atomicLong);//两个数组中0出现的次数为: 7
    }
}

如果没有使用原子类型进行计数运算,那么可能就是下面的结果

  非阻塞同步怎么在Java中应用

看完上述内容,你们掌握非阻塞同步怎么在Java中应用的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注亿速云行业资讯频道,感谢各位的阅读!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI