高效并发JUC锁-永恒砖石

JUC包的锁(可重入锁和读写锁)

Lock是JAVA5增加的内容,在JUC(java.util.concurrent.locks)包下面,作者是并发大师Doug Lea。JUC包提供了很多封装的锁,包括常用的ReentrantLock和ReadWriteLock。这些所其实都是依赖java.util.concurrent.AbstractQueuedSynchronizer(AQS)这个类来实现的,这个类有个简写的名字叫AQS,对这就是著名的AQS。

关于Lock,先说说线程获取Lock锁的时候会引起哪些事件呢。首先AQS是依赖一个被volatile修饰的int变量来标识当前锁的状态的,为0的时候代表当前锁不被任何线程拥有,当线程拿到这个锁的时候会通过CAS操作修改state的状态,那么对于争用失败的线程AQS会怎么办呢,AQS内部维护了一个等待队列,这个队列是纯JAVA实现的,其实现也是非常巧妙的,多线程在通过CAS来获取自己在队列中的位置,同时队列中的线程状态也是阻塞状态,遇到阻塞就头疼了,上面已经介绍过阻塞会带来的性能问题。在源码中我们可以看到的是AQS通过LockSupport(LockSupport底层依赖Unsafe)将线程阻塞,关于LockSupport其功能是用来代替wait和notity/notifyall的,更好的地方是LockSupport对park方法和unpark方法的调用没有先后的限制,而notify/notifyall必须在wait调用之后调用。尽管如此,这一切并没有阻止线程进入阻塞状态。

锁原理

可重入锁就是当前持有该锁的线程能够多次获取该锁,无需等待。基于AQS实现,AQS是JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,这个基础框架的重要性可以这么说,JUC包里面几乎所有的有关锁、多线程并发以及线程同步器等重要组件的实现都是基于AQS这个框架。AQS的核心思想是基于volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。当state的值为0的时候,标识改Lock不被任何线程所占有。

作为AQS的核心实现的一部分,举个例子,我们假设目前有三个线程Thread1、Thread2、Thread3同时去竞争锁,如果结果是Thread1获取了锁,Thread2和Thread3进入了等待队列,AQS的等待队列基于一个双向链表实现的,HEAD节点不关联线程,后面两个节点分别关联Thread2和Thread3,他们将会按照先后顺序被串联在这个队列上。这个时候如果后面再有线程进来的话将会被当做队列的TAIL。当三个线程同时进来,他们会首先会通过CAS去修改state的状态,如果修改成功,那么竞争成功,因此这个时候三个线程只有一个CAS成功,其他两个线程失败,也就是tryAcquire返回false。接下来,addWaiter会把将当前线程关联的EXCLUSIVE类型的节点入队列如果队尾节点不为null,则说明队列中已经有线程在等待了,那么直接入队尾。如果Thread2和Thread3同时进入了enq,同时t==null,则进行CAS操作对队列进行初始化,这个时候只有一个线程能够成功,然后他们继续进入循环,第二次都进入了else代码块,这个时候又要进行CAS操作,将自己放在队尾,因此这个时候又是只有一个线程成功,我们假设是Thread2成功,哈哈,Thread2开心的返回了,Thread3失落的再进行下一次的循环,最终入队列成功,返回自己。当有多个线程,或者说很多很多的线程同时执行的时候,怎么能保证最终他们都能够乖乖的入队列而不会出现并发问题的呢?这也是这部分代码的经典之处,多线程竞争,热点、单点在队列尾部,多个线程都通过【CAS+死循环】这个free-lock黄金搭档来对队列进行修改,每次能够保证只有一个成功,如果失败下次重试,如果是N个线程,那么每个线程最多loop N次,最终都能够成功。上面只是addWaiter的实现部分,那么节点入队列之后会继续发生什么呢,如果Thread1死死的握住锁不放,那么Thread2和Thread3现在的状态就是挂起状态啦,而且HEAD,以及Thread的waitStatus都是SIGNAL,尽管他们在整个过程中曾经数次去尝试获取锁,但是都失败了,失败了不能死循环呀,所以就被挂起了。

锁释放-等待线程唤起首先,Thread1会修改AQS的state状态,加入之前是1,则变为0,注意这个时候对于非公平锁来说是个很好的插入机会,举个例子,如果锁是公平锁,这个时候来了Thread4,那么这个锁将会被Thread4抢去。

我们继续走常规路线来分析,当Thread1修改完状态了,判断队列是否为null,以及队头的waitStatus是否为0,如果waitStatus为0,说明队列无等待线程,按照我们的例子来说,队头的waitStatus为SIGNAL=-1,因此这个时候要通知队列的等待线程,可以来拿锁啦,这也是unparkSuccessor做的事情,unparkSuccessor主要做三件事情:

将队头的waitStatus设置为0.

通过从队列尾部向队列头部移动,找到最后一个waitStatus<=0的那个节点,也就是离队头最近的没有被cancelled的那个节点,队头这个时候指向这个节点。

将这个节点唤醒,其实这个时候Thread1已经出队列了。

还记得线程在哪里挂起的么,上面说过了,在acquireQueued里面,我没有贴代码,自己去看哦。这里我们也大概能理解AQS的这个队列为什么叫FIFO队列了,因此每次唤醒仅仅唤醒队头等待线程,让队头等待线程先出。当有多个线程去竞争同一个锁的时候,假设锁被某个线程占用,那么如果有成千上万个线程在等待锁,有一种做法是同时唤醒这成千上万个线程去去竞争锁,这个时候就发生了羊群效应,海量的竞争必然造成资源的剧增和浪费,因此终究只能有一个线程竞争成功,其他线程还是要老老实实的回去等待。AQS的FIFO的等待队列给解决在锁竞争方面的羊群效应问题提供了一个思路:保持一个FIFO队列,队列每个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。

Lock是一个接口,定义了锁的基本方法:

public interface Lock {
    void lock();//加锁
    void lockInterruptibly() throws InterruptedException;//加可中断锁
    boolean tryLock();//加锁成功返回true,失败返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加锁成功返回true,失败等待一段时间后若仍无法加锁则返回false,可响应中断
    void unlock();//解锁
    Condition newCondition(); //返回一个Condition,利用它可以如和 Synchronizd配合使用的wait()和notify()一样对线程阻塞和唤醒,不同的是一个lock可以有多个condition
}

最后的newCondition返回一个Condition对象,该对象是一个接口,我们来看看其中提供的方法。

public interface Condition {
 void await() throws InterruptedException;//类似于wait(),可以响应中断
 void awaitUninterruptibly();//不响应中断的等待
 long awaitNanos(long nanosTimeout) throws
 InterruptedException;//等待指定时间(单位是纳秒),在接到信号、被中断或到达指定等待时间之前一直处于等待状态。方法返回被唤醒后的剩余等待时间,若返回值小于等于0则代表此次等待超时。
 boolean await(long time, TimeUnit unit) throws
 InterruptedException;//指定时间到达前结束等待返回true,否则返回false
 boolean awaitUntil(Date deadline) throws InterruptedException;//指定日期到达前被唤醒返回true,否则返回false
 void signal();//唤醒一个等待中的线程,类似于notify()
 void signalAll();//唤醒所有等待中的线程,类似于notifyAll()
}

可重入锁是Lock接口的一个重要实现类。所谓可重入锁即线程在执行某个方法时已经持有了这个锁,那么线程在执行另一个方法时也持有该锁。

ReadWriteLock是一个接口,也是定义锁的基本方法

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock和ReentrantLock不同,ReentrantReadWriteLock实现的是ReadWriteLock接口

读写锁是接口ReadWriteLock的一个重要实现类。加读锁时其他线程可以进行读操作但不可进行写操作,加写锁时其他线程读写操作都不可进行。

待续。。

原文地址:https://www.cnblogs.com/zhuoqingsen/p/9061694.html

时间: 2024-10-11 02:02:05

高效并发JUC锁-永恒砖石的相关文章

八、JVM视角浅理解并发和锁

根据<深入理解java虚拟机>这本书总结 提到java的并发和锁,第一反应可能回想到多线程.synchronized关键字等,那么对于jvm虚拟机,这些是如何实现的呢?或者用的什么思想实现的? 一.JAVA内存模型 为什么要谈到内存模型?并发编程和锁要解决的问题就是同步的问题,抛开java代码,虚拟机自身是如何实现单线程甚至是多线程保证同步的.这就需要对内存模型又了解,虚拟机如何读取.修改.保存数据,在多线程的情况下,又是如何使这些操作安全,这就是了解内存模型的出发点. 内存模型?jvm运行时

c++ 高效并发编程

高效并发编程 并发编程的基本模型包括,通过消息机制来管理运行顺序的message passing, 通过互斥保护共享的shared memory. 线程同步的基本原则 最低限度共享变量,考虑使用immutable对象 尽量减小锁粒度 互斥器和条件变量足以完成绝大多数任务,尽量使用高层的封装 避繁就简,读写锁 信号量 可重入锁 ,慎用. 关于死锁 RAII 控制锁区间 注意锁的获取顺序 Copy On Write 减小锁粒度 只读的情况下用shared_ptr 轻量级共享数据 在发生修改的情况下,

Java 高效并发

Java 高效并发 为了便于移植,Java 多线程内存模型不与硬件关联,不同硬件平台可以使用不同的实现手段 和 CPU 内存与高速缓存做对比 Java 内存模型被分为两大部分:主内存(对应 PC 内存)和工作内存(对应 CPU 高速缓存) 主内存与工作内存之间数据的交互 Java 定义了以下 8 种原子操作(最新的 Java 标准已经采用了新的内存访问协议,但下面 8 中操作也应该了解一下) lock,标识主内存变量为线程独占 同一个变量可以被一条线程多次 lock,但也需要同样次数的 unlo

高效并发(四)

前言 ? ? ? ?上篇已经分析了lock和concurrent提供的集合类包括阻塞队列和容器类.这里我们来介绍 Executor框架 线程池的作用 ? ? ? ?线程池作用就是限制系统中执行线程的数量.根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果:少了浪费了系统资源,多了造成系统拥挤效率不高.用线程池控制线程数量,其他线程 排队等候.一个任务执行完毕,再从队列的中取最前面的任务开始执行.若队列中没有等待进程,线程池的这一资源处于等待.当一个新任务需要运行时,如果线程 池中

Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3534050.html Semaphore简介 Semaphore是一个计数信号量,它的本质是一个"共享锁". 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可

Java多线程系列--“JUC锁”05之 非公平锁

获取非公平锁(基于JDK1.7.0_40) 非公平锁和公平锁在获取锁的方法上,流程是一样的:它们的区别主要表现在"尝试获取锁的机制不同".简单点说,"公平锁"在每次尝试获取锁时,都是采用公平策略(根据等待队列依次排序等待):而"非公平锁"在每次尝试获取锁时,都是采用的非公平策略(无视等待队列,直接尝试获取锁,如果锁是空闲的,即可获取状态,则获取锁).在前面的"Java多线程系列--"JUC锁"03之 公平锁(一)&q

【目录】JUC锁框架目录

JUC锁框架的目录整理如下: 1. [JUC]JUC锁框架综述 2. [JUC]JDK1.8源码分析之LockSupport(一) 3. [JUC]JDK1.8源码分析之AbstractQueuedSynchronizer(二) 4. [JUC]JDK1.8源码分析之ReentrantLock(三) 5. [JUC]JDK1.8源码分析之CyclicBarrier(四) 6. [JUC]JDK1.8源码分析之CountDownLatch(五) 7. [JUC]JDK1.8源码分析之Semapho

生产者消费者模式下的并发无锁环形缓冲区

上一篇记录了几种环形缓冲区的设计方法和环形缓冲区在生产者消费者模式下的使用(并发有锁),这一篇主要看看怎么实现并发无锁. 0.简单的说明 首先对环形缓冲区做下说明: 环形缓冲区使用改进的数组版本,缓冲区容量为2的幂 缓冲区满阻塞生产者,消费者进行消费后,缓冲区又有可用资源,由消费者唤醒生产者 缓冲区空阻塞消费者,生产者进程生产后,缓冲区又有可用资源,由生产者唤醒消费者 然后对涉及到的几个技术做下说明: ⑴CAS,Compare & Set,X86下对应的是CMPXCHG 汇编指令,原子操作,基本

并发无锁队列学习之二【单生产者单消费者】

1.前言 最近工作比较忙,加班较多,每天晚上回到家10点多了.我不知道自己还能坚持多久,既然选择了就要做到最好.写博客的少了.总觉得少了点什么,需要继续学习.今天继续上个开篇写,介绍单生产者单消费者模型的队列.根据写入队列的内容是定长还是变长,分为单生产者单消费者定长队列和单生产者单消费者变长队列两种.单生产者单消费者模型的队列操作过程是不需要进行加锁的.生产者通过写索引控制入队操作,消费者通过读索引控制出队列操作.二者相互之间对索引是独享,不存在竞争关系.如下图所示: 2.单生产者单消费者定长