Java多线程之原子操作类

3 篇文章 0 订阅

在并发编程中很容易出现并发安全问题,最简单的例子就是多线程更新变量i=1,多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的Atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新多种类型。Atomic包下的这些类都是采用乐观锁策略CAS来更新数据。

CAS原理与问题

CAS操作(又称为无锁操作)是一种乐观锁策略。它假设所有线程访问共享资源的时候不会出现冲突,因此不会阻塞其他线程的操作。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS的操作过程

举例说明:
Atomic包中的AtomicInteger类,是通过Unsafe类下的native函数compareAndSwapInt自旋来保证原子性,
其中incrementAndGet函数调用的getAndAddInt函数如下所示:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
可见只有自旋实现更新数据操作之后,while循环才能够结束。

CAS的问题

自旋时间过长。由compareAndSwapInt函数可知,自旋时间过长会对性能是很大的消耗。
ABA问题。因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C,或使用AtomicStampedReference工具类,这种更新方式会带有版本号。

AtomicStampedReference使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        Integer init1 = 1110;
//        Integer init2 = 126;
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(init1, 1);
        int curent1 = reference.getReference();
//        Integer current2 = reference.getReference();
        reference.compareAndSet(reference.getReference(), reference.getReference() + 1, reference.getStamp(), reference.getStamp() + 1);//正确写法
//        reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正确写法
//        reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//错误写法
//        reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//错误写法
//        reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);
        System.out.println("reference.getReference() = " + reference.getReference());
    }
}

Atomic包的使用

原子更新基本类型

Atomic包中原子更新基本类型的工具类:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法

  • int addAndGet(int delta):以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
  • int incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
  • int getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;
  • int getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;
  • boolean compareAndSet(int expect, int update):如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
  • void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger使用示例:

public class AtomicExample {

    private static AtomicInteger atomicInteger = new AtomicInteger(2);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.incrementAndGet());
        System.out.println(atomicInteger.get());
    }
}
LongAdder

为了解决自旋导致的性能问题,JDK8在Atomic包中推出了LongAdder类。LongAdder采用的方法是,共享热点数据分离的计数:将一个数字的值拆分为一个数组。不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多;要得到这个数字的话,就要把这个值加起来。相比AtomicLong,并发量大大提高。

优点:有很高性能的并发写的能力
缺点:读取的性能不是很高效,而且如果读取的时候出现并发写的话,结果可能不是正确的

其他基本类型

Atomic包提供了3种基本类型的原子更新,但是Java的基本类型里还有char、float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的,让我们一起看一下Unsafe的源码。

/**
* 如果当前数值是expected,则原子的将Java变量更新成x
* @return 如果更新成功则返回true
*/
public final native boolean compareAndSwapObject(Object o,long offset , Object expected , Object x );
public final native boolean compareAndSwapInt(Object o , long offset , int expected, int x );
public final native boolean compareAndSwapLong(Object o , long offset , long expected ,long x );

通过代码,我们发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。

// AtomicBoolean
public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

// AtomicDouble
public final boolean compareAndSet(double expect, double update) {
    return updater.compareAndSet(this,Double.doubleToRawLongBits(expect),Double.doubleToRawLongBits(update));
}
原子更新数组类型

Atomic包中提供能原子更新数组中元素的工具类:
AtomicIntegerArray:原子更新整型数组中的元素;
AtomicLongArray:原子更新长整型数组中的元素;
AtomicReferenceArray:原子更新引用类型数组中的元素

这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:

get(int index):获取索引为index的元素值。
addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;
getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新

AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在前者的方法中会多一个指定数组索引位i。
AtomicIntegerArray使用示例:

public class AtomicExample {

    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //对数组中索引为2的位置的元素加3
        int result = integerArray.getAndAdd(2, 3);
        System.out.println(integerArray.get(2)); //6
        System.out.println(result); //3, add前原数值
        System.out.println(value[2]); //3
    }
}

需要注意的是,数组value通过构造方法传递进去,然后integerArray 会将当前数组复制一份,所以当integerArray 对内部的数组元素修改的时候,不会影响原先传入的value数组。
构造方法:

/**
 * Creates a new AtomicIntegerArray with the same length as, and
 * all elements copied from, the given array.
 *
 * @param array the array to copy elements from
 * @throws NullPointerException if array is null
 */
public AtomicIntegerArray(int[] array) {
    // Visibility guaranteed by final field guarantees
    this.array = array.clone();
}
原子更新引用类型

如果需要原子更新引用类型变量的话,为了保证线程安全,Atomic也提供了相关的类:

AtomicReference:原子更新引用类型;
AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
AtomicMarkableReference:原子更新带有标记位的引用类型;

这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现,但AtomicReferenceFieldUpdater略有不同,更新的字段必须用volatile修饰,下面提供两段示例代码:

AtomicReference使用示例:

public class AtomicExample {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user); // User{userName='a', age=1}
        System.out.println(reference.get()); // User{userName='b', age=2}
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

AtomicReferenceFieldUpdater使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
        Dog dog1 = new Dog();
        updater.compareAndSet(dog1, dog1.name, "cat");
        System.out.println(dog1.name);
    }
}

class Dog {
    volatile String name = "dog1";
}
原子更新字段类

Atomic包提供了四个类进行原子字段更新:

AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。

要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符。

AtomicIntegerFieldUpdater使用示例:

public class AtomicExample {

    private static AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public static void main(String[] args) {
        User user = new User("a", 1);
        System.out.println(updater.getAndAdd(user, 5));
        System.out.println(updater.addAndGet(user, 1));
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值