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

目录

线程共享模型总目录

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<BigDecimal> 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<String> 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 <T> void demo(
     Supplier<T> arraySupplier,
     Function<T, Integer> lengthFun,
     BiConsumer<T, Integer> putConsumer,
     Consumer<T> printConsumer ) {
     List<Thread> ts = new ArrayList<>();
     T array = arraySupplier.get();
     int length = lengthFun.apply(array);
     for (int i = 0; i < length; i++) {
         // 每个线程对数组作 10000 次操作
         ts.add(new Thread(() -> {
             for (int j = 0; j < 10000; j++) {
                 putConsumer.accept(array, j%length);
             }
         }));
     }
     ts.forEach(t -> 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 <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
     T adder = adderSupplier.get();
     long start = System.nanoTime();
     List<Thread> ts = new ArrayList<>();
     // 4 个线程,每人累加 50 万
     for (int i = 0; i < 40; i++) {
         ts.add(new Thread(() -> {
             for (int j = 0; j < 500000; j++) {
                 action.accept(adder);
             }
         }));
     }
     ts.forEach(t -> 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 < 5; i++) {
     demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
for (int i = 0; i < 5; i++) {
     demo(() -> 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 操作:

@Data
class 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); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);

输出:

Student(id=20, name=张三)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值