前言
大家好,牧码心今天给大家推荐一篇并发编程系列(七)—原子类框架Atomic剖析的文章,希望对你有所帮助。内容如下:
- Atomic 概要
- Atomic框架分类
- 基本原理和应用
- 总结
概要
- 同步锁synchronized问题
我们先来看用同步锁synchronized 在多线程环境下实现一个的计数器的实例,实例代码如下:
public class Counter {
private static int count;
public synchronized int getCount(){
System.out.println("当前计数为:"+count);
return count;
}
public synchronized void increment(){
++count;
}
public synchronized void decrement(){
--count;
}
public static void main(String[] args) {
Counter counter=new Counter();
// 累加
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
counter.increment();
counter.getCount();
}
}
});
// 累减
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
counter.decrement();
counter.getCount();
}
}
});
t1.start();
t2.start();
}
}
此实例是通过synchronized 关键字基于内置锁的方式保证线程安全,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,基本满足需求,但也会引发些问题:
1.如果被阻塞的线程优先级很高很重要怎么办?
2.如果获得锁的线程一直不释放锁怎么办?
3.如果有大量的线程来竞争资源,那CPU上下文切换将变得非常频繁,导致CPU使用率飙高,还有可能出现一些例如死锁之类的情况?
4.锁机制是一种比较粗糙,粒度比较大。
因此,对于这种需求我们期待一种更合适、更高效的线程安全机制。这里我们引出Atomic框架。
- 什么Atomic框架
Atomic包它属于java.util.concurrent 并发包下的一个原子操作类的集合,里面包含不同数据类型的原子操作类,基本都依赖于CAS,voliatle,Unsafe等无锁话思想来实现。关于CAS,voliatle的分析详见:
其特点是在多线程的环境下,当前线程执行方法时不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。实际上也是借助硬件的相关指令来实现的,不会阻塞线程。
Atomic 框架分类
Atomic 框架下的类根据数据类型可以分为4中原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用类型和对象属性修改类型等。同时这些类基本都是使用Unsafe实现的包装类。
- 基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
- 数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
- 引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
- 对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater 。
基本原理和应用
Atomic 框架的基本实现都离不开CAS操作和Unsafe类,这里我们从不同类型的原子类来分析常用方法,实现逻辑和基本应用。
基本类型原子类
基本类型原子类包含AtomicInteger, AtomicLong, AtomicBoolean等。这里以 AtomicInteger 为例子来介绍。
- 属性定义
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value属性在AtomicInteger中的偏移量,通过这个偏移量可以快速定位到value字段,这个是实现AtomicInteger的关键。
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value; // 使用volatile修饰可以保证咋多线程下操作时内存的可见性
-
常用方法:
int addAndGet(int delta);//以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果 boolean compareAndSet(int expect, int update) ;//如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。 int incrementAndGet() ;// 以原子方式将当前值加 1,并返回加1后的值。等价于“++num” int getAndIncrement();//以原子方式将当前值加1,注意:这里返回的是自增前的值。 void lazySet(int newValue);//最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 int getAndSet(int newValue);//以原子方式设置为newValue的值,并返回旧值。
这里以incrementAndGet()为例,对AtomicInteger 的原理进行说明。incrementAndGet() 的源码(JDK1.8)如下:
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
方法内部调用Unsafe类中的getAndAddInt()方法,我们看一下getAndAddInt()源码:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
具体说明:
- this.getIntVolatile:从主内存中获取变量最新的值。
- compareAndSwapInt:CAS操作,CAS的原理是拿期望的值和需要更新的值作比较,如果相同则更新成新的值,可以确保在多线程情况下只有一个线程会操作成功,不成功的返回false。
- do-while循环,compareAndSwapInt返回false之后,会再次从主内存中获取变量的值,继续做CAS操作,直到成功为止。
-
实例演示
使用AtomicInteger实现网站访问量计数器功能,模拟100人同时访问网站,每个人访问10次,代码如下:
public class Demo {
//访问次数
static AtomicInteger count = new AtomicInteger();
//模拟访问一次
public static void request() throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//对count原子+1
count.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
}
}
数组类型原子类
通过原子的方式更新数组里的某个元素,Atomic包提供了以下三个类: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray等。这里以 AtomicIntegerArray为例子来介绍。
-
常用方法:
AtomicIntegerArray(int [] array);// 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素。 int addAndGet(int i, int delta);// 以原子方式将给定值添加到索引 i 的元素。 boolean compareAndSet(int i, int expect, int update);// 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。 int decrementAndGet(int i);// 以原子方式将索引 i 的元素减1。 int get(int i);// 获取位置 i 的当前值。 int getAndAdd(int i, int delta);// 以原子方式将给定值与索引 i 的元素相加。 int getAndDecrement(int i);// 以原子方式将索引 i 的元素减 1。 int incrementAndGet(int i);// 以原子方式将索引 i 的元素加1。
-
实现逻辑
这里以incrementAndGet()方法为例,对AtomicIntegerArray的原理进行说明。incrementAndGet() 的源码(JDK1.8)如下:public final int incrementAndGet(int i) { return getAndAdd(i, 1) + 1; } public final int getAndAdd(int i, int delta) { return unsafe.getAndAddInt(array, checkedByteOffset(i), delta); }
方法内部调用Unsafe类中的getAndAddInt()方法,原理与AtomicInteger相同,不同的是需要检查数组是否越界(见)。如果没有越界的话,则先获取数组索引i的值;然后通过CAS函数更新i的值。
// 检查数组是否越界 private long checkedByteOffset(int i) { if (i < 0 || i >= array.length) throw new IndexOutOfBoundsException("index " + i); return byteOffset(i); }
-
实例演示
统计网站页面访问量,假设网站有10个页面,现在模拟100个人并行访问每个页面10次,然后将每个页面访问量输出,应该每个页面都是1000次,代码如下:
public class Demo2 {
static AtomicIntegerArray pageRequest = new AtomicIntegerArray(new int[10]);
/**
* 模拟访问一次
* @param page 访问第几个页面
* @throws InterruptedException
*/
public static void request(int page) throws InterruptedException {
//模拟耗时5毫秒
TimeUnit.MILLISECONDS.sleep(5);
//pageCountIndex为pageCount数组的下标,表示页面对应数组中的位置
int pageCountIndex = page - 1;
pageRequest.incrementAndGet(pageCountIndex);
}
public static void main(String[] args) throws InterruptedException {
long starTime = System.currentTimeMillis();
int threadSize = 100;
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
// 模拟100个人并行访问每个页面10次
for (int i = 0; i < threadSize; i++) {
Thread thread = new Thread(() -> {
try {
for (int page = 1; page <= 10; page++) {
for (int j = 0; j < 10; j++) {
request(page);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
thread.start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime));
for (int pageIndex = 0; pageIndex < 10; pageIndex++) {
System.out.println("第" + (pageIndex + 1) + "个页面访问次数为" + pageRequest.get(pageIndex));
}
}
}
引用类型原子类
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类,引用类型原子类包含AtomicReference, AtomicStampedRerence, AtomicMarkableReference等。这里以AtomicReference为例子来介绍。
- 初始化和属性定义
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile V value;
- 常用方法
AtomicReference(V initialValue);//使用给定的初始值创建新的 AtomicReference。
boolean compareAndSet(V expect, V update); // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
V get();// 获取当前值。
V getAndSet(V newValue);// 以原子方式设置为给定值,并返回旧值。
void set(V newValue);// 设置为给定值。
-
实现逻辑
从AtomicReference的属性定义和常用方法,AtomicReference 和 AtomicInteger 非常类似,不同之处在于 AtomicInteger是对整数的封装,而AtomicReference则是对应普通的对象引用,它可以确保你在修改对象引用时的线程安全性。实现逻辑也是通过volatile和Unsafe提供的CAS函数实现原子操作。 -
实例演示
模拟AtomicReference 修改对象Person对象的属性
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Person p1=new Person(10);
Person p2=new Person(20);
//创建
AtomicReference reference=new AtomicReference(p1);
//通过CAS设置reference,若reference的原始值为期望值p1,则设置为p2
reference.compareAndSet(p1,p2);
Person p3=(Person) reference.get();
System.out.println("p3 is "+p3);
System.out.println("p3.equals(p1)="+p3.equals(p1));
}
// Person对象定义
class Person{
volatile long id;
public Person(long id) {
this.id = id;
}
public String toString() {
return "id:"+id;
}
}
}
对象的属性修改类型原子类
如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改原子类,其包含AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater等。这些类提供的方法几乎相同,这里以AtomicIntegerFieldUpdater为例子来介绍。
- 初始化和属性定义
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName)
说明:通过静态方法newUpdater可初始化AtomicIntegerFieldUpdater对象,参数包含:
- tclass:需要更新的字段所在的目标类;
- fieldName:需要更新的字段名称
- 常用方法:
int addAndGet(T obj, intdelta);// 以原子方式将给定值添加到此更新器管理的给定对象的字段的当前值。
abstract boolean compareAndSet(T obj, intexpect, intupdate);// 如果当前值 == 预期值,则以原子方式将此更新器所管理的给定对象的字段设置为给定的更新值。
int decrementAndGet(T obj);// 以原子方式将此更新器管理的给定对象字段当前值减 1。
int getAndAdd(T obj, int delta);// 以原子方式将给定值添加到此更新器管理的给定对象的字段的当前值。
int getAndDecrement(T obj);// 以原子方式将此更新器管理的给定对象字段当前值减 1。
int getAndIncrement(T obj);// 以原子方式将此更新器管理的给定对象字段的当前值加 1。
int incrementAndGet(T obj);// 以原子方式将此更新器管理的给定对象字段当前值加 1。
- 实现逻辑
这里以incrementAndGet()为例,对AtomicIntegerFieldUpdater的实现逻辑进行说明。incrementAndGet() 的源码(JDK1.8)如下:
public int incrementAndGet(T obj) {
int prev, next;
do {
prev = get(obj);
next = prev + 1;
} while (!compareAndSet(obj, prev, next));
return next;
}
- 实例演示
模拟AtomicIntegerFieldUpdater 修改Person对象的字段属性
public class Demo4{
public static void main(String[] args) {
// 获取Person的class对象
Class cls = Person.class;
// 新建AtomicLongFieldUpdater对象,传递参数是“class对象”和“long类型在类中对应的名称”
AtomicIntegerFieldUpdater mAtoLong = AtomicIntegerFieldUpdater.newUpdater(cls, "id");
Person person = new Person(123);
// 比较person的"id"属性,如果id的值为123,则设置为1000。
mAtoLong.compareAndSet(person, 123, 1000);
System.out.println("id="+person.getId());
}
}
class Person {
volatile int id;
public Person(intid) {
this.id = id;
}
public void setId(intid) {
this.id = id;
}
public intgetId() {
return id;
}
}
总结
本文介绍了原子类框架Atomic的基本分类,实现原理和应用。那原子类框架Atomic如何实现线程安全呢?其实我们在语言层面是没有做任何同步的操作的,可以看到源码没有任何锁加在上面,这就是Atomic包下这些类的奥秘:语言层面不做处理,我们将其交给硬件—CPU和内存,利用CPU的多处理能力,实现硬件层面的阻塞,再加上volatile变量的特性即可实现基于原子操作的线程安全。所以说,原子类框架Atomic的底层并不是无阻塞,只是阻塞并非在语言、线程方面,而是在硬件层面,所以无疑这样的操作会更快更高效!