02.JUC 锁

来源:互联网 发布:meanshift算法流程图 编辑:程序博客网 时间:2024/05/17 17:55

基本概念

CAS(compare and swap),即比较并交换。它在 J.U.C 包中被广泛使用。是一种对内存中的共享数据进行操作的指令,而且该操作是原子的读写操作。

CAS 操作包含三个操作数:内存位置(V)、预期原值(A)以及新值(B)。它具有以下特点:

  • 当且仅当 A 和 V 相同,将 V 修改为 B ,否则什么都不做。

锁机制

在分析 CAS 的原理之前,先来介绍下 Java 的锁机制。常见的锁机制有两种:乐观锁和悲观锁

1.悲观锁

  • 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

  • 悲观锁的实现,往往依靠底层提供的锁机制。

  • 悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。


2.乐观锁

  • 假设不会发生并发冲突,每次不加锁而是假设没有冲突而去完成某项操作,只在提交操作时检查是否违反数据完整性。如果因为冲突失败就重试,直到成功为止。

  • 乐观锁大多是基于数据版本记录机制实现。为数据增加一个版本标识。

  • 比如在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。


3.实现

Java 中关于乐视锁、悲观锁的实现如下:

  • synchronized 关键字:它是悲观锁,同时也是独占锁,被 synchronized 修饰的对象,同一时间只能被一个线程访问,若此时其他线程也想访问该对象,只能进入等待状态。这也是其效率低下的主要原因。

  • CAS 操作:它是乐观锁,CAS 并不限制线程的访问的数量,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。


4.总结

  • 乐观锁的缺点是不能解决脏读的问题。

  • 在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题。

  • 但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法。


原理分析

这里以 AtomicInteger 类的 addAndGet 方法为例,来对 CAS 的原理做分析。

private volatile int value;public final int addAndGet(int delta) {    for (;;) {        // 返回 value        int current = get();        int next = current + delta;        if (compareAndSet(current, next)){            return next;        }       }}public final boolean compareAndSet(int expect, int update) {    // valueOffset 内存位置、expect 预期原值、update 新值    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}

观察代码,首先可以发现对于 addAndGet 操作,它并没有操作类似 synchronized 的阻塞方式实现,而是采用非阻塞的方式。

非阻塞,即同时可以有多个线程调用该方法,对数据进行操作。

那么它是如何保证数据的正确性?

  • volatile :该关键字保证了不同线程对[被其修饰变量]进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,这样保证了获取变量值是当前最新的。

  • 循环 CAS 操作:每次从内存中读取数据然后将此数据修改后的结果进行 CAS 操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成 CPU 指令的操作。

0 0