并发编程系列之掌握原子类使用
学习目标:
原子类是jdk的juc包中提供的对单个变量进行无锁、线程安全修改的工具类。
juc中提供的锁,能很好地保证线程安全,但是在高并发的情况下,可能不能保证高性能,所以适当地使用原子类,有时候是可以提高性能
主要用途,对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
提供对引用类型变量的原子更新
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='***'}
提供对对应类型数组的原子更新
基于反射的,对应类型的属性原子更新器。使用规则:
public/protected/private
static
关键字AtomicIntegerFieldUpdater
和AtomicLongFieldUpdater
只能修改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
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的原子类里,提供了AtomicStampedReference
和AtomicMarkableReference
来处理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();
}
}