线程学习(26)-Atomic原子类型系列

因为在之前的学习中,也学到了多个线程安全的方法组合到一块却不是线程安全的。如果采用synchronized这种方式,锁比较重。所以提供一系列的基于CAS的组合方法类的API,来用以保证线程安全。

AtomicInteger,AtomicLong,AtomicBoolean

在jdk1.8的文档里,这三种类型的方法基本上都是类似的。源码的底层都是do{}While()循环调用CAS方法完成操作。



package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 基本类型的AtomicInteger,AtomicLong,AtomicBoolean使用
 */
@Slf4j
public class AtomicIntegerTypeTest {

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(1000);
//        extracted(atomicInteger);
//        extracted1(atomicInteger);
//        extracted2(atomicInteger);
        extracted3(atomicInteger);


    }

    /**
     * 这个方法就是使用accumulateAndGet方法来对值进行原子性的函数运算
     * accumulate  积累的意思
     * @param atomicInteger
     * @throws InterruptedException
     */
    private static void extracted3(AtomicInteger atomicInteger) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        int num1 = 5;
        int num2 = 10;
        new Thread(() -> {
            try{
                for (int i = 0; i < 1000; i++) {
                    //这个和UpdateAndGet方法从源码层面都类似,只不过代入了两个入参来进行操作
                    //left是当前atomicInteger对象的值,而right代表的第一个参数
                    atomicInteger.accumulateAndGet(num1,(left, right) -> left+right);
                }
                countDownLatch.countDown();
            }catch (Exception e){
                log.error("222",e);
            }

        }).start();

        new Thread(() -> {
            try{
                for (int i = 0; i < 1000; i++) {
                    atomicInteger.accumulateAndGet(num2,(left, right) -> left+right);
                }
                countDownLatch.countDown();
            }catch (Exception e){
                log.error("111",e);
            }
        }).start();

        countDownLatch.await();
        log.debug(atomicInteger.get()+"");
    }

    private static void extracted2(AtomicInteger atomicInteger) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        int t = 2;
        int m = 3;
        //updateAndGet函数,传入的是一个函数表达式IntUnaryOperator接口,这里的话只需要计算结果,而不需要返回结果
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.updateAndGet(x -> x+t);
                //和上面的这个类似,只是返回结果不一样
//                atomicInteger.getAndUpdate(x -> x+t);
            }
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.updateAndGet(x -> x+m);
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        log.debug(atomicInteger.get()+"");
    }

    //校验getAndAdd以及addAndGet的原子性
    private static void extracted1(AtomicInteger atomicInteger) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(() -> {
            //底层与getAndIncrement等类似,都是调用了Unsafe类名下的getAndAddInt方法
            for (int i = 0; i < 1000; i++) {
                atomicInteger.getAndAdd(5);
            }
            countDownLatch.countDown();

        }).start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                //只是返回结果不同,底层调用的都是Unsafe类名下的
                atomicInteger.addAndGet(-5);
            }
           countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        log.debug(atomicInteger.get()+"");
    }

    //验证多线程的++以及--,累减操作是否线程安全
    private static void extracted(AtomicInteger atomicInteger) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);

        //验证AtomicInteger的累加以及累减原子性
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                //递加1000次(先获取到主存的值再++)
                atomicInteger.getAndIncrement();

                //先进行累加,再获取主存的值
//                atomicInteger.incrementAndGet();
            }
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                //递减1000次,先获取到值,再进行--
                atomicInteger.getAndDecrement();

                //先进行--再获取到--后的值
//                atomicInteger.decrementAndGet();
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        log.debug(atomicInteger.get()+"");
    }

}

AtomicReference

AtomicReference底层是一个泛型结构,底层使用==来保证compareAndSet中的compare,所以可以搭配一些不可变类来进行使用。比如String,BigInteger,BigDecimal。因为底层采用了==,即比较地址的方式来比较,不可变类因为每次的结果都是new出来的,保证只要被修改过,底层地址肯定是不同的。而在普通类的使用,如果只是对某个属性发生改变,最终compareAndSet来比较这个对象的话,还是原先的地址,compareAndSet底层采用了==,就算线程A对某个属性发生改变,线程B这时compareAndSet比当前对象还是相等的,就没有办法保证线程安全。

package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

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

/**
 * 测试引用类型的原子操作
 * AtomicReference 引用类型原子操作,底层都是基于泛型实现的
 * AtomicStampReference 加了版本号的引用类型,防止aba问题
 * AtomicMarkableReference 加了一个boolean的引用类型
 * 我看了下api,这里的compare都是采用==判定的,而不是equals判定的
 */
@Slf4j
public class AtomicReferenceTest {

    public static void main(String[] args) throws InterruptedException {
        AtomicReference<BigDecimal> ref = new AtomicReference<>(new BigDecimal("10000"));
        CountDownLatch countDownLatch = new CountDownLatch(2);
        //在使用的时候一定要用对方法,不然会出问题,我最开始用的getAndSet,我想Set的这个值,我内部写了方法,不是原子的
        //捋一下操作流程,getAndSet我传入了一个替换的值,然后在源码中,通过while循环,不断获取当前主存的值,并且使用compareAndSet方法来与主存进行比较,它是没有办法保证我的计算是原子的
        //底层所有的代码都可以按照CAS方法来实现,有时间试着写一下复杂逻辑
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                //这里就没有累加方法了,不过也正常,引用什么类型都可能出现
                ref.getAndUpdate(bigDecimal -> bigDecimal.add(new BigDecimal(10)));
            }
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                //bigDecimal是地址层面发生了改变,此时comareAndSet比较就不同
                ref.getAndUpdate(bigDecimal -> bigDecimal.add(new BigDecimal(10)));
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        log.debug(ref.get().intValue()+"");


    }

}

AtomicStampReference

虽然可以通过AtomicReference可以保证线程安全的执行。但是,会存在ABA问题.。

即假如线程A想将状态A改成状态B,此时已经获取到了A的状态,但未进行CAS比较。但此时线程B先插了一手,将A改成B,然后再改成A,此时A线程采用CAS的方式来进行比较时,虽然现在的A不是当初的A,但可以成功的用A替换掉B的。

针对这种场景,我就想在原先的A状态替换掉,而不是更改后的A状态替换,此时就可以使用AtomicStampReference,这个里面我们可以手动维护一个版本号,达到只能在原先的A状态替换的效果。

package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 解决ABA问题,引入了AtomicStampReference,利用版本号,来对ABA的值来进行控制
 */
@Slf4j
public class AtomicStampReferenceTest {
    //之所以用bigDecimal,是因为bigDecimal是不可变类,这里采用不可变类,才能在使用compareAndSet方式时,根据地址的不同来进行比较,普通的对象是不行的
    public static void main(String[] args) {
        AtomicStampedReference<String> stringAtomicStampedReference = new AtomicStampedReference<String>("A",0);

        String reference = stringAtomicStampedReference.getReference();
        int stamp = stringAtomicStampedReference.getStamp();

        new Thread(() -> {
            String reference1 = stringAtomicStampedReference.getReference();
            int stamp1 = stringAtomicStampedReference.getStamp()+1;
            //原子性可以保证的,如果用了循环的话,因为stamp值肯定会被更改
            log.debug(stringAtomicStampedReference.compareAndSet(reference1,"B",stamp,stamp1)+"");
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String reference1 = stringAtomicStampedReference.getReference();
            int stamp1 = stringAtomicStampedReference.getStamp()+1;
            log.debug(stringAtomicStampedReference.compareAndSet(reference1,"A",stamp1-1,stamp1)+"");
        }).start();


        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if(stringAtomicStampedReference.compareAndSet(reference,"B",stamp,stamp+1)){
            log.debug("修改成功");
        }else{
            //打印了修改失败,虽然reference没有发生变化,但版本号发生了变化,也就不行了
            log.debug(stringAtomicStampedReference.getReference());
            log.debug(stringAtomicStampedReference.getStamp()+"");
            log.debug("修改失败");
        }

    }

AtomicMarkableReference

和AtomicStampReference有点像,但是使用场景要更简单点,有的时候我们在修改的时候,并不想知道它具体修改了多少次,只是想观察它是否修改过。这时,可以使用AtomicMarkableReference,这个里面维护了个boolean类型,可以保证当前是否被其它线程修改过。

package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicMarkableReference;

/**
 * AtomicMarkableReference,这个的话和AtomicStampReference类似,只不过内部是由一个true,fasle状态来控制
 * 这种现象只是想查看元素是否被更改过的情况,可以写个换垃圾袋的情况
 */
@Slf4j
public class AtomicMarkableReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicMarkableReference<String> stringAtomicMarkableReference = new AtomicMarkableReference<String>("A",true);
        String reference = stringAtomicMarkableReference.getReference();
        boolean marked = stringAtomicMarkableReference.isMarked();
        new Thread(() -> {
            stringAtomicMarkableReference.compareAndSet(reference,"B",marked,false);
        }).start();

        Thread.sleep(1000);
        String reference1 = stringAtomicMarkableReference.getReference();
        boolean marked1 = stringAtomicMarkableReference.isMarked();
        log.debug(stringAtomicMarkableReference.compareAndSet(reference1,"A",marked1,true)+"");
    }

}

AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

原子数组类型,这个其实和AtomicInteger等类似,只不过在修改时需要指明数组下标。刚才也说过,CAS是按照==来根据地址进行比较。数组比较地址,肯定是不行的。只能比较下标元素。而比较下标元素,就和元素的类型有关系了。

这里其实没啥难度,就是涉及到了函数表达式编程,这个当时有点迷,现在也清除了。

package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.Condition;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * 这里的目的是为了测试Atomic***Array系列的,因为原先我也试过,AtomicReference这种方式比较的是底层的地址,所以如果数组这么比,完蛋
 * 原子数组总共有三种,AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray。
 * 按这个AtomicReferenceArray来吧,有一定通用性,先看看API
 */
@Slf4j
public class AtomicArrayTest {

    public static void main(String[] args) throws InterruptedException {
        //这里用的Reference模拟的Integer,我暂时明白了
        demo(() ->{
                return new AtomicReferenceArray<Integer>(10);
            },
                    (array) ->  array.length(),
                    (array, index) -> array.getAndUpdate(index,s -> {
                        if(s == null)
                            s = 0;
                        return ++s;
                    }),
                    array -> System.out.println(array.toString())
        );
    }

    /**
     参数1,提供数组、可以是线程不安全数组或线程安全数组(没有入参,有返回结果,使用supplier)
     参数2,获取数组长度的方法 有一个入参,以及返回结果,使用function
     参数3,自增方法,回传 array, index 需要入参,数组以及下标 但不需要返回结果
     参数4,打印数组的方法 入参传入数组,不需要打印结果
     */
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    private static <T>void demo(Supplier<T> supplier,
                                Function<T,Integer> function,
                                BiConsumer<T,Integer> biConsumer,
                                Consumer<T> consumer) throws InterruptedException {
        //得到数组返回的元素
        T t = supplier.get();
        //后面的Integer是返回结果啊
        Integer length = function.apply(t);

        List<Thread> threads = new ArrayList<>();

        CountDownLatch countDownLatch = new CountDownLatch(length);

        //根据数组长度来进行遍历,对值进行累加
        for (Integer i = 0; i < length; i++) {
            //创建数组长度数量的线程
            threads.add(new Thread(() -> {
                //在每个线程中,都对参数中传入的数组进行1000次累加
                for (int i1 = 0; i1 < 1000; i1++) {
                    //入参为数组以及下标
                    biConsumer.accept(t,i1%length);
                }
                countDownLatch.countDown();
            }));
        }
        threads.forEach(thread -> thread.start());
        countDownLatch.await();


        //打印所有的元素值
        consumer.accept(t);


    }

}

AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

原子修改器,之前所说的,可变类如果采用CAS的方式来进行比较compare,那么最终采用的是==方式,比较的是地址。所以为了针对某一类型中某个元素的改动,引入了原子修改器AtomicReferenceFieldUpdater,可以修改对象内元素。

要被修改的元素一定要标记volatile来保证可见性。

这种修改方式基于反射来进行修改,先生成一个修改器,再使用修改器修改对应的对象属性。

package com.bo.threadstudy.six;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * 原子更新器,来更改对象内部的属性
 * AtomicInteger与AtomicIntegerFieldUpdater虽然功能类似,但是后者有一个好处,在大批量需要对原子整型进行操作时,后者只需创建一个对象就可以
 * 前者需要创建大批量对象,对内存不友好
 */
@Slf4j
public class AtomicReferenceFieldUpdaterTest {

    public static void main(String[] args) {
        Person person = new Person();
        person.setName("123");
        //使用原子修改器对值进行修改,必须要配合volatile类来进行操作,而且不能是private,否则利用反射其不可见
        AtomicReferenceFieldUpdater<Person, String> nameReference = AtomicReferenceFieldUpdater.newUpdater(Person.class, String.class, "name");
        //这里假设采用debug的setValue改造成345,那么输出结果就会是345,而不是456
        log.debug(nameReference.compareAndSet(person,"123","456")+"");
        log.debug(person.toString());


    }

}

@Data
class Person{
    public  volatile String name;

    private Integer age;

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

我当时在修改的时候有个疑问,在AtomicInteger,AtomicLong已经存在的基础上,为什么要增加一个AtomicIntegerFieldUpdater,AtomicLongFieldUpdater呢?后面专门查了下,如果要对许多个Integer对象来进行原子性修改,那么需要创建很多个AtomicInteger对象。而使用修改器,只需要创建一个修改器就好,对内存相对比较友好。

原子累加器

原子累加器,在使用过程中,虽然AtomicLong这些方法存在累加方法,但是和原子累加器相比,性能要低了10倍以上。之所以造成这个的原因在于,使用AtomicLong在使用过程中会出现指令交错,一旦不符合自己的预期值,就会重新循环,造成性能的降低。而LongAddr以及DoubleAddr这些方法,在内部模拟了一个数组,每个线程在自己内部进行累加后,最终发给到这个数组上,统一完成最后的汇总,性能也就较快。

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
源码的话老师讲的没听明白,先过了,不是面试重灾区。

package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

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

/**
 * 原子累加器测试类,和AtomicLong相比,性能较快,
 */
@Slf4j
public class LongAddrTest {

    public static void main(String[] args) throws InterruptedException {
        //只有LongAddr以及DoubleAddr这两种自增
        demo(
                () -> {return new AtomicLong();
        },atomicInteger -> {
                    atomicInteger.getAndIncrement();
                }
        );

        //从纳秒层面来说,性能快了10倍
        //性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加
        //Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性
        //能s
        demo(
                () -> {
                    return new LongAdder();
                },longAdder -> {
                    longAdder.increment();
                }
        );

    }

    //代入两个方法,1方法来提供一个数据,2方法来提供针对于该数据的测试方法以及性能监控
    private static<T> void demo(Supplier<T> supplier, Consumer<T> consumer) throws InterruptedException {
        //得到当前数据
        T t = supplier.get();

        List<Thread> threads = new ArrayList<>();
        CountDownLatch countDownLatch = new CountDownLatch(40);
        long l = System.nanoTime();
        for (int i = 0; i < 40; i++) {
            //线程中记录的方法
            threads.add(new Thread(() -> {
                for (int i1 = 0; i1 < 500000; i1++) {
                    consumer.accept(t);
                }
                countDownLatch.countDown();
            }));
        }
        threads.forEach(thread -> thread.start());
        countDownLatch.await();
        //统计出执行完成的时间
        log.debug( System.nanoTime() - l +"");
    }
}

Unsafe

Unsafe其实是一个基础类,底层基本都是native方法,可以通过反射获取到Unsafe对象,并调用内部方法得到偏移量,可以通过内部的compareAndSwap方法完成CAS操作。

package com.bo.threadstudy.six;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * LongAddr源码先过,面试不是重点,工作中也暂时用不到
 */
@Slf4j
public class UnsafeTest {
    //针对于Unsafe类来做一些操作。源码层面的东西,unsafe内部都是源码层面的东西
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        //cas的样例
//        casTest(unsafe);


    }

    private static void casTest(Unsafe unsafe) throws NoSuchFieldException {
        //通过Unsafe实现CAS操作,普通的CAS采用的是compareAndSet方式来执行
        //需要计算出偏移量
        Field name = Student.class.getDeclaredField("name");
        Field age = Student.class.getDeclaredField("age");
        long nameOffset= unsafe.objectFieldOffset(name);
        long ageOffset = unsafe.objectFieldOffset(age);

        //根据偏移量对结果进行赋值,保证原子操作,可以用debug的setValue试一下
        Student student = new Student();
        while(true){
            //采用setValue对其进行改造过后,初始就不再为null,便会死循环,执行不完
            if(unsafe.compareAndSwapObject(student,nameOffset,null,"张三")
            && unsafe.compareAndSwapObject(student,ageOffset,null,20)){
                log.debug("修改成功");
                break;
            }
        }
    }

}



@Data
class Student{
    private String name;

    private Integer age;

}

也可以通过Unsafe类来自己封装对应的Atomic类,这个其实就是配合CAS来进行使用,也不难。不过如果有其它的业务场景需要好好考虑。

package com.bo.threadstudy.six;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;

/**
 * 自己编写一个AtomicInteger的底层功能
 */
public class UnsafeAtomicTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo atomicIntegerDemo = new AtomicIntegerDemo(10000);
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                atomicIntegerDemo.decrease(2);
            }
            countDownLatch.countDown();
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                atomicIntegerDemo.decrease(2);
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        System.out.println(atomicIntegerDemo.get());

    }
}

class AtomicIntegerDemo{

    private volatile int value;
    private static Unsafe unsafe;
    private static long OFFSET;

    static{
        try {
            Field field= Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe)field.get(null);
            //得到value的便宜量
            OFFSET = unsafe.objectFieldOffset(AtomicIntegerDemo.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public AtomicIntegerDemo(int value){
        this.value = value;
    }

    public int get(){
        return value;
    }

    public void decrease(int i){
        while(true){
            int curValue = get();
            //这么写我个人感觉只是容易理解而已,假如上一步执行完成后,下一步如果发生变化则重新寻新欢
            //如果这不用这个get方法,直接使用value,那么整个操作应该都是原子的,没有问题
            //之所以这么写的原因估计是怕中间有什么用到curValue的操作,在操作完成后通过CAS判断,如果在操作过程发生了线程并发,导致值发生改变,那么CAS代入curValue这就有用了
            if(unsafe.compareAndSwapInt(this,OFFSET,curValue,curValue-i)){
                break;
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值