1、前言

之前好几次看到有人在面经中提到了乐观锁与悲观锁,但是一本《Java Concurrency In Practice》快看完了都没有见到过这两种锁,今天终于在第15章发现了它们的踪迹。

15.2 Hardware support for concurrency

Exclusive locking is a pessimistic technique—it assumes the worst (if you don’t lock your door, gremlins will come in and rearrange your stuff) and doesn’t proceed until you can guarantee, by acquiring the appropriate locks, that other threads will not interfere.

For fine-grained operations, there is an alternate approach that is often more efficient—the optimistic approach, whereby you proceed with an update, hopeful that you can complete it without interference. This approach relies on collision detection to determine if there has been interference from other parties during the update, in which case the operation fails and can be retried (or not). The optimistic approach is like the old saying, “It is easier to obtain forgiveness than permission”, where “easier” here means “more efficient”.

原来乐观锁与悲观锁并不是特指某个锁,而是在并发情况下保证数据完整性的不同策略。悲观锁指的就是我们平常使用的加锁机制,它假设我们总是处于最坏的情况下,如果不加锁数据完整性就会被破坏。而乐观锁指是一种基于冲突检测的方法,检测到冲突时操作就会失败。

2、CAS机制介绍

CAS(Compare And Swap)是一种常见的“乐观锁”,大部分的CPU都有对应的汇编指令,它有三个操作数:内存地址V,旧值A,新值B。只有当前内存地址V上的值是A,B才会被写到V上,否则操作失败。

public class SimulatedCAS {
  private int value;
  
  public synchronized int get() { return value; }
  
  public synchronized int compareAndSwap(int expectedValue, int newValue) {
    int oldValue = value;
    if (oldValue == expectedValue)
      value = newValue;
    return oldValue;
  }
}

上边的类模拟了CAS操作,如果成员变量 value 的值与参数 expecredValue 的值不同,那就说明其他的线程已对其进行了修改,本次操作失败。

接下来看一个使用CAS实现线程安全的计数器的例子。

public class CasCounter {
private SimulatedCAS value; public int getValue() {
return value.get();
} public int increment() {
int v;
do {
v = value.get();
}
while (v != value.compareAndSwap(v, v + 1));
return v + 1;
}
}

在并发数不是特别高的情况下,使用CAS的乐观锁在性能上要优于使用加锁方式的悲观锁,因为大部分情况下经过数次轮询后CAS操作都可以成功,而使用加锁机制则会造成线程的阻塞与调度,相对而言更耗时。

Java从5.0开始引入了对CAS的支持,与之对应的是 java.util.concurrent.atomic 包下的AtomicInteger、AtomicReference等类,它们提供了基于CAS的读写操作和并发环境下的内存可见性。

3、非阻塞算法

通过CAS可以实现高效的支持并发访问的数据结构,首先来看一个栈的实现。

public class ConcurrentStack <E> {
AtomicReference<Node<E>> top = new AtomicReference<>();
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null)
return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}
} public class Node <E> {
public final E item;
public Node<E> next;
public Node(E item) {
this.item = item;
}
}

这个栈实际上是一个单链表结构,变量 top 代表头节点。当 push() 被调用后,首先新建一个节点 newHead ,它的后继节点应该为当前的头节点。然后使用CAS操作尝试将新节点赋值给 top ,如果 top 没有发生变化,CAS操作成功。如果 top 已改变(可能是某个线程加入或移除了元素),CAS操作失败,替换新节点的后继节点为当前的头节点,再次尝试。无论CAS操作是否成功,链表的结构都不会被破坏。

现在为止我们已经看了两个基于CAS操作的例子,一个是计数器另一个是栈,它们都有一个共同的特点,那就是它们的状态只由一个变量决定,而且显然CAS操作一个也只能更新一个变量。那么如何使用CAS实现更为复杂的数据结构呢?这里给出一个队列的例子。

public class Node <E> {
final E item;
final AtomicReference<Node<E>> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
} public class LinkedQueue <E> {
private final Node<E> dummy = new Node<E>(null, null);
private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy);
private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy);
public boolean put(E item) {
Node<E> newNode = new Node<E>(item, null);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
if (curTail == tail.get()) {
if (tailNext != null) {
// Queue in intermediate state, advance tail
tail.compareAndSet(curTail, tailNext);
} else {
// In quiescent state, try inserting new node
if (curTail.next.compareAndSet(null, newNode)) {
// Insertion succeeded, try advancing tail
tail.compareAndSet(curTail, newNode);
return true;
}
}
}
}
}
}

为了维护队列结构的有效性,我们必须要做到,如果线程A正在修改链表,线程B即将修改链表,线程B应该可以得知当前某个线程正对链表进行操作,线程B不可以立即对其进行修改。然后,如果线程B发现线程A正在修改链表,链表中应该含有足够的信息使线程B能够“帮助”线程A完成工作。线程B完成了线程A未完成的工作后,线程B就可以立即开始执行自己的任务,而且线程A应该可以知道,线程B已经替自己完成了剩下的工作。这个算法实际是Michael-Scott nonblocking linked-queue algorithm,JDK中的ConcurrentLinkedQueue就是使用了这个算法。

现在来看一下具体的实现,首先表头 head 跟表尾 tail 在初始化时被指向了一个名叫 dummy 的哨兵节点。如果链表正在被修改,指针状态如下:

修改完成后指针状态如下:

算法的关键之处就在于,如果链表正在被修改,那么 tail 指向的节点的 next 属性是不为 null 的。那么任何线程只要发现 tail.next 不为 null ,它就可以断定链表当前正在被另一个线程操作。而且更巧妙的是,这个线程可以通过将 tail.next 赋值给 tail 来帮助另一个线程完成工作。

附:

在上边的队列中,每个Node节点在初始化时都要新创建一个AtomicReference对象,我们还可以对其进行优化:

public class Node<E> {
private final E item;
private volatile Node<E> next;
public Node(E item) {
this.item = item;
}
} private static AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");

通过AtomicReferenceFieldUpdater,我们可以用CAS的方式对 volatile 修饰变量进行修改,避免创建额外的对象。

参考:《Java Concurrency In Practice》

Java中的乐观锁的更多相关文章

  1. Java中的各种锁--分类总结

    前言 本文需要具备一定的多线程基础才能更好的理解. 学习java多线程时,最头疼的知识点之一就是java中的锁了,什么互斥锁.排它锁.自旋锁.死锁.活锁等等,细分的话可以罗列出20种左右的锁,光是看着 ...

  2. Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等

    Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...

  3. 分门别类总结Java中的各种锁,让你彻底记住

    概念 公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁.有可能,会造成优先级反转或者饥 ...

  4. Java中15种锁的分类综合总结

    本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...

  5. Java 中的各种锁和 CAS + 面试题

    Java 中的各种锁和 CAS + 面试题 如果说快速理解多线程有什么捷径的话,那本文介绍的各种锁无疑是其中之一,它不但为我们开发多线程程序提供理论支持,还是面试中经常被问到的核心面试题之一.因此下面 ...

  6. Java中15种锁的介绍

    作者:搜云库技术团队 原文:https://segmentfault.com/a/1190000017766364 1. Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观 ...

  7. mysql中的乐观锁和悲观锁

    mysql中的乐观锁和悲观锁的简介以及如何简单运用. 关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的. mysql的悲观锁: 其实理解起来非常简单,当数据被外界修改持保守态度,包括 ...

  8. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  9. 一文带你看懂Java中的Lock锁底层AQS到底是如何实现的

    前言 相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题.那你是不是很好奇,这些Lock锁api是如何实现的呢?本 ...

随机推荐

  1. 文档工具的王者Sphinx

    Sphinx https://www.sphinx.org.cn/ Sphinx是一个工具,可以轻松创建由Georg Brandl编写并根据BSD许可证授权的智能和美观文档 它最初是为Python文档 ...

  2. 20190726_安装CentOS7minimal版本后需要做的优化和配置

    20190726_安装CentOS7minimal版本后需要做的优化和配置 CentOS系统镜像下载地址:https://www.centos.org/ CentOS的Minimal(最小化安装版本) ...

  3. 使用HOSTNAMECTL配置主机名

    hostnamectl工具是用来管理给定主机中. 查看所有主机名 请运行下面的命令查看所有当前主机名: 〜] $ hostnamectl status 如果未指定任何选项,默认则使用status选项对 ...

  4. log4j Logger 使用简介

    项目结构: log4j.properties 内容: log4j.rootCategory=info,stdout log4j.appender.stdout=org.apache.log4j.Con ...

  5. 初识IO流

    输入输出流,用来进行设备之间的数据传输. 是我们IO传输的数据是以文件的形式体现的,所以Java给我们提供了一个类,Flie用来描文件和目录 File(File parent, String chil ...

  6. Ubuntu下好用的pdf工具

    安装okular sudo apt-get install okular 汉化 sudo apt-get install kde-l10n-zhcn 然后打开PDF文件时,右键选择打开方式选择okul ...

  7. 【转帖】Flink 核心技术浅析(整理版)

    Flink 核心技术浅析(整理版) https://www.cnblogs.com/swordfall/p/10612404.html 分类: Flink undefined 1. Flink简介 A ...

  8. Java开发笔记(一百五十一)Druid连接池的用法

    C3P0连接池自诞生以来在Java Web领域反响甚好,业已成为hibenate框架推荐的连接池.谁知人红是非多,C3P0在大型应用场合中暴露了越来越多的局限性,包括但不限于下列几点:1.C3P0管理 ...

  9. Python学习之路:关于列表(List)复制的那点事

    要谈列表的复制,我们就要谈到Python的赋值规则 首先我们创建列表a: a = [1,2,3] 通常我们复制一个元素的方法是这样的: b = a #复制元素的一般方法 print(a) print( ...

  10. Python 入门(3):运算符

    Python语言支持以下类型的运算符: 算术运算符 比较(关系)运算符 赋值运算符 逻辑运算符 位运算符 成员运算符 身份运算符 运算符优先级 Python算术运算符: + 加 两个对象相加 a + ...