Preface

When using multi-threaded concurrent programming, we often encounter modification operations on shared variables. At this point we can choose ConcurrentHashMap, ConcurrentLinkedQueue to store data safely. But if it only involves state modification and thread execution order issues, using atomic components starting with Atomic or synchronization components such as ReentrantLock and CyclicBarrier will be a better choice. The principles and usage of them will be introduced one by one below.

  • The realization principle of atomic components CAS
  • The usage of atomic components such as AtomicBoolean, AtomicIntegerArray,
  • The realization principle of the synchronization component
  • Usage of synchronization components such as ReentrantLock and CyclicBarrier

Follow the public account, communicate together, search WeChat: sneak forward

github address , thanks star

The realization principle of atomic components CAS

Application scenario

  • It can be used to realize the atomic operation of variables and states in multiple threads
  • Can be used to implement synchronization locks (ReentrantLock)

Atomic component

  • The atomic operation of atomic components is achieved by using cas to manipulate volatile variables.
  • Volatile type variables ensure that when the variable is modified, other threads can see the latest value
  • cas guarantees that the value modification operation is atomic and will not be interrupted

    Basic type atomic class

    AtomicBoolean //布尔类型
    AtomicInteger //正整型数类型
    AtomicLong      //长整型类型
  • Example of use

    public static void main(String[] args) throws Exception {
        AtomicBoolean atomicBoolean = new AtomicBoolean(false);
        //异步线程修改atomicBoolean
        CompletableFuture<Void> future = CompletableFuture.runAsync(() ->{
       try {
           Thread.sleep(1000); //保证异步线程是在主线程之后修改atomicBoolean为false
           atomicBoolean.set(false);
       }catch (Exception e){
           throw new RuntimeException(e);
       }
        });
        atomicBoolean.set(true);
        future.join();
        System.out.println("boolean value is:"+atomicBoolean.get());
    }
    ---------------输出结果------------------
    boolean value is:false

Reference class atomic class

AtomicReference
//加时间戳版本的引用类原子类
AtomicStampedReference
//相当于AtomicStampedReference,AtomicMarkableReference关心的是
//变量是否还是原来变量,中间被修改过也无所谓
AtomicMarkableReference
  • AtomicReference source below, which defines an internal volatile V value , and means VarHandle (particularly subclass FieldInstanceReadWrite) atomicity operation, MethodHandles will help calculate the offset value in the class, the last call Unsafe. In VarHandle public final native boolean compareAndSetReference(Object o, long offset, Object expected, Object x) atomic modify the object Attributes

    public class AtomicReference<V> implements java.io.Serializable {
        private static final long serialVersionUID = -1848883965231344442L;
        private static final VarHandle VALUE;
        static {
       try {
           MethodHandles.Lookup l = MethodHandles.lookup();
           VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
       } catch (ReflectiveOperationException e) {
           throw new ExceptionInInitializerError(e);
       }
        }
        private volatile V value;
        ....

    ABA problem

  • Thread X prepares to change the value of the variable from A to B, but during this period, thread Y changes the value of the variable from A to C, and then to A; finally, thread X detects that the variable value is A and replaces it with B. But in fact, A is no longer the original A
  • The solution is to make the variable a unique type. The value can be added with a version number or a timestamp. If the version number is added, the modification of thread Y becomes A1->B2->A3. At this time, thread X is updated again and it can be judged that A1 is not equal to A3
  • The implementation of AtomicStampedReference is similar to that of AtomicReference, but its atomically modified variable is volatile Pair<V> pair; , and Pair is its internal class. AtomicStampedReference can be used to solve the ABA problem

    public class AtomicStampedReference<V> {
        private static class Pair<T> {
       final T reference;
       final int stamp;
       private Pair(T reference, int stamp) {
           this.reference = reference;
           this.stamp = stamp;
       }
       static <T> Pair<T> of(T reference, int stamp) {
           return new Pair<T>(reference, stamp);
       }
        }
        private volatile Pair<V> pair;
  • If we do not care whether the variable has been modified in the intermediate process, but only care whether the current variable is still the original variable, we can use AtomicMarkableReference
  • AtomicStampedReference usage example

    public class Main {
        public static void main(String[] args) throws Exception {
       Test old = new Test("hello"), newTest = new Test("world");
       AtomicStampedReference<Test> reference = new AtomicStampedReference<>(old, 1);
       reference.compareAndSet(old, newTest,1,2);
       System.out.println("对象:"+reference.getReference().name+";版本号:"+reference.getStamp());
        }
    }
    class Test{
        Test(String name){ this.name = name; }
        public String name;
    }
    ---------------输出结果------------------
    对象:world;版本号:2

    Array atom class

    AtomicIntegerArray     //整型数组
    AtomicLongArray         //长整型数组
    AtomicReferenceArray    //引用类型数组
  • A final array is initialized inside the array atom class, which treats the entire array as an object, and then calculates the element offset according to the subscript index, and then calls UNSAFE.compareAndSetReference to perform atomic operations. The array is not modified by volatile. In order to ensure that the element type is visible in different threads, the UNSAFE public native Object getReferenceVolatile(Object o, long offset) method is used to obtain the element value in real time.
  • Example of use

    //元素默认初始化为0
    AtomicIntegerArray array = new AtomicIntegerArray(2);
    // 下标为0的元素,期待值是0,更新值是1
    array.compareAndSet(0,0,1);
    System.out.println(array.get(0));
    ---------------输出结果------------------
    1

Attribute atom class

AtomicIntegerFieldUpdater 
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
  • If the operation object is a certain type of attribute, you can use AtomicIntegerFieldUpdater to update atomically, but the attribute of the class needs to be defined as a volatile modified variable to ensure the visibility of the attribute in each thread, otherwise an error will be reported
  • Example of use

    public class Main {
        public static void main(String[] args) {
       AtomicReferenceFieldUpdater<Test,String> fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Test.class,String.class,"name");
       Test test = new Test("hello world");
       fieldUpdater.compareAndSet(test,"hello world","siting");
       System.out.println(fieldUpdater.get(test));
       System.out.println(test.name);
        }
    }
    class Test{
        Test(String name){ this.name = name; }
        public volatile String name;
    }
    ---------------输出结果------------------
    siting
    siting

accumulator

Striped64
LongAccumulator
LongAdder
//accumulatorFunction:运算规则,identity:初始值
public LongAccumulator(LongBinaryOperator accumulatorFunction,long identity)
  • LongAccumulator and LongAdder are both inherited from Striped64. The main idea of Striped64 is similar to ConcurrentHashMap. When the concurrency of calculation of a single variable is slow, we can disperse the mathematical operations on multiple variables. When we need to calculate the total value, we need Add up
  • LongAdder is equivalent to a special case implementation of LongAccumulator
  • Examples of LongAccumulator

    public static void main(String[] args) throws Exception {
        LongAccumulator accumulator = new LongAccumulator(Long::sum, 0);
        for(int i=0;i<100000;i++){
       CompletableFuture.runAsync(() -> accumulator.accumulate(1));
        }
        Thread.sleep(1000); //等待全部CompletableFuture线程执行完成,再获取
        System.out.println(accumulator.get());
    }
    ---------------输出结果------------------
    100000

    The realization principle of the synchronization component

  • Most synchronous components of java maintain a state value internally. Like atomic components, modifying the state value is generally implemented through cas. The maintenance of state modification is abstracted by Doug Lea from AbstractQueuedSynchronizer (AQS) to achieve
  • The principle of AQS can be seen in an article written before: detailed unlocking principle, synchronized, volatile+cas bottom layer realization

Synchronization component

ReentrantLock、ReentrantReadWriteLock

  • ReentrantLock and ReentrantReadWriteLock are implemented based on AQS (AbstractQueuedSynchronizer). Because they have a distinction between fair lock and unfair lock, they do not directly inherit AQS, but use internal classes to inherit. Fair locks and unfair locks implement AQS respectively, and ReentrantLock and ReentrantReadWriteLock use internal classes to achieve synchronization.
  • Example of using ReentrantLock

    ReentrantLock lock = new ReentrantLock();
    if(lock.tryLock()){
        //业务逻辑
        lock.unlock();
    }
  • Example of using ReentrantReadWriteLock

    public static void main(String[] args) throws Exception {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        if(lock.readLock().tryLock()){ //读锁
       //业务逻辑
       lock.readLock().unlock();
        }
        if(lock.writeLock().tryLock()){ //写锁
       //业务逻辑
       lock.writeLock().unlock();
        }
    }

    Semaphore implementation principle and usage scenarios

  • Semaphore, like ReentrantLock, also has a fair and unfair competition lock strategy. It also implements synchronization by inheriting AQS from internal classes.
  • Popular explanation: Suppose there is a well, and there are at most three people to fetch water. Everyone who fetches water needs to occupy a place. When the three positions are all occupied, the fourth person needs to fetch water, and you must wait for one of the first three people to leave the water level before you can continue to get the water fetching position
  • Example of use

    public static void main(String[] args) throws Exception {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 3; i++)
       CompletableFuture.runAsync(() -> {
           try {
               System.out.println(Thread.currentThread().toString() + " start ");
               if(semaphore.tryAcquire(1)){
                   Thread.sleep(1000);
                   semaphore.release(1);
                   System.out.println(Thread.currentThread().toString() + " 无阻塞结束 ");
               }else {
                   System.out.println(Thread.currentThread().toString() + " 被阻塞结束 ");
               }
           } catch (Exception e) {
               throw new RuntimeException(e);
           }
       });
        //保证CompletableFuture 线程被执行,主线程再结束
        Thread.sleep(2000);
    }
    ---------------输出结果------------------
    Thread[ForkJoinPool.commonPool-worker-19,5,main] start 
    Thread[ForkJoinPool.commonPool-worker-5,5,main] start 
    Thread[ForkJoinPool.commonPool-worker-23,5,main] start 
    Thread[ForkJoinPool.commonPool-worker-23,5,main] 被阻塞结束 
    Thread[ForkJoinPool.commonPool-worker-5,5,main] 无阻塞结束 
    Thread[ForkJoinPool.commonPool-worker-19,5,main] 无阻塞结束 
  • It can be seen that there are three threads. Because the semaphore is set to 2, the third thread cannot obtain information successfully, and will print the end of the block.

    CountDownLatch implementation principle and usage scenarios

  • CountDownLatch is also a synchronous operation implemented by AQS
  • Popular explanation: When playing a game, if the main task needs to complete five small tasks, the main task can continue. At this time, you can use CountDownLatch, the main line task is blocked and waiting, every time a small task is completed, it is done once and the main line cannot be triggered until all five small tasks are executed.
  • Example of use

    public static void main(String[] args) throws Exception {
        CountDownLatch count = new CountDownLatch(2);
        for (int i = 0; i < 2; i++)
       CompletableFuture.runAsync(() -> {
           try {
               Thread.sleep(1000);
               System.out.println(" CompletableFuture over ");
               count.countDown();
           } catch (Exception e) {
               throw new RuntimeException(e);
           }
       });
        //等待CompletableFuture线程的完成
        count.await();
        System.out.println(" main over ");
    }
    ---------------输出结果------------------
     CompletableFuture over 
     CompletableFuture over 
     main over 

    CyclicBarrier implementation principle and usage scenarios

  • CyclicBarrier relies on ReentrantLock lock and Condition trip attributes to achieve synchronization
  • Popular explanation: CyclicBarrier needs to block all threads to the await state, and then all threads are awakened for execution. Imagine that there is a railing blocking five sheep. When the five sheep stand together on the railing, the railing will be pulled up. At this time, all the sheep can fly out of the sheep pen.
  • Example of use

    public static void main(String[] args) throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(2);
        CompletableFuture.runAsync(()->{
       try {
           System.out.println("CompletableFuture run start-"+ Clock.systemUTC().millis());
           barrier.await(); //需要等待main线程也执行到await状态才能继续执行
           System.out.println("CompletableFuture run over-"+ Clock.systemUTC().millis());
       }catch (Exception e){
           throw new RuntimeException(e);
       }
        });
        Thread.sleep(1000);
        //和CompletableFuture线程相互等待
        barrier.await();
        System.out.println("main run over!");
    }
    ---------------输出结果------------------
    CompletableFuture run start-1609822588881
    main run over!
    CompletableFuture run over-1609822589880

    StampedLock

  • StampedLock does not rely on AQS, but maintains multiple state values internally and implements it with cas
  • StampedLock has three modes: write mode, read mode, and optimistic read mode
  • StampedLock's read-write lock can be converted to each other

    //获取读锁,自旋获取,返回一个戳值
    public long readLock()
    //尝试加读锁,不成功返回0
    public long tryReadLock()
    //解锁
    public void unlockRead(long stamp) 
    //获取写锁,自旋获取,返回一个戳值
    public long writeLock()
    //尝试加写锁,不成功返回0
    public long tryWriteLock()
    //解锁
    public void unlockWrite(long stamp)
    //尝试乐观读读取一个时间戳,并配合validate方法校验时间戳的有效性
    public long tryOptimisticRead()
    //验证stamp是否有效
    public boolean validate(long stamp)
  • Usage example

    public static void main(String[] args) throws Exception {
        StampedLock stampedLock = new StampedLock();
        long stamp = stampedLock.tryOptimisticRead();
        //判断版本号是否生效
        if (!stampedLock.validate(stamp)) {
       //获取读锁,会空转
       stamp = stampedLock.readLock();
       long writeStamp = stampedLock.tryConvertToWriteLock(stamp);
       if (writeStamp != 0) { //成功转为写锁
           //fixme 业务操作
           stampedLock.unlockWrite(writeStamp);
       } else {
           stampedLock.unlockRead(stamp);
           //尝试获取写读
           stamp = stampedLock.tryWriteLock();
           if (stamp != 0) {
               //fixme 业务操作
               stampedLock.unlockWrite(writeStamp);
           }
       }
        }
    }    

Welcome refers to the error in the text

Reference article


cscw
160 声望107 粉丝

思君