(Java并发编程——JUC)带你研究共享模型通过无锁的方式解决并发问题!本文分析对原子对象的使用!

1. 无锁解决

  • CAS与volatile
  • 原子整数
  • 原子引用
  • 原子累加器
  • Unsafe

1.1 提出问题

有如下需求,保证 accounet.withdraw 取款方式的线程安全

/**
 * 无锁
 */
public class test1 {
    public static void main(String[] args) {
        AccountUnsafe account = new AccountUnsafe(10000);
        Account.demo(account);
    }
}

class AccountUnsafe implements Account{
    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        // 临界区
        return this.balance;
    }

    @Override
    public void withdraw(Integer amount) {
        // 临界区
        this.balance -= amount;
    }
}
interface Account{
    Integer getBalance();

    void withdraw(Integer amount);

    static void demo(Account account){
        ArrayList<Thread> ts = new ArrayList<>();
        /*
        这里开启1000个线程,每个线程在启动时都取走了10;最后结果输出应该是0
         */
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(()->{account.withdraw(10);},"t"+i));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()+" ——cost: "+(end-start)/100_000 +"ms");
    }
}

  • 解决问题

在临界区的两个方法上使用synchronized修饰即可

@Override
public synchronized Integer getBalance() {
    return this.balance;
}

@Override
public synchronized void withdraw(Integer amount) {
    // 临界区
    this.balance -= amount;
}

1.1.1 无锁解决

其他代码不变,更改实现类

class AccountCas implements Account{

   private AtomicInteger balance;

   public AccountCas(int balance) {
       this.balance = new AtomicInteger(balance);
   }

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

   @Override
   public void withdraw(Integer amount) {
       while (true){
           // 获取余额的最新值
           int prev = balance.get();
           // 要修改的金额
           int next = prev - amount;
           // 真正修改;第一个参数:修改的值;第二个参数:更换的新值
           if (balance.compareAndSet(prev, next)){
               break;
           }
       }
   }
}

1.2 CAS 与 volatile

1.2.1 CAS

AtomicInteger解决方案,内部并没有用锁来保护共享变量的线程安全。

那么它底层是如何实现无锁即线程安全呢?

public void withdraw(Integer amount) {
    while (true){
        // 获取余额的最新值
        int prev = balance.get();
        // 要修改的金额
        int next = prev - amount;
        // 真正修改;第一个参数:修改的值;第二个参数:更换的新值
        if (balance.compareAndSet(prev, next)){
            break;
        }
    }
}

其中的关键是compareAndSet,它的简称就是CAS(Coompare And Swap),它必须是 原子操作

在这里插入图片描述

compareAndSet 会先进行判断,判断什么?

  • 判断获得的值与共享变量是否一致
    • 如果一致:返回true,并修改值
    • 如果不一致:返回false,不做修改

CAS的底层使用的是:lock cmpxchg 指令(X86架构)

在单核CPU和多核CPU下都能够保证【比较-交换】的原子性

  • 在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。

    在这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的

1.2.2 volatile

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰

它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

即一个线程对volatile变量的修改,对另一个线程可见

需要注意的是:volatile仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

CAS必须借助 volatile 才能读取到共享变量的最新值来实现【compareAndSet】的效果

1.2.3 为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
    • 就如一辆开车起步熄火一样,起步又得做一套点火、挂挡等操作
    • 而一直缓慢行驶则能够保持较好运行轨迹
  • 无锁情况下,因为线程需要保持运行,需要额外CPU的支持,CPU在这里就好比高速跑道。
    • 没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入课运行状态,还是会导致上下文切换

1.2.4 CAS的特点

结合CAS和volatile可以实现无锁并发,适用于线程数较少、多核CPU的场景下

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,会进行重试
  • synchronized是基于悲观锁的思想:最悲观的估计,需要防止其他线程来修改关共享变量,只有自己将任务做完才会释放锁,让其他人使用
  • CAS体现的是无锁并发、无阻塞并发。
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这时效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

1.3 原子对象

1.3.1 原子整数

JUC并发包中提供了

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

基本使用都是一致的,以其中一种为例

private volatile int value;

/**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
public AtomicInteger(int initialValue) {
    value = initialValue;
}
  • 基本使用
public class test2 {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(0);

        System.out.println(i.incrementAndGet()); // 自增并获取值 - ++i
        System.out.println(i.getAndIncrement()); // i++

        System.out.println(i.decrementAndGet()); //  --i
        System.out.println(i.getAndDecrement()); // i--

        i.getAndAdd(5); // i+=5
        i.addAndGet(5);// 先增加再赋值

        System.out.println(i.get());
    }
}
1.3.1.1 updateAndGet() & getAndUpdate()
i.updateAndGet(value->value / 2);
i.updateAndGet(value->{
    int num = value * 2;
    return num / 4;
});

方法内部是提供一个函数式接口(lambda表达式)

它所返回值是做出原子操作后的结果

其中这两个方法没有本质区别,只不过是先赋值还是先读取罢了

  • 接口底层
private volatile int value;
public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        // 使用接口的抽象方法来处理更新值
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return next;
}
public final int get() {
    return value;
}
int applyAsInt(int operand);
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

底层还是使用compareAndSwapInt()方法来进行计算

1.3.2 原子引用

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedreferenece
package com.renex.c7;

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

/**
 * 无锁
 */
public class test3 {
    public static void main(String[] args) {
        DecimalCas account = new DecimalCas(new BigDecimal(10000));
        AccountBigDecimal.demo(account);
    }
}

class DecimalCas implements AccountBigDecimal{

    // 使用原子引用类型:这里引用 BigDecimal 类型
    private AtomicReference<BigDecimal> balance;

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

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

    @Override
    public void withdraw(BigDecimal amount) {
        while (true){
            // 获取余额的最新值
            BigDecimal prev = balance.get();
            // 要修改的金额
            BigDecimal next = prev.subtract(amount);
            // 真正修改;第一个参数:修改的值;第二个参数:更换的新值
            if (balance.compareAndSet(prev, next)){
                break;
            }
        }
    }
}

interface AccountBigDecimal{
    BigDecimal getBalance();

    void withdraw(BigDecimal amount);

    static void demo(AccountBigDecimal account){
        ArrayList<Thread> ts = new ArrayList<>();
        /*
        这里开启1000个线程,每个线程在启动时都取走了10;最后结果输出应该是0
         */
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(()->{account.withdraw(BigDecimal.TEN);},"t"+i));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()+" ——cost: "+(end-start)/100_000 +"ms");
    }
}

其实原子对象的使用方式都基本一致

1.3.2.1 ABA问题
static AtomicReference<String> ref = new AtomicReference<String>("A");
public static void main(String[] args) throws InterruptedException {
    log.info("main start....");
    String prev = ref.get();
    other();
    Thread.sleep(1000);
    log.info("change A->C {}",ref.compareAndSet(ref.get(),"C"));
}

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

[main] INFO test4 - main start....
[t1] INFO test4 - change A->B true
[t2] INFO test4 - change B->A true
[main] INFO test4 - change A->C true

无法感知其他线程对共享变量是否做出更改。

虽然不会影响运行,但是会存在一种隐患

主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况,如果主线程希望:

  • 只要有其他线程【动过了】共享变量,那么它自己的cas就算失败,这时仅比较值是不够的,需要再加一个版本号
1.3.2.2 AtomicStampedreferenece

除了需要比较值的一致,还需要比较版本号一致

  • 只有两者都处于一致状态,才会发生修改
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
    log.info("main start....");
    String prev = ref.getReference();
    // 版本号
    int stamp = ref.getStamp();

    other();
    Thread.sleep(1000);
    log.info("change A->C {}",ref.compareAndSet(ref.getReference(),"C",stamp,stamp+1));
    System.out.println("main "+stamp);
}

private static void other(){
    new Thread(()->{
        int stamp = ref.getStamp();
        log.info("change A->B {}",ref.compareAndSet(ref.getReference(),"B",stamp,stamp+1));
        System.out.println("=========t1============"+stamp);
    },"t1").start();
    new Thread(()->{
        int stamp = ref.getStamp();
        log.info("change B->A {}",ref.compareAndSet(ref.getReference(),"A",stamp,stamp+1));
        System.out.println("===========t2=========="+stamp);
    },"t2").start();
}
///
[main] INFO test5 - main start....
[t1] INFO test5 - change A->B true
[t2] INFO test5 - change B->A true
=========t1============0
===========t2==========1
main 0
[main] INFO test5 - change A->C false

main线程获得的stamp还是初始值 0,但是获取到初始值后,main线程就开始了休眠

能够看到,t1线程获得stamp版本号还是初始值0,当做完修改后版本号+1(stamp=1)

因为t2线程开始时先获取了一次stamp版本号,所以t2拿取的版本号依旧是t1线程更改后的 1

那么t2就可以判断一致,所以也进行了更改

当main线程经过休眠继续执行时,就会发现版本号不一致,那么这时候就无法继续修改

1.3.2.3 AtomicMarkableReference

AtomicStampedreferenece 可以给原子引用加上版本号,追踪原子引用整个的变化过程

通过 AtomicStampedreferenece 可以知道,引用变量在中途中被更改过几次

  • 而在有些时候,并不关心引用变量更改了几次,只是单纯的关系是否被更改过,所有就有了 AtomicMarkableReference
@Slf4j(topic = "test5")
public class test6 {
    public static void main(String[] args) throws InterruptedException {
        demo data = new demo("OK");
        AtomicMarkableReference<demo> ref = new AtomicMarkableReference<>(data,false);
        log.info("start....");
        demo prev = ref.getReference();
        log.info("result:{}", prev.toString());

        new Thread(() -> {
            log.info("thread start");
            data.setOk("NOT OK");
            boolean b = ref.compareAndSet(data, data, true, false);
            log.info(data +"__"+b);
        },"t1").start();

        Thread.sleep(1000);
        boolean newOk = ref.compareAndSet(prev, new demo("NEW OK"), true, false);
        log.info("change result:{}", newOk);
        log.info(ref.getReference().toString());
    }

    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    static class demo{
        private String ok;
    }
}
///
[main] INFO test5 - start....
[main] INFO test5 - result:test6.demo(ok=OK)
[t1] INFO test5 - thread start
[t1] INFO test5 - test6.demo(ok=NOT OK)__false
[main] INFO test5 - change result:false
[main] INFO test5 - test6.demo(ok=NOT OK)

AtomicMarkableReference 没有那么复杂,它仅做bool值的判断,但值不一致时就判断共享变量遭遇了变动

1.3.3 原子数组

原子数组类型,这个其实和AtomicInteger等类似,只不过在修改时需要指明数组下标。

CAS是按照==来根据地址进行比较。数组比较地址,肯定是不行的,只能比较下标元素。而比较下标元素,就和元素的类型有关系了。

原子类型数组有以下四种:

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
  • AtomicBooleanArray

AtomicIntegerArrayAtomicLongArray 的使用方式差别不大,AtomicReferenceArray因为他的参数为引用数组,所以跟前两个的使用方式有所不同。AtomicBooleanArray在生产中使用的很少。

public static void main(String[] args) {
    AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
    for (int i = 0; i < atomicIntegerArray.length(); i++) {
        // 这里执行,i的目标初始值是0,因为在for循环前定义了i这个变量的值是0
        // 第一个参数:更新的数组的索引;第二个参数:确认值;第三个参数:实行更改的值
        boolean b = atomicIntegerArray.compareAndSet(i, 0, i + 10);
        System.out.println(b);
    }
    // 这里目标确认值是0是错误的,因为在for循环中已经成功更改过了,所以这里的目标确认值应是 13
    System.out.println(atomicIntegerArray.compareAndSet(3, 0,  30));
    //        System.out.println(atomicIntegerArray.compareAndSet(3, 13,  30));

    System.out.println(atomicIntegerArray.get(2));
    System.out.println(atomicIntegerArray.get(3));
}
///
true
true
true
true
true
true
true
true
true
true
false
12
30

1.3.4 字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
	at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.<init>(AtomicReferenceFieldUpdater.java:348)
	at java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceFieldUpdater.java:110)
	at com.renex.c8.test2.main(test2.java:16)

示例代码:

public class test2 {
    public static void main(String[] args) {
        Student stu = new Student();
        /**
         * newUpdater()
         * 第一个参数:保护的类的类型
         * 第二个参数:保护的具体变量类型
         * 第三个参数:修改的变量名
         */
        AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater
                .newUpdater(Student.class, String.class, "name");
        /**
         * updater.compareAndSet()
         * 第一个参数:修改的对象
         * 第二个参数:确定目标值
         * 第三个参数:修改为什么值
         */
        System.out.println(updater.compareAndSet(stu, null, "zhangsan"));;
        System.out.println(stu);
    }
}
class Student{
    volatile String name;

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

1.3.5 原子累加器

1.3.5.1 LongAdder 累加器性能比较
package com.renex.c8;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class test3 {
    public static void main(String[] args) {
        demo(()->new AtomicLong(0),
                (adder)->adder.getAndIncrement()
                );
        //
        demo(()->new LongAdder(),
                (adder)->adder.increment()
        );
    }

    /**
     * @param adderSupplier 累加对象
     * @param action 行动抽象函数
     * @param <T>
     */
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action){
        T adder = adderSupplier.get();
        ArrayList<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 50000; j++) {
                    action.accept(adder);
                }
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder+"cost: "+(end-start)/100_000+" ms");
    }

}
/
200000cost: 115 ms
200000cost: 69 ms

性能提升的原因很简单,就是在有竞争的时候,设置多个累加单元,Thread-0 累加 Cell[0],而Thread-1 累加 Cell[1]…最后将结果汇总即可

这样它们在累加时操作着不同的Cell变量,因此减少了CAS重试失败,从而提高性能

1.3.6 Unsafe

Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得

// 找到unsafe类名,通过反射找到对应的类
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true); // 不检查
Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 默认就是null
1.3.7 Unsafe CAS 操作
package com.renex.c8;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class test5 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 找到unsafe类名,通过反射找到对应的类
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true); // 不检查
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 默认就是null

        Field id = Student.class.getDeclaredField("id");
        Field name = Student.class.getDeclaredField("name");

        long idOffset = unsafe.objectFieldOffset(id);
        long nameOffset = unsafe.objectFieldOffset(name);

        Student student = new Student();
        // 基本就是原子操作那一套
        unsafe.compareAndSwapObject(student, idOffset, 0, 22);
        unsafe.compareAndSwapObject(student, nameOffset, null, "张三");
    }

    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    static class Student{
        volatile int id;
        volatile String name;
    }
}

2. 👍JUC 专栏 - 前篇回顾👍

3. 💕👉 其他好文推荐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值