并发编程系列之掌握原子类使用
学习目标:
- 知道什么是原子类和用途
- 掌握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 关键字 - 对于
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
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 {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
atomicInteger.compareAndSet(100 , 101);
atomicInteger.compareAndSet(101,100);
});
Thread t2 = new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
boolean flag = atomicInteger.compareAndSet(100,102);
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(()->{
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) {
}
boolean flag = atomicStampedReference.compareAndSet(100,102 ,
stamp , stamp+1);
System.out.println(flag);
});
tt1.start();
tt2.start();
}
}
|