原子整数、引用、数组、更新器-JUC-并发编程(Java)

一、原子整数

JUC并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

下面以AtomicInteger为例,来了解下常用API,所有的操作都是原子的

  • 自增:
    • incrementAndGet():自增并获取,等同于++i
    • getAndIncrement():获取并自增,等同于i++
  • 自减
    • decrementAndGet():自减并获取,等同于–i
    • getAndDecrement():获取并自减,等同于i–
  • 加减法
    • getAndAdd(int delta):获取并加delta
    • addAndGet(int delta):加delta并获取

可以利用上面的API对我们之前的取款案例(传送门)进一步改造,如下:

// 只更改withdraw()方法
// 之前withdraw()
@Override
public void withdraw(Integer amount) {
    while (true) {
        int prev = balance.get();
        int next = prev - amount;
        if (balance.compareAndSet(prev, next)) {
        	break;
        }
    }
}
    
// 改造后的withdraw
@Override
public void withdraw(Integer amount) {
	balance.getAndAdd(amount);
}

很显然,改造后能实现相同的功能,但是要更简洁。

  • 复杂运算

    • updateAndGet(IntUnaryOperator updateFunction):更新并获取

    • getAndUpdate(IntUnaryOperator updateFunction):后去并更新

    IntUnaryOperator updateFunction :这是什么参数呢?

    @FunctionalInterface
    public interface IntUnaryOperator {
    
        /**
         * Applies this operator to the given operand.
         *
         * @param operand the operand
         * @return the operator result
         */
        int applyAsInt(int operand);
        //...
    }
    

    通过查看源码发现,这是一个函数式接口,示例,我们做一个简单的乘法运算:

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class TestAtmoticInteger {
        public static void main(String[] args) {
            AtomicInteger ai = new AtomicInteger(10);
            ai.updateAndGet(n ->  n * 10);
            System.out.println(ai.get());
        }
    }
    
    // 测试结果
    100
    

    用起来是不是很简单,那么是怎么实现的呢?下面简单介绍下原理,我们通过while循环+compareAndSet来模拟updateAndGet来实现乘法运行,代码如下:

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class TestAtmoticInteger {
        public static void main(String[] args) {
            AtomicInteger ai = new AtomicInteger(10);
    //        ai.updateAndGet(n ->  n * 10);
    //        System.out.println(ai.get());
    
            while (true) {
                int prev = ai.get();
                int next = prev * 10;
                if (ai.compareAndSet(prev, next)) {
                    break;
                }
            }
            System.out.println(ai.get());
       
    

    上面同样可以实现相同的效果,我们来看下源码是如何实现的

    /**
         * Atomically updates the current value with the results of
         * applying the given function, returning the updated value. The
         * function should be side-effect-free, since it may be re-applied
         * when attempted updates fail due to contention among threads.
         *
         * @param updateFunction a side-effect-free function
         * @return the updated value
         * @since 1.8
         */
        public final int updateAndGet(IntUnaryOperator updateFunction) {
            int prev, next;
            do {
                prev = get();
                next = updateFunction.applyAsInt(prev);
            } while (!compareAndSet(prev, next));
            return next;
        }
    

    源码也是这么实现的。

二、原子引用

1、AtomicReference-原子引用类型

为什么需要有原子引用呢?有时候我们修改不是仅仅需要修改变量的值,还需要修改引用类型的成员变量值,原子整数这里就不适用了,所以就有了原子引用。

以之前的取款案例,我们都知道实际生活呢,取款金额可以是小数的,涉及金额的小说我们一般使用BigDecimal处理,代码如下:

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * 账户接口
 */
public interface AccountDecimal {
    // 获取余额
    BigDecimal getBalance();
    // 取款
    void withdraw(BigDecimal amount);
    // 模拟多个线程取款操作
    static void multiWithdraw(AccountDecimal account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(BigDecimal.TEN);
            }));
        }
        long start = System.currentTimeMillis();
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("balance: "+ account.getBalance() + " cost: " + (end - start) + " ms");
    }
}


import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * CAS无锁实现
 */
public class AccountDecimalCAS implements AccountDecimal {

    private final AtomicReference<BigDecimal> balance;

    public AccountDecimalCAS(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        balance.getAndUpdate(m -> m.subtract(amount));
    }

}

import java.math.BigDecimal;

/**
 * 测试类
 */
public class TestAccount {
    public static void main(String[] args) {

        AccountDecimal account3 = new AccountDecimalCAS(new BigDecimal("10000"));
        AccountDecimal.multiWithdraw(account3);

    }
}

// 测试结果
balance: 0 cost: 67 ms
  • 说明:我们这里使用1000个线程并不符合CAS使用场景,但是这里我们只是为了测试这么做,实际使用中并不适合。

2、AtomicStampedReference-原子引用类型(版本标记)

原子引用中的ABA问题,即初始值为A改为B之后再改会A,我们是否能判断改变量是否被修改过呢?

import lombok.extern.slf4j.Slf4j;

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

@Slf4j(topic = "c.TestABA")
public class TestABA {
    static AtomicReference<String> ref = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start ...");
        // 获取值A
        String prev = ref.get();
        other();
        TimeUnit.SECONDS.sleep(1);
        // 尝试更改为C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }

    private static void other() {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();

        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }
}

// 测试结果
2021-09-27 10:44:52.619 DEBUG [main] c.TestABA - main start ...
2021-09-27 10:44:52.662 DEBUG [t1] c.TestABA - change A->B true
2021-09-27 10:44:53.161 DEBUG [t2] c.TestABA - change B->A true
2021-09-27 10:44:54.161 DEBUG [main] c.TestABA - change A->C true

有A修改为C成功了,但是我们并不能判定A是否被修改过(其他已经修改过了)。当然大部分情况下ABA问题不影响业务,也不排除会有影响的情况。当有需要这种场景-一旦共享变量被修改,其他线程可以感知到,我们改怎么做呢?需要用到AtomicStampedReference类,还是通过上面小案例,来了解AtomicStampedReference使用。

import lombok.extern.slf4j.Slf4j;

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

@Slf4j(topic = "c.TestABA")
public class TestABAStamped {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start ...");
        // 获取值A
        String prev = ref.getReference();
        int stamp = ref.getStamp();
        other();
        TimeUnit.SECONDS.sleep(1);
        // 尝试更改为C
        log.debug("change A->C {},stamp {}->{}", ref.compareAndSet(prev, "C", stamp, stamp + 1), stamp, stamp + 1);

    }

    private static void other() {
        new Thread(() -> {
            int stamp = ref.getStamp();
            log.debug("change A->B {},stamp {}->{}", ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1), stamp, stamp + 1);
        }, "t2").start();

        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            int stamp = ref.getStamp();
            log.debug("change B->A {},stamp {}->{}", ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1), stamp, stamp + 1);
        }, "t2").start();
    }
}

// 测试结果
2021-09-27 10:57:45.997 DEBUG [main] c.TestABA - main start ...
2021-09-27 10:57:46.039 DEBUG [t2] c.TestABA - change A->B true,stamp 0->1
2021-09-27 10:57:46.539 DEBUG [t2] c.TestABA - change B->A true,stamp 1->2
2021-09-27 10:57:47.539 DEBUG [main] c.TestABA - change A->C false,stamp 0->1

当回到主线程时,当前版本还是0,而其他线程已经更改版本为2,虽然值都是A,因为版本不一致,导致最后修改失败,这就解决了ABA问题。

3、AtomicMarkableReference-原子引用类型(布尔型标记)

AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程;但是有时候,我们并不关心引用变量变更了多少次,只是单纯的关心是否变更过,这里就使用AtomicMarkableReference类。

  • AtomicStampedReference内部使用整数记录版本号
  • AtomicMarkableReference:内部使用布尔值来记录是否变更过

场景描述:日常家里有垃圾我们都会扔垃圾桶中的垃圾袋中,如果垃圾袋满了,我们需要更换一个新的空的垃圾袋;假设现在垃圾袋满了,我们需要更换新的垃圾袋,怎么实现呢?

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 垃圾袋
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GarbageBag {
    private String desc;
}

import lombok.extern.slf4j.Slf4j;

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

/**
 * 更换垃圾袋
 */
@Slf4j(topic = "c.ReplaceGarbageBag")
public class ReplaceGarbageBag {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("满的垃圾袋");
        // true表示垃圾袋已满,false为空
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
        log.debug("start ...");
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());
        TimeUnit.SECONDS.sleep(1);
        log.debug("换一个空的垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空的垃圾袋"), true, false);
        log.debug("更换了吗?{}", success);
        log.debug(ref.getReference().toString());
    }
}

// 测试结果
2021-09-27 11:15:49.322 DEBUG [main] c.ReplaceGarbageBag - start ...
2021-09-27 11:15:49.324 DEBUG [main] c.ReplaceGarbageBag - GarbageBag(desc=满的垃圾袋)
2021-09-27 11:15:50.325 DEBUG [main] c.ReplaceGarbageBag - 换一个空的垃圾袋?
2021-09-27 11:15:50.325 DEBUG [main] c.ReplaceGarbageBag - 更换了吗?true
2021-09-27 11:15:50.326 DEBUG [main] c.ReplaceGarbageBag - GarbageBag(desc=空的垃圾袋)

三、原子数组

有时候我们需要包含数组内元素的线程安全性,这时候我们需要用到原子数组相关的类。

  • AtomicIntegerArray:原子整形数组
  • AtomicLongArray:原子长整形数组
  • AtomicReferenceArray:原子引用数组

下面我们通过代码来测试下普通数组和原子数组的线程安全性

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class TestArray {
    public static void main(String[] args) {
        // 普通数组
        demo(
                () -> new int[10],
                array ->array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array))
        );
        // 原子数组
        demo(
                () -> new AtomicIntegerArray(10),
                AtomicIntegerArray::length,
                AtomicIntegerArray::getAndIncrement,
                System.out::println
        );
    }
    /**
     参数1,提供数组、可以是线程不安全数组或线程安全数组
     参数2,获取数组长度的方法
     参数3,自增方法,回传 array, index
     参数4,打印数组的方法
     */
    // supplier 提供者 无中生有  ()->结果
    // function 函数   一个参数一个结果   (参数)->结果  ,  BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果  (参数)->void,      BiConsumer (参数1,参数2)->
    private static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer
    ) {
        List<Thread> list = new ArrayList<>();
        T array = arraySupplier.get();
        int len = lengthFun.apply(array);
        for (int i = 0; i < len; i++) {
            list.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % len);
                }
            }));
        }
        list.forEach(Thread::start);
        list.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(array);
    }
}

// 测试结果
[8910, 8911, 8901, 8907, 8871, 8893, 8879, 8876, 8883, 8882]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

说明:代码中几个都为JDK8新特性中函数式接口,lambda表达式的内容。有兴趣的或者不懂的可自行查阅相关文档,这里不在详述。

四、原子更新器

有以下三种原子更新器:

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用原子更新器,可以针对对象的某个域(Field字段)进行原子操作,只能配合volatile修饰的字段使用,否则报错:Exception in thread “main” java.lang.IllegalArgumentException: Must be volatile type

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

@Slf4j(topic = "c.TestAtomicFieldUpdater")
public class TestAtomicFieldUpdater {
    public static void main(String[] args) {
        Student stu = new Student();
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        System.out.println(updater.compareAndSet(stu, null, "张三"));
        System.out.println(updater.get(stu));
    }
}


class Student {
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

QQ:806797785

仓库地址:https://gitee.com/gaogzhen/concurrent

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值