浅析从同步原语看非阻塞同步以及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查看汇编代码如下

这是个复合操作,是不具备原子性的。而保证这个操作原子性的方法最简单的就是加上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问题

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

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重新实现

下面是运行结果

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类提供了硬件级别的原子性操作

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

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

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

 

(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失败时候进行重试。

 

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

 

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能够执行成功),但是最终运行时下面的结果

我们看到上面的异常报错在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);
    }
}

得到下面的结果:

五、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中的应用的详细内容,更多关于同步 非阻塞同步的资料请关注我们其它相关文章!

时间: 2021-06-10

java同步锁的正确使用方法(必看篇)

同步锁分类 对象锁(this) 类锁(类的字节码文件对象即类名.class) 字符串锁(比较特别) 应用场景 在多线程下对共享资源的安全操作. 需求:启动5个线程对共享资源total进行安全操作. 同步锁在多线程单例模式下的使用 以上三类同步锁都可以. package cn.myThread; public class MyThread implements Runnable { private static int total = 10; @Override public void run()

详解java 三种调用机制(同步、回调、异步)

1:同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用 2:回调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口: 3:异步调用:一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口 具体说来:就是A类中调用B类中的某个方法C,然后B类中反过来调用A类中的方法D,D这个方法就叫回调方法, 实例1:使用java中Timer来在给定时间间隔发送通知,每隔十秒打印一次数据 Tim

java 中同步方法和同步代码块的区别详解

java 中同步方法和同步代码块的区别详解 在Java语言中,每一个对象有一把锁.线程可以使用synchronized关键字来获取对象上的锁.synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁). 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问

Java同步容器和并发容器详解

同步容器 在 Java 中,同步容器主要包括 2 类: Vector.Stack.HashTableCollections 类中提供的静态工厂方法创建的类(由 Collections.synchronizedXxxx 等方法) Collections类中提供的静态工厂方法创建的类 Vector 实现了 List 接口,Vector 实际上就是一个数组,和 ArrayList 类似,但是Vector 中的方法都是 synchronized 方法,即进行了同步措施. Stack 也是一个同步容器,它

Java中的同步与异步详细介绍

进程同步用来实现程序并发执行时候的可再现性. 一.进程同步及异步的概念 1.进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.也就是必须一件一件事做,等前一件做完了才能做下一件事.就像早上起床后,先洗涮,然后才能吃饭,不能在洗涮没有完成时,就开始吃饭.按照这个定义,其实绝大多数函数都是同步调用(例如sin,isdigit等).但是一般而言,我们在说同步.异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务.最常见的例子就是 sendmessage.该函数发送一个消

java 中同步、异步、阻塞和非阻塞区别详解

java 中同步.异步.阻塞和非阻塞区别详解 简单点说: 阻塞就是干不完不准回来,一直处于等待中,直到事情处理完成才返回: 非阻塞就是你先干,我先看看有其他事没有,一发现事情被卡住,马上报告领导. 我们拿最常用的send和recv两个函数来说吧... 比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话

java 线程之对象的同步和异步(实例讲解)

一.多线程环境下的同步与异步 同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去. package com.jalja.org.thread.demo01; public class Thread02 { public synchronized void method1(){ System.out.println("method1:"+Thread.currentThread().getName()); try { T

Java多线程按指定顺序同步执行

笔者今天看到一个有趣的面试题,如何让多个线程按照既定的顺序依次执行?比如每个线程输出一个整数, 那么期望就是这样的:0,1,2,3,4,5,6,7,8,9. 而不是0,2,4,1,3,5,8,7,9,6 乍一看,这不是反人性的考题吗?多线程本来就以乱序执行出名的.稍加思索,想到3种解决方案,分别用代码实现之. 方法1 使用newSingleThreadExecutor newSingleThreadExecutor返回仅仅包含一个线程的线程池,将多个任务交给此Executor时,这个线程池处理完

Java并发编程之原子变量与非阻塞同步机制

1.非阻塞算法 非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 -- 例如比较和交换.非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御.使用底层的原子化机器指令取代锁,比如比较并交换(CAS,compare-and-swap). 2.悲观技术 独占锁是一种悲观的技术.它假设最坏的情况发生(如果不加锁,其它线程会破坏对象状态),即使没有发生最坏的情况,仍然用锁保护对象状态.

java 实现增量同步和自定义同步的操作

场景 公司有多个系统. 需要从某个系统拿数据.初始化拿一次,然后每天增量同步. 因为逻辑关系有些复杂,且涉及到多表,所以纯sql不太方便. 项目之前的mybatis写死了, sysdate-1的数据. 如何全量同步 老办法 可以让某系统调整modifyDate,本系统通过定时任务同步这些数据. 缺点: 按规定不允许改数据,别人也不乐意改. 较好的办法 调整mybatis,传入参数变为自定义方法,这样就可以同步任意时刻的数据. 一般有2种方法: 传入游标方法: sysdate-n 直接传入日期字符

Java并发编程中构建自定义同步工具

当Java类库没有提供适合的同步工具时,就需要构建自定义同步工具. 可阻塞状态依赖操作的结构 复制代码 代码如下: acquir lock on object state;//请求获取锁 while(precondition does not hold){//没有满足前提条件    release lock;//先释放锁    wait until precondition might hold;//等待满足前提条件    optionlly fail if interrupted or tim

ASP.NET之自定义同步HTTP处理程序(图文教程)

一.前言 说来这片简单的教程还真是碰巧,是刚好我在写另一个页指令的时候看到的.就跟着进来看了看是什么东西,发现比我们之前只会用.ashx,并且只能在申请这个文件才可以进行响应.这些似乎让它变不灵活,不能让我们灵活的应用.而自定义HTTP处理程序可以完全消除上面的问题,它只是一个.cs的类文件,并且你只要按照很简单的准备工作就可以获得很多的灵活度: 1.可以自定义用户访问何网页时响应该文件并把数据呈现发送给客户端 2.可以对所有具有一致的后缀名的请求文件定义一个处理程序 3.具有同步与异步选择 我

详解java中保持compareTo和equals同步

详解java中保持compareTo和equals同步 摘要 : 介绍重写equlas()和comparable接口,两者进行不相同的判断.从而使两者的对应的list.indexOf()与 Collections.binarySearch()得到的不一样. 在Java中我们常使用Comparable接口来实现排序,其中compareTo是实现该接口方法.我们知道compareTo返回0表示两个对象相等,返回正数表示大于,返回负数表示小于.同时我们也知道equals也可以判断两个对象是否相等,那么

浅谈同步监视器之同步代码块、同步方法

如果有多个线程访问共享资源,可能会出现当一个线程没有处理完业务,然后另一个线程进入,从而导致共享资源出现不安全的情况. 日常例子:银行取钱,A和B有拥有同一个银行账户,A用存折在柜台取钱,B在取款机取钱.取钱有两个关键步骤: (1)判断账户里的钱的余额是否大于所取钱数 (2)如果大于所取钱数,则账户最终所剩余额 = 余额 - 所取钱数. 如果没有线程同步的情况下,我们假设这一种情况,这个共同的账户里共1000元. (1)A B同时去取600元,A所在线程执行到上面的第一个步骤,判断所取钱数小于现

idea maven 项目src下的配置文件没有同步至target的解决操作

如果发现maven项目里面src/main/resources下的配置文件或src/main/java下的xml配置文件未能同步至target,首先,要确保src下的文件夹职能划分好,如下图,请注意java, resources, webapp这些目录的图标,如果不是这样,请选中后右键->make directory as来指明文件夹用途. 在下面这里分别指出web.xml的位置和web root的位置. 这个编译后输出的路径用默认的, 不要改变 . 重点是pom.xml,如果你的pom里面没有

Java 中模仿源码自定义ArrayList

Java 中模仿源码自定义ArrayList 最近看了下ArrayList的源码,抽空根据ArrayList的底层结构写了一个功能简单无泛型的自定义ArrayLsit,帮助自己更好理解ArrayList:,其实现的底层数据结构为数Object组,代码如下: /** * 自己实现一个ArrayList * */ public class MyArrayList { private Object[] elementData; private int size; public int size(){

java application maven项目打自定义zip包实例(推荐)

1.配置pom.xml文件,添加build节点 <build> <!-- 输出的包名 --> <finalName>p2p</finalName> <sourceDirectory>src/main/java</sourceDirectory> <resources> <!-- 控制资源文件的拷贝(默认复制到classes目录,最后打进jar包) --> <resource> <directo

详解JAVA使用Comparator接口实现自定义排序

1.原则 Comparator接口可以实现自定义排序,实现Comparator接口时,要重写compare方法: int compare(Object o1, Object o2) 返回一个基本类型的整型 如果要按照升序排序,则o1 小于o2,返回-1(负数),相等返回0,01大于02返回1(正数) 如果要按照降序排序,则o1 小于o2,返回1(正数),相等返回0,01大于02返回-1(负数) import java.util.ArrayList; import java.util.Compar

Java实现微信公众号自定义菜单的创建方法示例

本文实例讲述了Java实现微信公众号自定义菜单的创建方法.分享给大家供大家参考,具体如下: 开发公众号的时候可能需要给一些自定义菜单添加事件,比如点击某菜单然后服务端给用户推送信息. 我们也可以使用微信提供的网页调试工具直接生成菜单,不过需要考虑是否影响用户问题. 注意:使用代码自定义菜单事件,手动添加的菜单会失效!谨慎使用! 官方文档 httpClientUtil工具类需要的话可以在前面微信公众号获取access_token中获取 public class WechatDIYMenuTest