Java中的无锁编程

无锁的介绍

无锁的概念可参考前面的《高并发的基本概念》的介绍。无锁的特点

  1. 无障碍的;
  2. 保证有一个线程胜出;
  3. 如果临界区的每个线程在每次竞争中都无法胜出那么该线程即将被饿死。

Java当中提供了一些有关无锁类的使用,在底部使用比较交换指令来实现。一般来说有锁的方式,会导致线程可能会阻塞、挂起,在进入临界区之前由系统对它进行阻塞和挂起,相对来讲无锁的性能会更好些,除非是人为的挂起线程,否则通过无锁的方式线程是不可能被挂起的只会不断的重试。如果线程被挂起,做一次线程的上下文切换可能需要8万个时钟周期,但是如果做重试的操作(比如循环体),除非重试的操作过多,否则一般基本上无锁的操作比有锁的方式要好很多。

无锁的原理

CAS(Compare And Swap/Set)比较并交换算法

我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。

在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

CPU指令对CAS的支持

或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

CAS的CPU指令是cmpxchg指令代码如下:

/*
accumulator = AL, AX, or EAX, depending on whether
a byte, word, or doubleword comparison is being performed
*/
if(accumulator == Destination) {
ZF = 1;
Destination = Source;
}
else {
ZF = 0;
accumulator = Destination;
}

目标值和寄存器里的值相等的话,就设置一个跳转标志,并且把原始数据设到目标里面去。如果不等的话,就不设置跳转标志了。

Unsafe类

  在jdk1.8中无法查看Unsafe源码,请参考http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/5b86f66575b7/src/share/classes/sun/misc 或者我们可以将openjdk8的源码下载后加载到IDE中,sun.misc.Unsafe是JDK内部用的工具类。它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。该类不应该在JDK核心类库之外使用。

  JVM的实现可以自由选择如何实现Java对象的“布局”,也就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。sun.misc.Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java对象的某个字段。每个JVM都有自己的方式来实现Java对象的布局。

unsafe类的源码

获取Unsafe实例静态方法:

private Unsafe() {}
    private static final Unsafe theUnsafe = new Unsafe();
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

底层native方法(底层C++)

    //扩充内存
    public native long reallocateMemory(long address, long bytes);

    //分配内存
    public native long allocateMemory(long bytes);

    //释放内存
    public native void freeMemory(long address);

    //在给定的内存块中设置值
    public native void setMemory(Object o, long offset, long bytes, byte value);

    //从一个内存块拷贝到另一个内存块
    public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);

    //获取值,不管java的访问限制,其他有类似的getInt,getDouble,getLong,getChar等等
    public native Object getObject(Object o, long offset);

    //设置值,不管java的访问限制,其他有类似的putInt,putDouble,putLong,putChar等等
    public native void putObject(Object o, long offset);

    //从一个给定的内存地址获取本地指针,如果不是allocateMemory方法的,结果将不确定
    public native long getAddress(long address);

    //存储一个本地指针到一个给定的内存地址,如果地址不是allocateMemory方法的,结果将不确定
    public native void putAddress(long address, long x);

    //该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的
    public native long staticFieldOffset(Field f);

    //报告一个给定的字段的位置,不管这个字段是private,public还是保护类型,和staticFieldBase结合使用
    public native long objectFieldOffset(Field f);

    //获取一个给定字段的位置
    public native Object staticFieldBase(Field f);

    //确保给定class被初始化,这往往需要结合基类的静态域(field)
    public native void ensureClassInitialized(Class c);

    //可以获取数组第一个元素的偏移地址
    public native int arrayBaseOffset(Class arrayClass);

    //可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用, 可以定位数组中每个元素在内存中的位置
    public native int arrayIndexScale(Class arrayClass);

    //获取本机内存的页数,这个值永远都是2的幂次方
    public native int pageSize();

    //告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类
    public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);

    //定义一个类,但是不让它知道类加载器和系统字典
    public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);  

    //锁定对象,必须是没有被锁的
    public native void monitorEnter(Object o);

    //解锁对象
    public native void monitorExit(Object o);

    //试图锁定对象,返回true或false是否锁定成功,如果锁定,必须用monitorExit解锁
    public native boolean tryMonitorEnter(Object o);

    //引发异常,没有通知
    public native void throwException(Throwable ee);

    //CAS,如果对象偏移量上的值=期待值,更新为x,返回true.否则false.类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等.
    public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object x);

    // 该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。类似的方法有getIntVolatile,getBooleanVolatile等等
    public native Object getObjectVolatile(Object o, long offset);

    //线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。
    public native void park(boolean isAbsolute, long time);

    //终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,也正是使用这两个方法
    public native void unpark(Object thread);

    //获取系统在不同时间系统的负载情况
    public native int getLoadAverage(double[] loadavg, int nelems);

    //创建一个类的实例,不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,我们也可以通过这个方法创建它的实例,对于单例模式,简直是噩梦,哈哈
    public native Object allocateInstance(Class cls) throws InstantiationException;

JAVA中的无锁类

AtomicInteger

AtomicInteger和Integer一样,都继承与Number类。当然还有AtomicBoolean,AtomicLong等等,都大同小异。

public class AtomicInteger extends Number implements java.io.Serializable{
 //通过volatile关键字来保证多线程间数据的可见性的。在没有锁的机制下可能需要借助volatile,保证线程间的数据是可见的(共享的)。这样才获取变量的值的时候才能直接读取。
 private volatile int value;
}

compareAndSet利用JNI来完成CPU指令的操作。compareAndSwapInt就是借助C来调用CPU底层指令实现的。

public final boolean compareAndSet(int expect, int update) {
   return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

这里来解释一下unsafe.compareAndSwapInt方法,他的意思是,对于this这个类上的偏移量为valueOffset的变量值如果与期望值expect相同,那么把这个变量的值设为update。

static {
 try {
   //获取字段value相对于对象的起始内存地址的字节偏移量,因为是相对偏移量,所以它跟某个具体对象没太大关系,而跟class的定义和虚拟机的内存模型的实现细节更相关。
   valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
 } catch (Exception ex) { throw new Error(ex); }
}

CAS是有可能会失败的,但是失败的代价是很小的,所以一般的实现都是在一个无限循环体内,直到成功为止。

public final int getAndIncrement() {
 for (;;) {
  int current = get();
  int next = current + 1;
  if (compareAndSet(current, next))
  return current;
 }
 }

AtomicReference

前面已经提到了AtomicInteger,AtomicReference是一种模板类,它可以用来封装任意类型的数据。

public class AtomicReference<V>  implements java.io.Serializable
public class AtomicReferenceTest {
    public final static AtomicReference<String> atomicString = new AtomicReference<>("dawn");
    public static void main(String[] args)
    {
        for (int i = 0; i < 10; i++)
        {
            new Thread(()->{
                try {
                    Thread.sleep(Math.abs((int)Math.random()*100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (atomicString.compareAndSet("dawn", "ztk"))
                {
                    System.out.println(Thread.currentThread().getName() + "Change value: "+atomicString);
                }else {
                    System.out.println(Thread.currentThread().getName() + "Failed: "+atomicString);
                }
            }).start();
        }
    }
}

只有一个线程能够修改成功,其它的线程都不能再修改。

AtomicStampedReference

CAS操作带来的ABA问题。ABA在一些敏感的场合是不适合的,比如个一个账户充了10块钱,又消费了10元,随后又充值了10元,虽然该账户最余额是10元但不能说该账户没有消费。因为CAS在比操作值得时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,中间变成B,最后又变成A,那么使用CAS进行检查时会发现值没有发生变化,但是实际上是变化了的。解决思路就是添加版本号。 JDK1.5之后,Atomic包提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。其内部实现一个Pair类来封装值和时间戳。

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

这个类的主要思想是加入时间戳来标识每一次改变。当期望值等于当前值,并且期望时间戳等于现在的时间戳时,才写入新值,并且更新新的时间戳。

public boolean compareAndSet(V expectedReference,
     V newReference,
     int expectedStamp,
     int newStamp) {
 Pair<V> current = pair;
 return
  expectedReference == current.reference &&
  expectedStamp == current.stamp &&
  ((newReference == current.reference &&
  newStamp == current.stamp) ||
  casPair(current, Pair.of(newReference, newStamp)));
 }

充值案例,要求money只允许充值一次。

public class AtomicStampedReferenceTest {


    public static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19, 0);

    public static void main(String[] args)
    {
        for (int i = 0; i < 3; i++)
        {
            final int timestamp = money.getStamp();
            new Thread(()->{
                while (true){
                    Integer m = money.getReference();
                    if (m < 20){
                        if (money.compareAndSet(m, m + 20, timestamp,timestamp + 1)){
                            System.out.println("充值成功,余额:"+ money.getReference());
                            break;
                        }
                    }
                    break;
                }
            }).start();
        }

        new Thread(()->{
            for (int i = 0; i < 100; i++){
                while (true){
                    int timestamp = money.getStamp();
                    Integer m = money.getReference();
                    if (m > 10){
                        if (money.compareAndSet(m, m - 10, timestamp,timestamp + 1)){
                            System.out.println("消费10元,余额:"+ money.getReference());
                            break;
                        }
                    }
                    break;
                }
                try{
                    Thread.sleep(100);
                }
                catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

AtomicIntegerArray

与AtomicInteger相比,数组的实现不过是多了一个下标。它的内部只是封装了一个普通的array里面有意思的是运用了二进制数的前导零来算数组中的偏移量。shift = 31 - Integer.numberOfLeadingZeros(scale);前导零的意思就是比如8位表示12,00001100,那么前导零就是1前面的0的个数,就是4。具体偏移量如何计算,这里就不再做介绍了。

private final int[] array;
public final boolean compareAndSet(int i, int expect, int update) {
  return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater类的主要作用是让普通变量也享受原子操作。就比如原本有一个变量是int型,并且很多地方都应用了这个变量,但是在某个场景下,想让int型变成AtomicInteger,但是如果直接改类型,就要改其他地方的应用。AtomicIntegerFieldUpdater就是为了解决这样的问题产生的。

public class AtomicIntegerFieldUpdaterTest {
    public static class V{
        volatile int score;
        public int getScore(){
            return score;
        }
        public void setScore(int score){
            this.score = score;
        }
    }
    public final static AtomicIntegerFieldUpdater<V> vv = AtomicIntegerFieldUpdater.newUpdater(V.class, "score");

    public static AtomicInteger allscore = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException
    {
        final V stu = new V();
        Thread[] t = new Thread[10000];
        for (int i = 0; i < 10000; i++){
            t[i] = new Thread(()->{
                if(Math.random()>0.4){
                    vv.incrementAndGet(stu);
                    allscore.incrementAndGet();
                }
            });
            t[i].start();
        }
        for (int i = 0; i < 10000; i++){
            t[i].join();
        }
        //vv和allscore的结果保持一致
        System.out.println("score="+stu.getScore());
        System.out.println("allscore="+allscore);
    }
}

上述代码将score使用 AtomicIntegerFieldUpdater变成 AtomicInteger。保证了线程安全。这里使用allscore来验证,如果score和allscore数值相同,则说明是线程安全的。 小说明: Updater只能修改被标识为volatile的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。比如如果某变量申明为private,就是不可行的。为了确保变量被正确的读取,它必须是volatile类型的。如果我们原有代码中未申明这个类型,那么简单得申明一下就行,这不会引起什么问题。由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()不支持静态变量)。

参考网址: https://blog.csdn.net/ls5718/article/details/52563959 https://blog.csdn.net/a67474506/article/details/48310515

转载于:https://my.oschina.net/cqqcqqok/blog/1925073

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值