Java中的乐观锁
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中的乐观锁的更多相关文章
- Java中的各种锁--分类总结
前言 本文需要具备一定的多线程基础才能更好的理解. 学习java多线程时,最头疼的知识点之一就是java中的锁了,什么互斥锁.排它锁.自旋锁.死锁.活锁等等,细分的话可以罗列出20种左右的锁,光是看着 ...
- Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等
Java 中15种锁的介绍 Java 中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等,在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类 ...
- 分门别类总结Java中的各种锁,让你彻底记住
概念 公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁.有可能,会造成优先级反转或者饥 ...
- Java中15种锁的分类综合总结
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
- Java 中的各种锁和 CAS + 面试题
Java 中的各种锁和 CAS + 面试题 如果说快速理解多线程有什么捷径的话,那本文介绍的各种锁无疑是其中之一,它不但为我们开发多线程程序提供理论支持,还是面试中经常被问到的核心面试题之一.因此下面 ...
- Java中15种锁的介绍
作者:搜云库技术团队 原文:https://segmentfault.com/a/1190000017766364 1. Java 中15种锁的介绍 在读很多并发文章中,会提及各种各样锁如公平锁,乐观 ...
- mysql中的乐观锁和悲观锁
mysql中的乐观锁和悲观锁的简介以及如何简单运用. 关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的. mysql的悲观锁: 其实理解起来非常简单,当数据被外界修改持保守态度,包括 ...
- 轻松搞懂Java中的自旋锁
前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...
- 一文带你看懂Java中的Lock锁底层AQS到底是如何实现的
前言 相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题.那你是不是很好奇,这些Lock锁api是如何实现的呢?本 ...
随机推荐
- GPL & LGPL
Reference https://www.cnblogs.com/findumars/p/3556883.html GPL 我 们很熟悉的Linux就是采用了GPL.GPL协议和BSD, Apach ...
- windows下tomcat日志的坑
最近在调试工程,想着在windows下多开窗口操作方便,结果发现日志竟然不全,百思不得其解.最后发现竟然是部署的坑. 之前的部署都是在bin目录下运行service.bat安装服务后了事,启停都通过t ...
- 部署TiDB集群
架构图 节点规划 120.52.146.213 Control Machine 120.52.146.214 PD1_TiDB1 120.52.146.215 PD2_TiDB2 120.52.146 ...
- Centos6 系统下源码方式安装Mysql 记录
在运维工作中经常部署各种运维环境,涉及mysql数据库的安装也是时常需要的.mysql数据库安装可以选择yum在线安装,但是这种安装的mysql一般是系统自带的,版本方面可能跟需求不太匹配. #### ...
- 026 SSM综合练习02--数据后台管理系统--数据库表创建及SSM环境搭建
1.数据库准备 本项目我们Oracle数据库,Oracle 为每个项目创建单独user,oracle数据表存放在表空间下,每个用户有独立表空间. (1)采用数据库管理员账号:SYSTEM,再配合数据库 ...
- 《算法图解》全本PDF下载附百度云链接
作者使用Python和图画来解释算法,找了好久才找到PDF版本,末尾附百度云链接~ 作者[美]Aditya Bhargava 译者袁国忠 类别 出版 / 非虚构 出版社人民邮电出版社 / 2017-0 ...
- pytest_04_测试用例setup和teardown
学过unittest的都知道里面用前置和后置setup和teardown非常好用,在每次用例开始前和结束后都去执行一次. 当然还有更高级一点的setupClass和teardownClass,需配合@ ...
- LOJ #3103. 「JSOI2019」节日庆典
题意 给定字符串 \(S\) ,对于 \(S\) 的每个前缀 \(T\) 求 \(T\) 所有循环同构串的字典序最小的串,输出其起始下标.(如有多个输出最靠前的) \(|S| \le 3 \times ...
- SpringBoot中使用Interceptor
https://www.cnblogs.com/chao555/p/9573102.html 1.创建自定义的拦截器并实现HandlerInterceptor接口 package com.demo.c ...
- 阿里巴巴 Java 开发手册 (十)MySQL 数据库
(一) 建表规约 1. [强制]表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1 表示是,0 表示否). 说明:任何字段如果为非负数,必须 ...