Java——并发编程(CAS、Lock和AQS)

这篇具有很好参考价值的文章主要介绍了Java——并发编程(CAS、Lock和AQS)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、Java Concurrent API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

答:Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

优势:

  • 可以使锁更公平;
  • 可以使线程在等待锁的时候响应中断;
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;
  • 可以在不同的范围,以不同的顺序获取和释放锁;

整体上来说 Lock synchronized 的扩展版Lock 提供了无条件的、可轮询的(tryLock()方法)、定时的(tryLock() 带参方法)、可中断的(lockInterruptibly())、可多条件队列的(newCondition() 方法)锁操作。另外Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。

2、什么是CAS?

  • CAScompare and swap 的缩写,即我们所说的比较交换
  • CAS是一种基于锁的操作,而且是乐观锁。在 Java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
  • CAS 操作包含三个操作数 ——内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A的值是一样的,那么就将内存里面的值更新成BCAS 是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b线程修改了,那么a 线程需要自旋,到下次循环才有可能机会执行。
  • java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)

3、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

  • 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语synchronized 关键字的实现也是悲观锁。
  • 乐观锁: 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供。乐观锁。在Java java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS 实现的。

3.1、乐观锁的实现方式:

  • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
  • Java 中的 Compare and Swap CAS ,当多个线程尝试使用 CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

3.2、CAS 操作中包含三个操作数

  • 需要读写的内存位置(V
  • 进行比较的预期原值(A
  • 拟写入的新值(B)

如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

4、CAS 的会产生什么问题?

  • ABA 问题: 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 oneCAS 操作成功,但可能存在潜藏的问题。从Java1.5 开始 JDKatomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
  • 循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU资源,效率低于synchronized
  • 只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

5、AQS?

答:AQS 队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。比如我们提到的ReentrantLockSemaphore,其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态进行更改需要使用同步器提供的 3个方法 getState()setState()compareAndSetState() ,它们保证状态改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义若干同步状态获取和释放的方法,同步器既支持独占式也支持共享式。同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁面向使用者,定义了使用者与锁交互的接口,隐藏实现细节;同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。

每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源。等待队列通过双向链表实现,线程被封装在链表的 Node 节点中,Node 的等待状态包括:

  • CANCELLED(线程已取消)
  • SIGNAL(线程需要唤醒)
  • CONDITION(线程正在等待)
  • PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作用)。

6、AQS的原理?

  • 获取同步状态: 调用 acquire() 方法,维护一个同步队列,使用 tryAcquire() 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter() 方法加入到同步队列的尾部,在队列中自旋。之后调用 acquireQueued() 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点是头结点且成功获取了同步状态。
  • 释放同步状态: 同步器调用tryRelease() 方法释放同步状态,然后调用 unparkSuccessor() 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。

6.1、CLH队列:

答: CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

6.2、AQS原理图

答: 获取同步状态时,调用 acquire() 方法,维护一个同步队列,使用 tryAcquire() 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter() 方法加入到同步队列的尾部,在队列中自旋。之后调用 acquireQueued() 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点
是头结点且成功获取了同步状态。

释放同步状态时,同步器调用 tryRelease() 方法释放同步状态,然后调用 unparkSuccessor() 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。

Java——并发编程(CAS、Lock和AQS),Java,java,服务器,数据库

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过protected类型的getState()setState()compareAndSetState()进行操作:

//返回同步状态的当前值
protected final int getState() { 
    return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
    state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

6.3、AQS对资源的共享方式

答:AQS定义两种资源共享方式,独占模式通过 acquirerelease 方法获取和释放锁,共享模式通过acquireSharedreleaseShared 方法获取和释放锁。

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatchSemaphoreCountDownLatchCyclicBarrierReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

6.4、AQS底层使用了模板方法模式

答: 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  • 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  • AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

6.5、AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法。

//该线程是否正在独占资源。只有用到condition才需要去实现它。
isHeldExclusively()
//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
tryReleaseShared(int)

答: 默认情况下,每个方法都抛出 UnsupportedOperationException 。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用:

  • ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
  • 再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,stateCAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()调用主线程,然后主调用线程就会从await()函数返回,继续后面动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquire-tryReleasetryAcquireShared-tryReleaseShared 中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock

7、为什么只有前驱节点是头节点时才能尝试获取同步状态?

答: 头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节
点。

目的:维护同步队列的 FIFO 原则,节点和节点在循环检查的过程中基本不通信,而是简单判断自己的前驱是否为头节点,这样就使节点的释放规则符合 FIFO,并且也便于对过早通知的处理,过早通知指前驱节点不是头节点的线程由于中断被唤醒。

8、AQS共享式式获取/释放锁的原理?

答:

  • 同步状态: 调用 acquireShared() 方法,该方法调用 tryAcquireShared() 方法尝试获取同步状态,返回值为int 类型,返回值不小于0 表示能获取同步状态。因此在共享式获取锁的自旋过程中,成功获取同步状态并退出自旋的条件就是该方法的返回值不小于0。
  • 释放同步状态: 调用 releaseShared() 方法,释放后会唤醒后续处于等待状态的节点。它和独占式的区别在于 tryReleaseShared() 方法必须确保同步状态安全释放,通过循环 CAS 保证,因为释放同步状态的操作会同时来自多个线程。

9、什么是可重入锁(ReentrantLock)?

答:ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

在Java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。

重入性的实现原理

答: 要想支持重入性,就要解决两个问题:

  1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;

  2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO

10、ReadWriteLock 是什么?

答: 如果使用 ReentrantLock,可能本身是为了防止线程 A在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁ReadWriteLock。

ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLockReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
而读写锁有以下三个重要的特性:文章来源地址https://www.toymoban.com/news/detail-741466.html

  • 公平选择性: 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 重进入: 读锁和写锁都支持线程重进入。
  • 锁降级: 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

到了这里,关于Java——并发编程(CAS、Lock和AQS)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java CAS和AQS的实现原理

    CAS(Compare And Swap)是一种并发控制机制,用于解决多线程并发访问共享资源时的数据一致性问题。 在Java中,CAS操作通常使用Atomic类来实现。例如,可以使用java.util.concurrent.atomic.AtomicInteger类来实现对整数类型的原子操作。Atomic类提供了一系列的原子操作方法,例如getAndAdd、

    2024年02月06日
    浏览(54)
  • Java 网络编程 —— 创建多线程服务器

    一个典型的单线程服务器示例如下: 服务端接收到一个客户连接,就与客户进行通信,通信完毕后断开连接,然后接收下一个客户连接,假如同时有多个客户连接请求这些客户就必须排队等候。如果长时间让客户等待,就会使网站失去信誉,从而降低访问量。 一般用并发性

    2024年02月02日
    浏览(46)
  • Java 网络编程 —— 实现非阻塞式的服务器

    当 ServerSocketChannel 与 SockelChannel 采用默认的阻塞模式时,为了同时处理多个客户的连接,必须使用多线程 在非阻塞模式下, EchoServer 只需要启动一个主线程,就能同时处理三件事: 接收客户的连接 接收客户发送的数据 向客户发回响应数据 EchoServer 委托 Selector 来负责监控接

    2024年02月05日
    浏览(54)
  • Java 网络编程 —— 创建非阻塞的 HTTP 服务器

    HTTP 客户程序必须先发出一个 HTTP 请求,然后才能接收到来自 HTTP 服器的响应,浏览器就是最常见的 HTTP 客户程序。HTTP 客户程序和 HTTP 服务器分别由不同的软件开发商提供,它们都可以用任意的编程语言编写。HTTP 严格规定了 HTTP 请求和 HTTP 响应的数据格式,只要 HTTP 服务器

    2024年02月06日
    浏览(44)
  • linux并发服务器 —— linux网络编程(七)

    C/S结构 - 客户机/服务器;采用两层结构,服务器负责数据的管理,客户机负责完成与用户的交互;C/S结构中,服务器 - 后台服务,客户机 - 前台功能; 优点 1. 充分发挥客户端PC处理能力,先在客户端处理再提交服务器,响应速度快; 2. 操作界面好看,满足个性化需求; 3.

    2024年02月09日
    浏览(72)
  • 【网络编程】高性能并发服务器源码剖析

      hello !大家好呀! 欢迎大家来到我的网络编程系列之洪水网络攻击,在这篇文章中, 你将会学习到在网络编程中如何搭建一个高性能的并发服务器,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!! 希望这篇文章能

    2024年04月15日
    浏览(51)
  • 网络编程(8.14)TCP并发服务器模型

    作业: 1. 多线程中的newfd,能否修改成全局,不行,为什么? 2. 多线程中分支线程的newfd能否不另存,直接用指针间接访问主线程中的newfd,不行,为什么? 多线程并发服务器模型原代码: 1.将newfd改成全局变量效果:  答:不行,因为newfd是全局变量的话,客户端连接后生成

    2024年02月13日
    浏览(45)
  • Linux学习之网络编程3(高并发服务器)

    Linux网络编程我是看视频学的,Linux网络编程,看完这个视频大概网络编程的基础差不多就掌握了。这个系列是我看这个Linux网络编程视频写的笔记总结。 问题: 根据上一个笔记,我们可以写出一个简单的服务端和客户端通信,但是我们发现一个问题——服务器只能连接一个

    2024年02月01日
    浏览(46)
  • Linux网络编程:多进程 多线程_并发服务器

    文章目录: 一:wrap常用函数封装 wrap.h  wrap.c server.c封装实现 client.c封装实现 二:多进程process并发服务器 server.c服务器 实现思路 代码逻辑  client.c客户端 三:多线程thread并发服务器 server.c服务器 实现思路 代码逻辑  client.c客户端 ​​​​   read 函数的返回值 wrap.h  wrap

    2024年02月12日
    浏览(54)
  • 计算机网络编程 | 并发服务器代码实现(多进程/多线程)

    欢迎关注博主 Mindtechnist 或加入【Linux C/C++/Python社区】一起学习和分享Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,滤波估计、多传感器信息融合,机器学习,人工智能等相关领域的知识和技术。 专栏:《网络编程》 当涉及到构建高性能的服务

    2024年02月08日
    浏览(66)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包