并发线程基础第六篇

目录

共 享 模 型 之 无 锁

CAS与volatile

举个例子

CAS工作方式

volatile

为什么无锁效率高

CAS的特点

原子整数

原子引用

为什么需要原子引用类型?

J.U.C并发包提供了:

 比如上面取款的例子,我们这里把账户的类型改为小数BigDecimal类型

ABA问题

原子数组

为什么要有原子数组

函数式接口 

字段更新器 

为什么要有字段更新器

 示例

原子累加器

为什么要学原子累加器

示例

LongAdder的性能提升的原因 

源码之LongAdder

原理之伪共享

Unsafe

概述

获取 Unsafe对象

 使用自定义AtomicData实现之前线程安全的原子整数Account实现


共 享 模 型 之 无 锁

CAS与volatile

举个例子

问题提出,保证account.withdraw()取款方法的线程安全



interface Account {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        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) / 1000_000 + " ms");
    }
}

class AccountUnsafe implements Account {
    private Integer balance;

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

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

    @Override
    public void withdraw(Integer amount) {
        balance -= amount;
    }
}

执行测试代码

public static void main(String[]args){
        Account.demo(new AccountUnsafe(10000));
        }

290cost:115ms

 为什么不安全

因为这段代码被多个线程共享读写操作,存在安全问题。

解决思路

 加锁解决这个问题当然可以,下面我们采用一种不加锁的实现(乐观锁)

无锁实现,只需要把这个实现方法稍微修改一下就行了。

public class AccountSafe implements Account{
    private AtomicInteger balance;

    public AccountSafe(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;
            }
        }
    }
}

CAS工作方式

 其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交 换】的原子性。

volatile

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

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

所以CAS必须借助volatile才能读取到共享变量的最新值来实现(比较并交换)的效果

为什么无锁效率高

经过上面代码的几轮测试,我们发现无锁运行时间更快,这是为什么呢?

无锁情况下,即使重试失败,但是线程始终在高速运行中,没有停歇,而synchronized在没有获得锁的情况下,进行上下文切换,进入阻塞状态。

CAS的特点

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

CAS 体现的是无锁并发、无阻塞并发

  •  因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数

J.U.C并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以AtomicInteger提供的API为示例

AtomicInteger i = new AtomicInteger(0);
 
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
 
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
 
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
 
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
 
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
 
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
 
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
 
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
 
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
 
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

 这些API,大家有兴趣可以自行练习实验,可以自己写一个线程安全的接口,自己实验。

原子引用

为什么需要原子引用类型?

因为我们想要保护的共享变量并不一定是基本类型,也可以是引用类型

J.U.C并发包提供了:

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference

 比如上面取款的例子,我们这里把账户的类型改为小数BigDecimal类型

接口

public interface Account1 {
    //获取余额
    BigDecimal getBalance();
    //取款
    void withdraw(BigDecimal amount);

     static void demo(Account1 account){
         List<Thread> ts = new ArrayList<>();
         Long start = System.nanoTime();
         for (int i = 0; i < 1000; i++) {
             ts.add(new Thread(()->{
                     account.withdraw(BigDecimal.TEN);
             }));
         }
         ts.forEach(Thread::start);
         ts.forEach(t ->{
             try {
                 t.join();
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
         });
         long end = System.nanoTime();
         System.out.println(account.getBalance()+"cost:"+(end-start)/1000_000+"ms");
     }
}

实现类

public class AccountSafe1 implements Account1{
    private AtomicReference<BigDecimal> balance;

    public AccountSafe1(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;
            }
        }
    }
}

 测试

@Slf4j
public class Test1 {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Account1.demo(new AccountSafe1(new BigDecimal(10000)));
        }

    }
}

以上我是采用了CAS来解决并发中对共享资源为引用类型存在线程安全问题,还可以采用锁,自行实验。 

注意 

  • 我们这里采用线程数为1000个,是不符合CAS使用场景的,我们在这里只做测试用。
  • 线程最好不要超过你CPU核心数的时候 ,才能充分发挥它的特长

ABA问题


@Slf4j
public class Test2 {
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) {
        //拿到ref的值
        String prev = ref.get();
        
        //调用other,对ref的值进行修改。
        other();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //把A的值修改为C,当只有主线程,ref的值肯定没有改变,可以修改成功
        // 但是调用other之后,ref的值被修改过两次,但是最终最新值还是和prev之前拿到的一样,所以也可以修改成功
        ref.compareAndSet(prev,"C");
        //打印ref 的值
        log.debug("{}",ref);

    }
    static void other(){
        Thread t1 = new Thread(() -> {
            ref.set("B");
        });
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ref.set("A");
        });
        t1.start();
        t2.start();

    }
}

 虽然我们上面是否对ref做了修改,但只要prev和最新的ref一样,就可以修改成功。

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

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

@Slf4j
public class Test2 {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);

    public static void main(String[] args) {
        //拿到ref的值
        String prev = ref.getReference();
        int stamp = ref.getStamp();

        //调用other,对ref的值进行修改。
        other();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        ref.compareAndSet(prev,"C",stamp,stamp+1);
        //打印ref 的值
        log.debug("{}",ref.getReference());

    }
    static void other(){
        Thread t1 = new Thread(() -> {
            ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp()+1);
        });
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp()+1);
        });
        t1.start();
        t2.start();

    }

这时候我们可以看到,修改失败,原因是因为,我们加了版本号,不光要对比最新值,还要对比最新的版本号,只要期间被修改过,我们这里想要把它变成C就会失败。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference ,这个可以自己动手实验一下。

原子数组

为什么要有原子数组

因为有时候我们想要修改的并不是对象本身,而是对象里面的内容,比如数组,有时候我们并不是想修改数组的引用,而是想修改数组对象里面的内容这时候AtomicReference并不能达到我们的要求,为此J.U.C提供了

  •         AtomicIntegerArray
  •         AtomicLongArray
  •         AtomicReferenceArray

函数式接口 

在Java中,函数式接口是指只包含一个抽象方法的接口。Java 8引入了函数式接口的概念,并提供了 java.util.function 包来定义一些常用的函数式接口,以便在Lambda表达式中使用。以下是几种常见的函数式接口: 

Supplier<T>: 代表一个供应商,它不接受任何参数,但返回一个结果。

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Consumer<T>: 代表一个消费者,它接受一个参数但不返回结果。

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

 Function<T, R>: 代表一个函数,它接受一个参数并返回一个结果

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

示例:


/**
 * 参数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> ts=new ArrayList<>();
        T array=arraySupplier.get();
        int length=lengthFun.apply(array);
        for(int i=0;i<length; i++){
        // 每个线程对数组作 10000 次操作
        ts.add(new Thread(()->{
            for(int j=0;j< 10000;j++){
                putConsumer.accept(array,j%length);
                }
            }));
        }
        ts.forEach(t->t.start()); // 启动所有线程
        ts.forEach(t->{
            try{
            t.join();
            }catch(InterruptedException e){
                e.printStackTrace();
                }
            }); // 等所有线程结束
        printConsumer.accept(array);
}

不安全的数组

demo(
     ()->new int[10],
     (array)->array.length,
     (array, index) -> array[index]++,
     array-> System.out.println(Arrays.toString(array))
);

 安全数组

demo(
     ()-> new AtomicIntegerArray(10),
     (array) -> array.length(),
     (array, index) -> array.getAndIncrement(index),
     array -> System.out.println(array)
);

字段更新器 

为什么要有字段更新器

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

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

 示例

下面我举一个例子,演示一下字段更新器的使用

public class Test {
    public static void main(String[] args) {
        Student stu = new Student();

        AtomicReferenceFieldUpdater updater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
        //打印为true,修改成功;打印false,说明name值已经被其他线程所修改过,不为null了
        System.out.println(updater.compareAndSet(stu, null, "李四"));
    }
}

class Student{
    volatile String name;//必须加上volatile,因为cas操作必须要和volatile一起使用,保证可见性,才能进行cas操作

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

原子累加器

为什么要学原子累加器

虽然AtomicLong,AtomicInteger也可以做原子累加操作,但是J.U.C为我们提供了效率更高的LongAdder等累加器,我们可以学习一下大师的设计方式,然后针对不同需求,选择更优的API

示例


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

    }

    public static <T> void demo(
            Supplier<T> adderSupplied,
            Consumer<T> action
    ){
        //拿到提供的对象
        T t = adderSupplied.get();
        //开始记时
        long start = System.nanoTime();
        List<Thread> th = new ArrayList<>();
        //5个线程,每个累加50w
        for (int i = 0; i < 5; i++) {
            th.add(new Thread(()->{
                for (int j = 0; j < 500000; j++) {
                    action.accept(t);
                }
            }));
        }
        th.forEach(Thread::start);
        th.forEach(item ->{
            try {
                item.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });//主线程等待其他每个线程完成
        long end =System.nanoTime();
        System.out.println(t + "cost:"+(end-start)/1000_000);
    }
}

从上面示例中,我们可以看到LongAdder效率是比较高的,下面我们分析一下原因

LongAdder的性能提升的原因 

性能提升的原因很简单,我们知道cas操作在多个线程操作同一共享变量的时候,会一直while(true)进行判断,当竞争激烈的时候,cas重试次数也因此增加;但是LongAdder,在有竞争的时候,设置多个累加单元(Cells),线程一累加Thread-0,线程二累加Thread-1...,最后将结果累加汇总,这样它们在累加时操作不同的累加单元,减少了cas重试失败,从而提升性能。

源码之LongAdder

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧 LongAdder 类有几个关键域


// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
 
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
 
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

原理之伪共享

// 防止缓存行伪共享
@sun.misc.Contended 
static final class Cell {
     volatile long value;
     Cell(long x) { value = x; }
 
 // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
 final boolean cas(long prev, long next) {
     return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
 }
 // 省略不重要代码
}

 伪共享(False Sharing)是一种与缓存相关的性能问题,它出现在多个线程同时访问同一缓存行的不同部分时。虽然这些线程可能在不同的变量上进行操作,但它们共享同一缓存行的存在导致了额外的缓存同步开销,降低了程序的性能。使用 @Contended 注解(JDK8+): JDK8 中引入了 @Contended 注解,可以在变量声明时使用该注解来告诉 JVM 在存储该变量时对其进行填充,从而避免伪共享问题。

下面我们从缓存来看一下

从cpu到大约需要的时钟周期
寄存器1 cycle (4GHz 的 CPU 约为0.25ns)
一级缓存3~4 cycle
二级缓存10~20 cycle
三级缓存40~45 cycle
内存120~240 cycle
  • 因为cpu从内存读取速度与从缓存读取速度差异很大,需要靠预读取数据到缓存中来提升效率。
  • 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte.
  • 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。
  • CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU核心对应的整个缓存行必须失效。

 

 因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效  

Unsafe

概述

Unsafe对象提供了非常底层的,操作内存,线程的方法,Unsafe对象不能直接调用,只能通过反射获得。AtomicInteger,LockSupport中的park,unpark方法底层都是采用Unsafe实现的。

获取 Unsafe对象

public class Test4 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        //拿到域对象,由于theUnsafe为静态私有的,所以要用getDeclaredField(),不能用getField();
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //设置让不让拿这个私有对象,允不允许访问
        theUnsafe.setAccessible(true);
        //拿到theUnsafe对象
        Unsafe o = (Unsafe) theUnsafe.get(null);
        System.out.println(o);

    }
}


sun.misc.Unsafe@27c170f0

Process finished with exit code 0

 演示Unsafe的一些API

public class Test4 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        //拿到域对象,由于theUnsafe为静态私有的,所以要用getDeclaredField(),不能用getField();
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //设置让不让拿这个私有对象
        theUnsafe.setAccessible(true);
        //拿到theUnsafe对象
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        //拿到每个域的偏移量,id域偏移量
        long idOffset = unsafe.objectFieldOffset(Teacter.class.getDeclaredField("id"));
        //name域偏移量
        long nameOffset = unsafe.objectFieldOffset(Teacter.class.getDeclaredField("name"));
        Teacter teacter = new Teacter();
        //cas操作,如果有其他线程,要把这部分代码放到while里面,不断尝试
        unsafe.compareAndSwapInt(teacter,idOffset,0,1);
        unsafe.compareAndSwapObject(teacter,nameOffset,null,"xiaoming");
        System.out.println(teacter);
    }
}

class Teacter{
    volatile int id;
    volatile String name;

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

 使用自定义AtomicData实现之前线程安全的原子整数Account实现

获取Unsafe

public class UnsafeAccessor {
    static Unsafe unsafe;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    static Unsafe getUnsafe() {
        return unsafe;
    }
}

设置自定义AtomicData

class AtomicData {
    private volatile int data;
    static final Unsafe unsafe;
    static final long DATA_OFFSET;

    static {
        unsafe = UnsafeAccessor.getUnsafe();
        try {
            // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
            DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }

    public AtomicData(int data) {
        this.data = data;
    }

    public void decrease(int amount) {
        int oldValue;
        while(true) {
            // 获取共享变量旧值,可以在这一行加入断点,修改 data 调试来加深理解
            oldValue = data;
            // cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false
            if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
                return;
            }
        }
    }

    public int getData() {
        return data;
    }
}

实现

Account.demo(new Account() {
    AtomicData atomicData = new AtomicData(10000);
    @Override
    public Integer getBalance() {
        return atomicData.getData();
    }
    @Override    
    public void withdraw(Integer amount) 
        {        atomicData.decrease(amount);    }
});

  • 50
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值