一,Lock接口
  锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。
1.lock锁的使用形式
1     Lock lock = new ReentrantLock(); //可以是自己实现的Lock接口的实现类,也可以是jdk提供的同步组件
2 lock.lock();//一般不能放到try语句中
3 try {
4 } finally {
5 lock.unlock(); //一般要求放到finally中,确保即使发生异常也能安全释放掉锁
6 }
  • 在finally块中释放锁,目的是保证在获取到锁之后,即使发生异常,锁依然能被顺利释放,从而避免死锁情况的发生。
  • 不要将获取锁的过程写在try块中。假设放到try中,如果在获取锁时发生了异常,即锁没有被成功获取到,但finally语句中有释放锁的操作,这就会造成死锁,因为根本没有获取到锁,而底下又要求释放锁。如果没有放到try中,当获取锁失败时,代码立即会报异常而终止运行,因此就避免了死锁。
2.Lock接口的方法
  lock接口在java.util.cincurrent.locks包路径下
 

3.相比于synchronized,Lock接口所具备的其他特性
  ①尝试非阻塞的获取锁tryLock():当前线程尝试获取锁,如果该时刻锁没有被其他线程获取到,就能成功获取并持有锁,接着返回true,如果没有获取到则返回false。
  ②能被中断的获取锁lockInterruptibly():获取锁的线程能够响应中断。当线程在获取锁定过程中,如果锁被其他线程占用,则线程一直处于休眠状态,直到获取到锁或被其他线程中断才返回。要注意该线程允许其他线程调用Thread.interrupt()方法来中断等待的线程,当线程被中断掉,不会在去获取锁,会抛出interruptedException异常。
  ③超时的获取锁tryLock(long time, TimeUnit unit):在指定的截止时间获取锁,如果没有获取到锁返回false。

二,AbstractQueuedSynchronizer介绍
  谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!
  AbstractQueuedSynchronizer,简称AQS(同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。
   同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
  子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
  同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
1.AQS的核心思想
   AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
  用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
2.AQS的框架

  在AQS中维护了一个volatile int state(代表共享资源)和一个FIFO存放被阻塞的线程的同步队列(多线程争用资源被阻塞时会进入此队列)。

  其中state可以使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们使用CAS操作能够保证状态的改变是安全的。

那AQS的该如何使用呢?

  首先,我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。

  我们来看看AQS定义的这些可重写的方法:

    protected boolean tryAcquire(int arg) : 独占式获取同步状态,试着获取,成功返回true,反之为false

    protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;

    protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;

    protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false

    protected boolean isHeldExclusively() : 是否在独占模式下被线程占用。

接下来我们举一个自定义实现锁的实例的代码:

 package juc;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
//Mutex是我们自定的锁
public class Mutex implements java.io.Serializable {
//静态内部类,继承AQS
private static class Sync extends AbstractQueuedSynchronizer {
//是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0的时候获取锁,CAS操作成功,则state状态为1,
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁,将同步状态置为0
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
//同步对象完成一系列复杂的操作,我们仅需指向它即可
private final Sync sync = new Sync();
//加锁操作,代理到acquire(模板方法)上就行,acquire会调用我们重写的tryAcquire方法
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
//释放锁,代理到release(模板方法)上就行,release会调用我们重写的tryRelease方法。
public void unlock() {
sync.release(1);
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
}

  上面是锁的实现,其使用的方法和ReentrantLock的使用方法一样,因为ReentrantLock也是基于AQS实现的。

  通过前面介绍AQS的框架和使用方法,我们知道它是基于同步对列和state变量实现的,使用同步队列来存放被阻塞的线程。接下来就是介绍它是怎样运用同步队列的?

3.AQS的同步队列
  AQS的内部结构主要由同步等待队列(CLH)构成。同步器依赖内部的FIFO同步队列(一个虚拟的双向链表)来完成同步状态的管理,当前线程获取锁失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当锁释放时,会把下一个等待的节点中的线程唤醒,使其再次尝试获取锁。
  同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下所示。
  Node节点的设计:
 static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = ;
static final int SIGNAL = -;
static final int CONDITION = -;
static final int PROPAGATE = -;
volatile int waitStatus;//等待状态
volatile Node prev;//指向前一个结点的指针
volatile Node next;//指向后一个节点的指针
volatile Thread thread;//当前结点代表的状态
Node nextWaiter;

  前面我们提到过,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为"CLH"队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。注意队列中的第一个元素表示正在使用锁的线程,而队列中第二个结点才是第一个真正排队的结点,同步队列的基本结构如图所示。

 

  其实就是个双端双向链表

为了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:

  • 1.同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。
  • 2.队列首结点可以用来表示当前正获取锁的线程。
  • 3.当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。

 3.AQS的底层源码分析

  之前看的这篇博客感觉写的不错,在这里就直接引用下:https://blog.csdn.net/java_lyvee/article/details/98966684

  下面是我根据博客梳理的AQS的tryAcquire()的执行过程图:

https://www.cnblogs.com/chengxiao/p/7141160.html

AQS机制的更多相关文章

  1. 深入理解Java并发类——AQS

    目录 什么是AQS 为什么需要AQS AQS的核心思想 AQS的内部数据和方法 如何利用AQS实现同步结构 ReentrantLock对AQS的利用 尝试获取锁 获取锁失败,排队竞争 参考 什么是AQ ...

  2. Java面试03|并发及锁

    1.synchronized与Lock的区别 使用synchronized这个关键字实现的同步块有一些缺点: (1)锁只有一种类型 (2)线程得到锁或者阻塞 (3)Lock是在Java语言层面基于CA ...

  3. 流量控制闸门——LimitLatch套接字连接数限制器

    Tomcat作为web服务器,对于每个客户端的请求将给予处理响应,但对于一台机器而言,访问请求的总流量有高峰期且服务器有物理极限,为了保证web服务器不被冲垮我们需要采取一些措施进行保护预防,需要稍微 ...

  4. 【并发编程】【JDK源码】J.U.C--AQS (AbstractQueuedSynchronizer)(1/2)

    J.U.C实现基础 AQS.非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),concurrent包中的基础类都是使用这种模式来实现的.而concurren ...

  5. 一文彻底搞懂CAS实现原理 & 深入到CPU指令

    本文导读: 前言 如何保障线程安全 CAS原理剖析 CPU如何保证原子操作 解密CAS底层指令 小结 朋友,文章优先发布公众号,如果你愿意,可否扫文末二维码关注下? 前言 日常编码过程中,基本不会直接 ...

  6. 关于Synchornized,Lock,AtomicBoolean和volatile的区别介绍

    1.  volatile 变量可以被看作是一种 "程度较轻的 synchronized". 2.  Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的 ...

  7. 同步工具类—— CountDownLatch

    本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. 并发编程系列博客传送门 CountDownLatch简介 CountDownLa ...

  8. 并发工具类——Semaphore

    本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. 并发编程系列博客传送门 Semaphore([' seməf :(r)])的主要 ...

  9. 从连接器组件看Tomcat的线程模型——BIO模式

    在高版本的Tomcat中,默认的模式都是使用NIO模式,在Tomcat 9中,BIO模式的实现Http11Protocol甚至都已经被删除了.但是了解BIO的工作机制以及其优缺点对学习其他模式有有帮助 ...

随机推荐

  1. Linux下多线程复制文件(C)

    Linux下实现多线程文件复制,使用<pthread.h>提供的函数: int pthread_create(pthread_t *thread,const pthread_attr_t ...

  2. iPhone8的十面埋伏

    ​ 不知不觉,iPhone已经走到了第十个年头,也正因如此,业界最普遍的预测就是:iPhone8会出现颠覆性创新,让人眼前一亮的同时,给苹果再度续命.平心而论,苹果早就青史留名,创造了大量的奇迹,科技 ...

  3. 网络编程之C10K

    网络编程之C10K 虽然在过去的十几年里C10K问题已经可以很好的解决,但学习网络编程时研究C10K问题仍然价值巨大,因为技术的发展都是有规律和线索可循的,了解C10K问题及其解决思路,通过举一反三, ...

  4. android使用giflib加载gif

    转载请标明出处:https:////www.cnblogs.com/tangZH/p/12356915.html 背景不多说,反正ndk加载gif比java上加载gif好很多很多,主要体现在内存占用与 ...

  5. wepack环境配置1之node的安装

    .向往已久的webpack终于配好了.. 1.要安装webpack,首先需要安装nodejs nodejs下载地址:https://nodejs.org/en/ 下载完成后,一步步安装即可,我是安装到 ...

  6. python列表解析补充:

    python列表解析补充: # 补充: f = [x + y for x in 'ABCDE' for y in '1234567'] print(f) test = [] for x in 'ABC ...

  7. 47-Python进阶小结

    目录 Python进阶小结 一.异常TODO 二.深浅拷贝 2.1拷贝 2.2 浅拷贝 2.3 深拷贝 三.数据类型内置方法 3.1 数字类型内置方法 3.1.1 整型 3.1.2 浮点型 3.2 字 ...

  8. PHP压缩文件夹 php

    $path = PUBLIC_DIR.'/images/'; //待压缩文件夹父目录 $zipPath = PUBLIC_DIR.'/images_zip/'; //压缩文件保存目录 !is_dir( ...

  9. 【WPF学习】第五十六章 基于帧的动画

    除基于属性的动画系统外,WPF提供了一种创建基于帧的动画的方法,这种方法只使用代码.需要做的全部工作是响应静态的CompositionTarge.Rendering事件,触发该事件是为了给每帧获取内容 ...

  10. 前端基础知识之HTML

    [1: What does a doctype do?] 1: doctype是html文件的第一行代码,意味着它的前面有注释都不行.所以要要写在<html>标签前面,而且它不属于html ...