并发编程系列之掌握原子类使用

Posted smileNicky

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程系列之掌握原子类使用相关的知识,希望对你有一定的参考价值。

并发编程系列之掌握原子类使用

学习目标:

  • 知道什么是原子类和用途
  • 掌握juc中原子类使用
  • 了解原子类的实现原理

1、什么是原子类?

原子类是jdk的juc包中提供的对单个变量进行无锁线程安全修改的工具类。

juc中提供的锁,能很好地保证线程安全,但是在高并发的情况下,可能不能保证高性能,所以适当地使用原子类,有时候是可以提高性能

2、掌握原子类api

2.1、AtomicInteger/AomicLong/AtomicBoolean

主要用途,对int,long变量提供原子更新,典型的应用场景是计数、计票等

AtomicInteger例子,使用AtomicInteger进行统计计数


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

public class AtomicIntegerExample 

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static Integer count() 
        return atomicInteger.getAndIncrement();
    
    
    public static void main(String[] args) throws InterruptedException 
        int threadSize = 500;
        final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        long start = System.currentTimeMillis();
        for (int i = 0 ; i < threadSize ; i ++ ) 
            new Thread(() -> 
                for (int n = 0 ; n < 10_000 ; n++) 
                    count();
                
                countDownLatch.countDown();
            ).start();
        
        countDownLatch.await();
        System.out.println("统计计数:" + atomicInteger.get());
    



统计计数:5000000
耗时:344ms

不过在前面我们学习了juc中的各种锁,如果不使用原子类,使用锁来实现,性能如何?

private static volatile int count = 0;
private static ReentrantLock reentrantLock = new ReentrantLock();

public static Integer countLock()
   reentrantLock.lock();
    try 
        count ++;
     finally 
        reentrantLock.unlock();
    
    return count;

统计计数:5000000
耗时:667ms
打印多几次,发现加锁的情况统计是相对比较慢的,不过也能保证统计的线程安全

如果不加锁,仅仅使用volatile关键字?运行几次,发现统计的数值是有偏差的,所以volatile是不一定能保证线程安全的

统计计数:4128826
耗时:332ms

2.2、AtomicReference

提供对引用类型变量的原子更新


import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceExample 

    public static void main(String[] args) 
        User user1 = new User("tom", "***");
        User user2 = new User("jack", "***");
        User user3 = new User("admin", "***");
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user1);

        System.out.println(atomicReference.compareAndSet(user1 , user2));
        System.out.println(atomicReference.get());

        System.out.println(atomicReference.compareAndSet(user1, user3));
        System.out.println(atomicReference.get());

    

    static class User 
        private String username;
        private String password;
        User(String username , String password) 
            this.username = username;
            this.password = password;
        
        @Override
        public String toString() 
            return "User" +
                    "username='" + username + '\\'' +
                    ", password='" + password + '\\'' +
                    '';
        
    


 true
Userusername='jack', password='***'
false
Userusername='jack', password='***'

2.3 AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray

提供对对应类型数组的原子更新

2.4 AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicReferenceFieldUpdater

基于反射的,对应类型的属性原子更新器。使用规则:

  • 字段必须是volatile类型的,在线程之间共享变量时保证可见性
  • 在执行更新代码中一定要保证能直接访问到该变量,不管字段修饰符是public/protected/private
  • 对于父类的字段,子类是不能直接操作的,尽管子类可以访问到父类的字段
  • 只要是实例变量,不能是类变量,也就是说不能加static关键字
  • 对于AtomicIntegerFieldUpdaterAtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型Integer/Long,如果要修改包装类型就需要使用AtomicReferenceFieldUpdater
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicFieldUpdaterExample 

    static class ParentBean 
        volatile int count;
    

    static class DemoBean extends ParentBean 
        volatile int count;
        public DemoBean ()
            this.count = 0;
        
        public int getCount ()
            return count;
        
    

    public static void main(String[] args) throws InterruptedException 
        DemoBean obj = new DemoBean();
        AtomicIntegerFieldUpdater<DemoBean> atomicIntegerFieldUpdater =
                AtomicIntegerFieldUpdater.newUpdater(DemoBean.class , "count");
        int threadSize = 50;
        CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        for (int t = 0 ; t < threadSize ; t++) 
            new Thread(()->
                for (int i = 0 ;i < 10_000; i ++) 
                    atomicIntegerFieldUpdater.incrementAndGet(obj);
                
                countDownLatch.countDown();
            ).start();
        
        countDownLatch.await();
        System.out.println("统计计数:"+obj.getCount());
    


如果不加上volatile关键字,出现错误,验证了上面的规则,其它规则也可以一个一个验证

Exception in thread “main” java.lang.IllegalArgumentException: Must be volatile type

2.5、什么是ABA问题?

ABA问题:举个例子来说明,在并发多线程环境,有个线程t1对变量进行修改改为A,然后改为B,接着又改回A,另外一个线程t2获取变量的值,做一系列操作,比如A改为C,但是获取的变量值为A,这个值可能是第一个,t1没修改之前的值,也有可能是t1修改后第一个A值,如果业务处理不允许这样的,就会有ABA问题

例子,下面使用AtomicInteger进行模拟


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

public class AtomicStampedReferenceExample 
	// 初始值为100
    private static AtomicInteger atomicInteger = new AtomicInteger(100);
 

    public static void main(String[] args) throws InterruptedException 
		// 线程t1进行两次修改,模拟ABA问题
        Thread t1 = new Thread(()->
            atomicInteger.compareAndSet(100 , 101);
            atomicInteger.compareAndSet(101,100);
        );
        // t2线程获取值,进行修改
        Thread t2 = new Thread(()->
            try 
            	// 线程睡眠1s
                TimeUnit.SECONDS.sleep(1);
             catch (InterruptedException e) 
            
            boolean flag = atomicInteger.compareAndSet(100,102);
            // true,说明不能识别处理ABA问题
            System.out.println(flag);
        );
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    


不过在JUC的原子类里,提供了AtomicStampedReferenceAtomicMarkableReference来处理ABA问题:

  • AtomicStampedReference:加入了int类型的邮戳(版本号),记录了变更的次数,来处理ABA问题
  • AtomicMarkableReference:加入了一个boolean类型的标识,标识值是否变更了,来处理ABA问题

使用AtomicStampedReference处理ABA问题

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

public class AtomicStampedReferenceExample 

    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,0);

    public static void main(String[] args) throws InterruptedException 

        Thread tt1 = new Thread(()->
        	// 每次修改都加上邮戳值,邮戳值加1
            atomicStampedReference.compareAndSet(100 , 101,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            atomicStampedReference.compareAndSet(101, 100 ,
                    atomicStampedReference.getStamp() , atomicStampedReference.getStamp() + 1);
        );
        Thread tt2 = new Thread(()->
            // 获取邮戳版本号
            int stamp = atomicStampedReference.getStamp();
            try 
                TimeUnit.SECONDS.sleep(1);
             catch (InterruptedException e) 
            
            // false,因为记录了变更次数邮戳值,所以可以处理ABA问题
            boolean flag = atomicStampedReference.compareAndSet(100,102 ,
                    stamp , stamp+1);
            System.out.println(flag);
        );
        tt1.start();
        tt2.start();
    
    


以上是关于并发编程系列之掌握原子类使用的主要内容,如果未能解决你的问题,请参考以下文章

并发编程系列之掌握LockSupport的用法

并发编程系列之掌握LockSupport的用法

并发编程之原子类

java并发编程之工具类

掌握系列之并发编程-1.并发基础

Java——多线程高并发系列之理解CAS原子变量类的使用