【java并发工具类-互斥(无锁)】原子类

1.无锁方案

1.1-使用无锁方案-原子类保证原子性

前面解决原子性问题,比如count+=1问题,都是使用互斥中的互斥锁来解决的,其实对于简单的原子性问题,还有一种**无锁方案,**Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。

下面我们用原子类AtomicLong来实现count+=1的问题。

public class Test {
  AtomicLong count = new AtomicLong(0);
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count.getAndIncrement();
    }
  }
}

1.2-无锁方案相对互斥锁

无锁方案相比互斥锁,最大的好处就是性能,互斥锁为了保证互斥性,需要执行加锁解锁操作,而加锁解锁本身就比较消耗性能;同时拿不到锁的线程会陷入阻塞状态,同时触发线程切换,线程切换对线程的消耗也比较大。相比较,无锁方案没哟加锁解锁的消耗,同时还能保持互斥性。那么它是怎么实现的,下面讲解它的原理。

1.3-无锁方案原理

CPU为了解决并发问题,提供了CAS指令,作为一条CPU指令,本身可以保证原子性。

CAS,全称是 Compare And Swap,即“比较并交换”)。
CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;
并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。
具体相关CAS更多问题可以查看这篇文章synchronized的实现原理

2.原子类概览

Java SDK并发包中提供的原子类很多,可以将其分成五个类:

  • 原子化的基本数据类型
  • 原子化的对象引用类型
  • 原子化数组
  • 原子化对象属性更新器
  • 原子化的累加器
    在这里插入图片描述
    下面根据每类来简单说明一下。

2.1-原子化的基本数据类型

相关实现有AtomicBoolean、AtomicInteger 和 AtomicLong,提供的方法主要有下面这几个:

getAndIncrement() //原子化i++
getAndDecrement() //原子化的i--
incrementAndGet() //原子化的++i
decrementAndGet() //原子化的--i
getAndAdd(delta) //当前值+=delta,返回+=前的值
addAndGet(delta)//当前值+=delta,返回+=后的值
compareAndSet(expect, update)//CAS操作,返回是否成功
//以下四个方法,新值可以通过传入func函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)//对当前值和x进行func运算
accumulateAndGet(x,func)

2.2-原子化的对象引用类型

相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。
AtomicReference 提供的方法和原子化的基本数据类型差不多。不过需要注意的是,对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。

解决 ABA 问题的思路其实很简单,增加一个版本号维度就可以了,这个在 StampedLock:比读写锁更快的锁?介绍的乐观锁机制很类似,每次执行 CAS 操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来(版本号递增的)。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,方法签名如下:

boolean compareAndSet(
  V expectedReference,
  V newReference,
  int expectedStamp,
  int newStamp) 

AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下:

boolean compareAndSet(
  V expectedReference,
  V newReference,
  boolean expectedMark,
  boolean newMark)

2.3-原子化数组

相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,
利用这些原子类,**我们可以原子化地更新数组里面的每一个元素。**这些类提供的方法和原子化的基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数,所以这里也不再赘述了。

2.4-原子化对象属性更新器

相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的。

需要注意的是,对象属性必须是 volatile 类型的,只有这样才能保证可见性,并且对象属性不能是private的;如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。
下面举例来看一下:

public class AtomicReferTest {
    public static void main(String[] args) throws Exception  
    { //可以看到创建的对象属性更新器并没有传入对象参数,更改对象属性,必须有对象把?      AtomicReferenceFieldUpdaterupdater=AtomicReferenceFieldUpdater.newUpdater(Dog.class,String.class,"name");  
        Dog dog1=new Dog();  
        //其实它是在执行compareAndSet方法中传入的对象。
        System.out.println(updater.compareAndSet(dog1,"dog1","compareAndSet"));   //true     
        System.out.println(dog1.name);  		//compareAndSet
        System.out.println(updater.getAndSet(dog1, "getAndSet")); //compareAndSet
        System.out.println(dog1.name);//getAndSet
    }  
}
class Dog  
{  
     volatile  String name="dog1";  
}  

2.5-原子化的累加器

DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。

2.6-总结

无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)

参考:极客时间
更多:邓新

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/105191153