> 文档中心 > 线程共享模型----之----无锁(三)

线程共享模型----之----无锁(三)

目录

线程共享模型总目录

3.1 CAS

3.2 原子整数

3.3 原子引用

3.4 原子数组

3.5 字段更新器

3.6 原子累加器

3.7 Unsafe


无锁与管程的区别:

  • 管程 ---- 悲观锁 ---- 阻塞
  • 无锁 ---- 乐观锁 ---- 非阻塞

        独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。CAS 就是一种乐观锁。

3.1 CAS

CAS, compare and swap 的缩写,中文翻译成比较并交换。是一种通过无锁的方式来保护共享变量线程安全问题的机制。其实现可如下:(其中 balance 是用 AtomicInteger 修饰的变量

/*    将初始余额1000元, 每次 -10 元, 如果启动100个线程, 则余额应该变成0元, 以下是减一次的操作*/public void withdraw(Integer amount) {     while(true) {  // 需要不断尝试,直到成功为止  while (true) {      // 比如拿到了旧值 1000      int prev = balance.get();      // 在这个基础上 1000-10 = 990      int next = prev - amount;      /*      compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值      - 不一致了,next 作废,返回 false 表示失败      比如,别的线程已经做了减法,当前值已经被减成了 990      那么本线程的这次 990 就作废了,进入 while 下次循环重试      - 一致,以 next 设置为新值,返回 true 表示成功      */      if (balance.compareAndSet(prev, next)) {   break;      }  }     }}

其中的关键是 compareAndSet,它的简称就是 CAS,它必须是原子操作。其底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证比较-交换的原子性。

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

volatile 回顾:
        获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。但需要注意的是:volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(即不能保证原子性)

当线程数不多于cpu核心数时,无锁的效率要高于加锁的效率,这是为什么呢?
        原因是,无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
        打个比喻,线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速...
恢复到高速运行,代价比较大。但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换,所以在线程数少于cpu核心数时,无锁的效率就会更高。

3.2 原子整数

J.U.C 并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以 AtomicInteger 为例:(其中 i 是用 AtomicInteger 修饰的变量)

AtomicInteger i = new AtomicInteger(0);

// 获取并自增( i = 0, 结果 i = 1, 返回 0 ),类似于 i++ System . out . println ( i . getAndIncrement ()); // 自增并获取( i = 1, 结果 i = 2, 返回 2 ),类似于 ++i System . out . println ( i . incrementAndGet ()); // 自减并获取( i = 2, 结果 i = 1, 返回 1 ),类似于 --i System . out . println ( i . decrementAndGet ()); // 获取并自减( i = 1, 结果 i = 0, 返回 1 ),类似于 i-- System . out . println ( i . getAndDecrement ()); // 获取并加值( i = 0, 结果 i = 5, 返回 0 System . out . println ( i . getAndAdd ( 5 )); // 加值并获取( i = 5, 结果 i = 0, 返回 0 System . out . println ( i . addAndGet ( - 5 )); // 获取并更新( i = 0, p i 的当前值 , 结果 i = -2, 返回 0 // 其中函数中的操作能保证原子,但函数需要无副作用 System . out . println ( i . getAndUpdate ( p -> p - 2 )); // 更新并获取( i = -2, p i 的当前值 , 结果 i = 0, 返回 0 // 其中函数中的操作能保证原子,但函数需要无副作用 System . out . println ( i . updateAndGet ( p -> p + 2 )); // 获取并计算( i = 0, p i 的当前值 , x 为参数 1, 结果 i = 10, 返回 0 // 其中函数中的操作能保证原子,但函数需要无副作用 // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final // getAndAccumulate 可以通过 参数 1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final System . out . println ( i . getAndAccumulate ( 10 , ( p , x ) -> p + x )); // 计算并获取( i = 10, p i 的当前值 , x 为参数 1, 结果 i = 0, 返回 0 // 其中函数中的操作能保证原子,但函数需要无副作用 System . out . println ( i . accumulateAndGet ( - 10 , ( p , x ) -> p + x ));

3.3 原子引用

原子引用类型有哪些?

  • AtomicReference
  • AtomicStampedReference
  • AtomicMarkableReference

AtomicReference

(其中 balance 是用 AtomicReference 修饰的变量)

/*    将初始余额1000元, 每次 -10 元, 如果启动100个线程, 则余额将会变成0元*/class DecimalAccountSafeCas implements DecimalAccount {     AtomicReference balance;     public DecimalAccountSafeCas(BigDecimal balance) {  this.balance = new AtomicReference(balance);     }     @Override     public BigDecimal getBalance() {  // 获取余额  return balance.get();     }     @Override     public void withdraw(BigDecimal amount) {// 取款  while (true) {      BigDecimal prev = balance.get();      BigDecimal next = prev.subtract(amount);      if (ref.compareAndSet(prev, next)) {   break;      }  }     }}

使用 AtomicReference 修饰的变量,仅能够判断共享变量的值是否与期盼的值相同,无法判断此共享变量之前是否被修改过,如果当前操作共享变量的线程希望:只要有其他线程动过了共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号,AtomicStampedReference

AtomicStampedReference

// 第二个参数即为当前的版本号static AtomicStampedReference ref = new AtomicStampedReference("A", 0);public static void main(String[] args) throws InterruptedException {     log.debug("main start...");     String prev = ref.getReference();    // 获取值 A     int stamp = ref.getStamp();   // 获取版本号     log.debug("版本 {}", stamp);     other(); // 如果中间有其它线程干扰,发生了 ABA 现象     sleep(1);     // 尝试改为 C     log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));}// 线程干扰, 即在主线程修改共享变量之前, 将共享变量的值修改为其他值后再修改回来, 使版本号变化private static void other() {     new Thread(() -> {  log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));  log.debug("更新版本为 {}", ref.getStamp());     }, "t1").start();     sleep(0.5);     new Thread(() -> {  log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));  log.debug("更新版本为 {}", ref.getStamp());     }, "t2").start();}

AtomicMarkableReference

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A - > B - > A - > C ,通过 AtomicStampedReference ,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过 ,所以就有了AtomicMarkableReference(第二参数为布尔类型)

3.4 原子数组

原子数组有哪些?

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

有如下方法:创建10个线程,每个线程分别对数组操作(自增)10000次(采用了函数式编程)

/ 参数1,提供数组、可以是线程不安全数组或线程安全数组 参数2,获取数组长度的方法 参数3,自增方法,回传 array, index 参数4,打印数组的方法*/// supplier 提供者 无中生有 ()->结果// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->private static  void demo(     Supplier arraySupplier,     Function lengthFun,     BiConsumer putConsumer,     Consumer printConsumer ) {     List ts = new ArrayList();     T array = arraySupplier.get();     int length = lengthFun.apply(array);     for (int i = 0; i  {      for (int j = 0; j  t.start()); // 启动所有线程     ts.forEach(t -> {  try {      t.join();  } catch (InterruptedException e) {      e.printStackTrace();  }     }); // 等所有线程结束     printConsumer.accept(array);}

不安全的数组

demo(     ()->new int[10],     (array)->array.length,     (array, index) -> array[index]++,     array-> System.out.println(Arrays.toString(array)));

结果:

[9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698]

安全的数组

demo(     ()-> new AtomicIntegerArray(10),     (array) -> array.length(),     (array, index) -> array.getAndIncrement(index),     array -> System.out.println(array));

结果:

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

3.5 字段更新器

  • AtomicReferenceFieldUpdater // 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域( Field )进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

加 volatile :

3.6 原子累加器

  • LongAdder
private static  void demo(Supplier adderSupplier, Consumer action) {     T adder = adderSupplier.get();     long start = System.nanoTime();     List ts = new ArrayList();     // 4 个线程,每人累加 50 万     for (int i = 0; i  {      for (int j = 0; j  t.start());     ts.forEach(t -> {  try {      t.join();  } catch (InterruptedException e) {      e.printStackTrace();  }     });     long end = System.nanoTime();     System.out.println(adder + " cost:" + (end - start)/1000_000);}

比较 AtomicLong LongAdder

for (int i = 0; i  new AtomicLong(), adder -> adder.getAndIncrement());}for (int i = 0; i  new LongAdder(), adder -> adder.increment());}

 输出:
 

可以看到使用 LongAdder 累加器性能明显提升。性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了CAS 重试失败,从而提高性能。

 

3.7 Unsafe

Unsafe 对象提供了非常底层的、操作内存、线程的方法, Unsafe 对象不能直接调用,只能通过反射获得

public class UnsafeAccessor {     static Unsafe unsafe;     static {  try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");      theUnsafe.setAccessible(true);      unsafe = (Unsafe) theUnsafe.get(null);  } catch (NoSuchFieldException | IllegalAccessException e) {      throw new Error(e);  }     }     static Unsafe getUnsafe() {  return unsafe;     }}

Unsafe CAS 操作:

@Dataclass Student {     volatile int id;     volatile String name; }
Unsafe unsafe = UnsafeAccessor.getUnsafe();Field id = Student.class.getDeclaredField("id");Field name = Student.class.getDeclaredField("name");// 获得成员变量的偏移量long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);Student student = new Student();// 使用 cas 方法替换成员变量的值UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 trueUnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 trueSystem.out.println(student);

输出:

Student(id=20, name=张三)

名医百科医学知识库