From 96a46f9b05ae89e98dd33fe673a27332765df9b0 Mon Sep 17 00:00:00 2001 From: Guide Date: Tue, 31 Jan 2023 16:27:36 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20add]=E4=B9=90=E8=A7=82=E9=94=81?= =?UTF-8?q?=E5=92=8C=E6=82=B2=E8=A7=82=E9=94=81=E8=AF=A6=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/sidebar.ts | 5 +- docs/database/mongodb/mongodb-questions-01.md | 2 +- docs/java/concurrent/atomic-classes.md | 325 +++++------------- .../java-concurrent-questions-02.md | 126 ++++++- .../java-concurrent-questions-03.md | 4 - .../optimistic-lock-and-pessimistic-lock.md | 128 +++++++ 6 files changed, 352 insertions(+), 238 deletions(-) create mode 100644 docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index ad39ddfb..0a57a7a6 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -102,6 +102,7 @@ export const sidebarConfig = sidebar({ icon: "star", collapsible: true, children: [ + "optimistic-lock-and-pessimistic-lock", "jmm", "java-thread-pool-summary", "java-thread-pool-best-practices", @@ -243,10 +244,10 @@ export const sidebarConfig = sidebar({ icon: "star", collapsible: true, children: [ - "mysql-index", + "mysql-index", { text: "MySQL三大日志详解", - link: "mysql-logs", + link: "mysql-logs", }, "transaction-isolation-level", "innodb-implementation-of-mvcc", diff --git a/docs/database/mongodb/mongodb-questions-01.md b/docs/database/mongodb/mongodb-questions-01.md index fa1001a0..5dbc2f36 100644 --- a/docs/database/mongodb/mongodb-questions-01.md +++ b/docs/database/mongodb/mongodb-questions-01.md @@ -60,7 +60,7 @@ MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据 #### 集合 -MongoDB 集合存在于数据库中,**没有固定的结构**,也就是 **无模式** 的,这意味着可以往集合插入不同格式和类型的数据。不过,通常情况相爱插入集合中的数据都会有一定的关联性。 +MongoDB 集合存在于数据库中,**没有固定的结构**,也就是 **无模式** 的,这意味着可以往集合插入不同格式和类型的数据。不过,通常情况下,插入集合中的数据都会有一定的关联性。 ![MongoDB 集合](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/database/mongodb/crud-annotated-collection.png) diff --git a/docs/java/concurrent/atomic-classes.md b/docs/java/concurrent/atomic-classes.md index 623a8d6e..16510073 100644 --- a/docs/java/concurrent/atomic-classes.md +++ b/docs/java/concurrent/atomic-classes.md @@ -5,7 +5,6 @@ tag: - Java并发 --- - ## Atomic 原子类介绍 Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 @@ -22,166 +21,41 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是 使用原子的方式更新基本类型 -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 +- `AtomicInteger`:整型原子类 +- `AtomicLong`:长整型原子类 +- `AtomicBoolean` :布尔型原子类 **数组类型** 使用原子的方式更新数组里的某个元素 -- AtomicIntegerArray:整型数组原子类 -- AtomicLongArray:长整型数组原子类 -- AtomicReferenceArray :引用类型数组原子类 +- `AtomicIntegerArray`:整型数组原子类 +- `AtomicLongArray`:长整型数组原子类 +- `AtomicReferenceArray` :引用类型数组原子类 **引用类型** -- AtomicReference:引用类型原子类 -- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- `AtomicReference`:引用类型原子类 +- `AtomicMarkableReference`:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题~~。 +- `AtomicStampedReference` :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 + +**🐛 修正(参见:[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))** : `AtomicMarkableReference` 不能解决 ABA 问题。 **对象的属性修改类型** -- AtomicIntegerFieldUpdater:原子更新整型字段的更新器 -- AtomicLongFieldUpdater:原子更新长整型字段的更新器 -- AtomicReferenceFieldUpdater:原子更新引用类型里的字段 - -> **🐛 修正(参见:[issue#626](https://github.com/Snailclimb/JavaGuide/issues/626))** : `AtomicMarkableReference` 不能解决 ABA 问题。 - -```java - /** - -AtomicMarkableReference是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false, - -修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已 - -@author : mazh - -@Date : 2020/1/17 14:41 -*/ - -public class SolveABAByAtomicMarkableReference { - - private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false); - - public static void main(String[] args) { - - Thread refT1 = new Thread(() -> { - try { - TimeUnit.SECONDS.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked()); - atomicMarkableReference.compareAndSet(101, 100, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked()); - }); - - Thread refT2 = new Thread(() -> { - boolean marked = atomicMarkableReference.isMarked(); - try { - TimeUnit.SECONDS.sleep(2); - } catch (InterruptedException e) { - e.printStackTrace(); - } - boolean c3 = atomicMarkableReference.compareAndSet(100, 101, marked, !marked); - System.out.println(c3); // 返回true,实际应该返回false - }); - - refT1.start(); - refT2.start(); - } - } -``` - -**CAS ABA 问题** - -- 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。 -- 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的! -- 代码例子(以`AtomicInteger`为例) - -```java -import java.util.concurrent.atomic.AtomicInteger; - -public class AtomicIntegerDefectDemo { - public static void main(String[] args) { - defectOfABA(); - } - - static void defectOfABA() { - final AtomicInteger atomicInteger = new AtomicInteger(1); - - Thread coreThread = new Thread( - () -> { - final int currentValue = atomicInteger.get(); - System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue); - - // 这段目的:模拟处理其他业务花费的时间 - try { - Thread.sleep(300); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - boolean casResult = atomicInteger.compareAndSet(1, 2); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - } - ); - coreThread.start(); - - // 这段目的:为了让 coreThread 线程先跑起来 - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - Thread amateurThread = new Thread( - () -> { - int currentValue = atomicInteger.get(); - boolean casResult = atomicInteger.compareAndSet(1, 2); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - - currentValue = atomicInteger.get(); - casResult = atomicInteger.compareAndSet(2, 1); - System.out.println(Thread.currentThread().getName() - + " ------ currentValue=" + currentValue - + ", finalValue=" + atomicInteger.get() - + ", compareAndSet Result=" + casResult); - } - ); - amateurThread.start(); - } -} -``` - -输出内容如下: - -``` -Thread-0 ------ currentValue=1 -Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true -Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true -Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true -``` - -下面我们来详细介绍一下这些原子类。 +- `AtomicIntegerFieldUpdater`:原子更新整型字段的更新器 +- `AtomicLongFieldUpdater`:原子更新长整型字段的更新器 +- `AtomicReferenceFieldUpdater`:原子更新引用类型里的字段 ## 基本类型原子类 -### 基本类型原子类介绍 - 使用原子的方式更新基本类型 -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 +- `AtomicInteger`:整型原子类 +- `AtomicLong`:长整型原子类 +- `AtomicBoolean` :布尔型原子类 -上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。 +上面三个类提供的方法几乎相同,所以我们这里以 `AtomicInteger` 为例子来介绍。 **AtomicInteger 类常用方法** @@ -195,24 +69,23 @@ boolean compareAndSet(int expect, int update) //如果输入的数值等于预 public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 ``` -### AtomicInteger 常见方法使用 +**`AtomicInteger` 类使用示例** : ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest { - public static void main(String[] args) { - // TODO Auto-generated method stub - int temvalue = 0; - AtomicInteger i = new AtomicInteger(0); - temvalue = i.getAndSet(3); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:0; i:3 - temvalue = i.getAndIncrement(); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:3; i:4 - temvalue = i.getAndAdd(5); - System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:4; i:9 - } + public static void main(String[] args) { + int temvalue = 0; + AtomicInteger i = new AtomicInteger(0); + temvalue = i.getAndSet(3); + System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:0; i:3 + temvalue = i.getAndIncrement(); + System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:3; i:4 + temvalue = i.getAndAdd(5); + System.out.println("temvalue:" + temvalue + "; i:" + i); //temvalue:4; i:9 + } } ``` @@ -221,7 +94,7 @@ public class AtomicIntegerTest { 通过一个简单例子带大家看一下基本数据类型原子类的优势 -**① 多线程环境不使用原子类保证线程安全(基本数据类型)** +**1、多线程环境不使用原子类保证线程安全(基本数据类型)** ```java class Test { @@ -237,7 +110,7 @@ class Test { } ``` -**② 多线程环境使用原子类保证线程安全(基本数据类型)** +**2、多线程环境使用原子类保证线程安全(基本数据类型)** ```java class Test2 { @@ -256,7 +129,7 @@ class Test2 { ### AtomicInteger 线程安全原理简单分析 -AtomicInteger 类的部分源码: +`AtomicInteger` 类的部分源码: ```java // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用) @@ -273,23 +146,21 @@ AtomicInteger 类的部分源码: private volatile int value; ``` -AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 +`AtomicInteger` 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 -CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 +CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 `objectFieldOffset()` 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 ## 数组类型原子类 -### 数组类型原子类介绍 - 使用原子的方式更新数组里的某个元素 -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray :引用类型数组原子类 +- `AtomicIntegerArray`:整形数组原子类 +- `AtomicLongArray`:长整形数组原子类 +- `AtomicReferenceArray` :引用类型数组原子类 -上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。 +上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerArray` 为例子来介绍。 -**AtomicIntegerArray 类常用方法** +**`AtomicIntegerArray` 类常用方法** : ```java public final int get(int i) //获取 index=i 位置元素的值 @@ -301,101 +172,97 @@ boolean compareAndSet(int i, int expect, int update) //如果输入的数值等 public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 ``` -### AtomicIntegerArray 常见方法使用 +**`AtomicIntegerArray` 类使用示例** : ```java - import java.util.concurrent.atomic.AtomicIntegerArray; public class AtomicIntegerArrayTest { - public static void main(String[] args) { - // TODO Auto-generated method stub - int temvalue = 0; - int[] nums = { 1, 2, 3, 4, 5, 6 }; - AtomicIntegerArray i = new AtomicIntegerArray(nums); - for (int j = 0; j < nums.length; j++) { - System.out.println(i.get(j)); - } - temvalue = i.getAndSet(0, 2); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndIncrement(0); - System.out.println("temvalue:" + temvalue + "; i:" + i); - temvalue = i.getAndAdd(0, 5); - System.out.println("temvalue:" + temvalue + "; i:" + i); - } + public static void main(String[] args) { + int temvalue = 0; + int[] nums = { 1, 2, 3, 4, 5, 6 }; + AtomicIntegerArray i = new AtomicIntegerArray(nums); + for (int j = 0; j < nums.length; j++) { + System.out.println(i.get(j)); + } + temvalue = i.getAndSet(0, 2); + System.out.println("temvalue:" + temvalue + "; i:" + i); + temvalue = i.getAndIncrement(0); + System.out.println("temvalue:" + temvalue + "; i:" + i); + temvalue = i.getAndAdd(0, 5); + System.out.println("temvalue:" + temvalue + "; i:" + i); + } } ``` ## 引用类型原子类 -### 引用类型原子类介绍 - 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。 -- AtomicReference:引用类型原子类 -- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~ +- `AtomicReference`:引用类型原子类 +- `AtomicStampedReference`:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 +- `AtomicMarkableReference` :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,~~也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。~~ -上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。 +上面三个类提供的方法几乎相同,所以我们这里以 `AtomicReference` 为例子来介绍。 -### AtomicReference 类使用示例 +**`AtomicReference` 类使用示例** : ```java import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceTest { - public static void main(String[] args) { - AtomicReference ar = new AtomicReference(); - Person person = new Person("SnailClimb", 22); - ar.set(person); - Person updatePerson = new Person("Daisy", 20); - ar.compareAndSet(person, updatePerson); + public static void main(String[] args) { + AtomicReference < Person > ar = new AtomicReference < Person > (); + Person person = new Person("SnailClimb", 22); + ar.set(person); + Person updatePerson = new Person("Daisy", 20); + ar.compareAndSet(person, updatePerson); - System.out.println(ar.get().getName()); - System.out.println(ar.get().getAge()); - } + System.out.println(ar.get().getName()); + System.out.println(ar.get().getAge()); + } } class Person { - private String name; - private int age; + private String name; + private int age; - public Person(String name, int age) { - super(); - this.name = name; - this.age = age; - } + public Person(String name, int age) { + super(); + this.name = name; + this.age = age; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public int getAge() { - return age; - } + public int getAge() { + return age; + } - public void setAge(int age) { - this.age = age; - } + public void setAge(int age) { + this.age = age; + } } ``` -上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下: +上述代码首先创建了一个 `Person` 对象,然后把 `Person` 对象设置进 `AtomicReference` 对象中,然后调用 `compareAndSet` 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 `person` 的话,则将其设置为 `updatePerson`。实现原理与 `AtomicInteger` 类中的 `compareAndSet` 方法相同。运行上面的代码后的输出结果如下: ``` Daisy 20 ``` -### AtomicStampedReference 类使用示例 +**`AtomicStampedReference` 类使用示例** : ```java import java.util.concurrent.atomic.AtomicStampedReference; @@ -454,7 +321,7 @@ currentValue=0, currentStamp=0 currentValue=666, currentStamp=999, wCasResult=true ``` -### AtomicMarkableReference 类使用示例 +**`AtomicMarkableReference` 类使用示例** : ```java import java.util.concurrent.atomic.AtomicMarkableReference; @@ -515,19 +382,17 @@ currentValue=true, currentMark=true, wCasResult=true ## 对象的属性修改类型原子类 -### 对象的属性修改类型原子类介绍 - 如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。 -- AtomicIntegerFieldUpdater:原子更新整形字段的更新器 -- AtomicLongFieldUpdater:原子更新长整形字段的更新器 -- AtomicReferenceFieldUpdater :原子更新引用类型里的字段的更新器 +- `AtomicIntegerFieldUpdater`:原子更新整形字段的更新器 +- `AtomicLongFieldUpdater`:原子更新长整形字段的更新器 +- `AtomicReferenceFieldUpdater` :原子更新引用类型里的字段的更新器 要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。 上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。 -### AtomicIntegerFieldUpdater 类使用示例 +**`AtomicIntegerFieldUpdater` 类使用示例** : ```java import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -578,6 +443,6 @@ class User { 23 ``` -## Reference +## 参考 - 《Java 并发编程的艺术》 diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md index 3fa3092a..3225c7f7 100644 --- a/docs/java/concurrent/java-concurrent-questions-02.md +++ b/docs/java/concurrent/java-concurrent-questions-02.md @@ -12,7 +12,127 @@ head: content: Java并发常见知识点和面试题总结(含详细解答)。 --- -## JMM(Java Memory Model) +## 乐观锁和悲观锁 + +### 什么是悲观锁?使用场景是什么? + +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。 + +也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 + +像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 + +**悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。** + +### 什么是乐观锁?使用场景是什么? + +乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 + +在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。 + +**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。** + +### 如何实现乐观锁? + +乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。 + +#### 版本号机制 + +一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。 + +**举一个简单的例子** :假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 \$100 。 + +1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。 +2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。 +3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。 +4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 + +这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 + +#### CAS 算法 + +CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 + +CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。 + +> **原子操作** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 + +CAS 涉及到三个操作数: + +- **V** :要更新的变量值(Var) +- **E** :预期值(Expected) +- **N** :拟写入的新值(New) + +当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。 + +**举一个简单的例子** :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。 + +1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 +2. i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 + +当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 + +Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。 + +`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作 + +```java +/** + * CAS + * @param o 包含要修改field的对象 + * @param offset 对象中某field的偏移量 + * @param expected 期望值 + * @param update 更新值 + * @return true | false + */ +public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); + +public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); + +public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); +``` + +关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 + +### 乐观锁存在哪些问题? + +ABA 问题是乐观锁最常见的问题。 + +#### ABA 问题 + +如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** + +ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference ` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 + +```java +public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + Pair current = pair; + return + expectedReference == current.reference && + expectedStamp == current.stamp && + ((newReference == current.reference && + newStamp == current.stamp) || + casPair(current, Pair.of(newReference, newStamp))); +} +``` + +#### 循环时间长开销大 + +CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 + +如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用: + +1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 +2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。 + +#### 只能保证一个共享变量的原子操作 + +CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。 + +## JMM(Java 内存模型) JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题: [JMM(Java 内存模型)详解](./jmm.md) 。 @@ -527,6 +647,10 @@ static class Entry extends WeakReference> { > > 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 +## Atomic 原子类 + +Atomic 原子类部分的内容我单独写了一篇文章来总结: [Atomic 原子类总结](./atomic-classes.md) 。 + ## 参考 - 《深入理解 Java 虚拟机》 diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index 9be73142..1db24f7c 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -294,10 +294,6 @@ head: CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 -## Atomic 原子类 - -Atomic 原子类部分的内容我单独写了一篇文章来总结: [Atomic 原子类总结](./atomic-classes.md) 。 - ## AQS ### AQS 是什么? diff --git a/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md new file mode 100644 index 00000000..04d07a86 --- /dev/null +++ b/docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md @@ -0,0 +1,128 @@ +--- +title: 乐观锁和悲观锁详解 +category: Java +tag: + - Java并发 +--- + +如果将悲观锁和乐观锁对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。 + +在程序世界中,乐观锁和悲观锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但是,相比于乐观锁,悲观锁对性能的影响更大! + +## 什么是悲观锁?使用场景是什么? + +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。 + +也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 + +像 Java 中`synchronized`和`ReentrantLock`等独占锁就是悲观锁思想的实现。 + +**悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。** + +## 什么是乐观锁?使用场景是什么? + +乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 + +在 Java 中`java.util.concurrent.atomic`包下面的原子变量类就是使用了乐观锁的一种实现方式 **CAS** 实现的。 + +**乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。** + +## 如何实现乐观锁? + +乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。 + +### 版本号机制 + +一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。 + +**举一个简单的例子** :假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 \$100 。 + +1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。 +2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。 +3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。 +4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 + +这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 + +### CAS 算法 + +CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 + +CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。 + +> **原子操作** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 + +CAS 涉及到三个操作数: + +- **V** :要更新的变量值(Var) +- **E** :预期值(Expected) +- **N** :拟写入的新值(New) + +当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。 + +**举一个简单的例子** :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。 + +1. i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 +2. i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 + +当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 + +Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。 + +`sun.misc`包下的`Unsafe`类提供了`compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对`Object`、`int`、`long`类型的 CAS 操作 + +```java +/** + * CAS + * @param o 包含要修改field的对象 + * @param offset 对象中某field的偏移量 + * @param expected 期望值 + * @param update 更新值 + * @return true | false + */ +public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); + +public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); + +public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); +``` + +关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 + +## 乐观锁存在哪些问题? + +ABA 问题是乐观锁最常见的问题。 + +### ABA 问题 + +如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** + +ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference ` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 + +```java +public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + Pair current = pair; + return + expectedReference == current.reference && + expectedStamp == current.stamp && + ((newReference == current.reference && + newStamp == current.stamp) || + casPair(current, Pair.of(newReference, newStamp))); +} +``` + +### 循环时间长开销大 + +CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 + +如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用: + +1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 +2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。 + +### 只能保证一个共享变量的原子操作 + +CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了`AtomicReference`类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用`AtomicReference`类把多个共享变量合并成一个共享变量来操作。