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

x33g5p2x  于2021-12-30 转载在 其他  
字(5.7k)|赞(0)|评价(0)|浏览(320)

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

学习目标:

  • 知道什么是原子类和用途
  • 掌握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
User{username='jack', password='***'}
false
User{username='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();
    
    }
}

相关文章