I. 使用Lock接口

只要不涉及到复杂用法,一般采用的是Java的synchronized机制

不过,Lock可以提供一些synchronized不支持的机制

  • 非阻塞的获取锁:尝试获取锁,如果能获取马上获取,不能获取马上返回,不会阻塞
  • 中断获取锁:当获取锁的线程被中断时,抛出异常,锁被释放
  • 超时获取锁:为尝试获取锁设定超时时间

相应API:

  • void lock():普通的获取锁
  • void lockInterruptibly() throws InterruptedException:可中断的获取锁,锁的获取中可以中断线程
  • boolean tryLock():非阻塞获取锁
  • boolean tryLock(long time, TimeUnit unit):超时获取锁
  • void unlock():释放锁

一般框架:

  1. //不要将lock写进try块,防止无故释放
  2. Lock lock = new ReentrantLock();
  3. lock.lock();
  4. try{
  5. ...;
  6. }finally{
  7. lock.unlock();
  8. }

II. 队列同步器AQS

AbstractQueuedSynchronizer:队列同步器,简称AQS,用来构建锁或者其他同步组件的基础框架

使用一个int的成员变量表示同步状态,通过内置的FIFO队列完成资源的排队工作

AQS实现锁可以看作:获取同步状态,成功则加锁成功;失败则加锁失败

调用AQS内部的获取同步状态的API,保证是线程安全的

  • getState()
  • setState(int newState)
  • compareAndSetState(int expect, int update)

1. 自己实现一个Mutex互斥锁

首先要继承一个Lock接口,然后自己实现里面的方法

  1. public class Mutex implements Lock {...}

Lock里面的方法是没有默认实现的,因此都需要重写

一般会实现一个继承于AQS的内部类来执行获取同步状态的实现:加锁相当于获取同步状态

  1. public class Mutex implements Lock {
  2. private static class Syn extends AbstractQueuedSynchronizer{...}
  3. }

可以看到,AQS的方法和锁需要实现的方法是对应的

先实现对应的AQS的几个方法

  1. private static class Syn extends AbstractQueuedSynchronizer{
  2. //判断同步器是否被线程占用
  3. @Override
  4. protected boolean isHeldExclusively() {
  5. return getState() == 1;
  6. }
  7. //获取锁
  8. @Override
  9. protected boolean tryAcquire(int arg) {
  10. if(compareAndSetState(0,1)){
  11. setExclusiveOwnerThread(Thread.currentThread()); //设置占用线程
  12. return true;
  13. }
  14. return false;
  15. }
  16. //释放锁
  17. @Override
  18. protected boolean tryRelease(int arg) {
  19. if(getState() == 0) throw new IllegalMonitorStateException();
  20. setExclusiveOwnerThread(null); //清空占用线程
  21. setState(0);
  22. return true;
  23. }
  24. }

锁的获取和AQS获取同步状态其实是一个道理

通过代理模式可以像下面这样实现

  1. public class Mutex implements Lock {
  2. private static class Syn extends AbstractQueuedSynchronizer{...}
  3. Syn syn = new Syn();
  4. @Override
  5. public void lock() {
  6. syn.acquire(1);
  7. }
  8. @Override
  9. public void lockInterruptibly() throws InterruptedException {
  10. syn.acquireInterruptibly(1);
  11. }
  12. @Override
  13. public boolean tryLock() {
  14. return syn.tryAcquire(1);
  15. }
  16. @Override
  17. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  18. return syn.tryAcquireNanos(1, unit.toNanos(time));
  19. }
  20. @Override
  21. public void unlock() {
  22. syn.release(1);
  23. }
  24. @Override
  25. public Condition newCondition() {
  26. return null;
  27. }
  28. }

2. AQS实现分析

锁实现的本质:信号量机制,互斥锁也就是0和1两个信号量

AQS维护了一个FIFO的队列,线程获取同步状态失败则会加入这个队列,然后阻塞,直到同步状态释放,队列首节点的线程被唤醒

同步队列中的节点保存的信息有:获取同步状态失败的线程引用,等待状态,前驱和后继节点

同步器有一个头节点和尾节点

加入新的阻塞线程:

构造节点,加入队列的尾节点

使用compareAndSetTail()加到尾部,这是一个原子操作

2.1 独占式的获取和释放

获取同步状态:

acquire()方法会调用tryAcquire(),如果获取失败,则开始调用addWaiter()来给尾节点添加新节点,再调用acquireQueued()等待请求调度

addWaiter()的作用是给FIFO队列添加尾节点,并返回这个节点的引用

因为可能会多个线程申请失败,因此需要使用原子操作compareAndSetTail()

enq()的作用是快速添加失败后的反复尝试,直到添加尾节点成功

acquireQueued()用来请求调度

可见等待调度期间是支持中断的

这个请求调度有两个条件:

  1. 该节点是首节点
  2. 申请互斥信号量成功

for循环的这个操作被称为自旋

release()释放互斥信号量,根据上文提到的获取信号量,除了tryRelease(),还应该唤醒后继节点

2.2 共享式状态获取和释放

最典型的场景就是读写场景:一个资源允许多个线程进行读取,此时写线程阻塞;而写线程执行时,所有读线程阻塞

共享锁锁也就是资源信号量的应用,主要解决下面问题:只想要有限的线程执行

调用tryAcquireShared()来申请资源信号量

doAcquireShared()是申请失败后,构造节点加入FIFO队列然后自旋的操作

使用releaseShared()来释放

注意:共享式的释放可能有多个线程,需要用CAS操作来实现tryReleaseShared()

3. 自己实现一个TwinsLock共享锁

需要自己实现的:

  • tryAcquiredShared()

  • tryReleaseShared():要保证释放操作的原子性

State()的取值就是资源信号量的取值

  1. public class TwinsLock {
  2. private int count;
  3. TwinsLock(int count){
  4. this.count = count;
  5. }
  6. private final Sync sync = new Sync(count);
  7. private static final class Sync extends AbstractQueuedSynchronizer{
  8. Sync(int count){
  9. if(count < 0) throw new IllegalArgumentException();
  10. setState(count); //设置资源总数
  11. }
  12. @Override
  13. protected int tryAcquireShared(int arg) {
  14. for(;;){
  15. int current = getState();
  16. int newCount = current - arg;
  17. if(newCount<0 || compareAndSetState(current, newCount)){
  18. return newCount;
  19. }
  20. }
  21. }
  22. @Override
  23. protected boolean tryReleaseShared(int arg) {
  24. for(;;){
  25. int current = getState();
  26. int newCount = current + arg;
  27. if(compareAndSetState(current, newCount)){
  28. return true;
  29. }
  30. }
  31. }
  32. }
  33. public void lock(){
  34. sync.acquireShared(1);
  35. }
  36. public void unlock(){
  37. sync.releaseShared(1);
  38. }
  39. }

III. 可重入锁

可重入锁:支持一个线程对资源反复加锁

synchronized支持可重入

ReentrantLock是可重入锁的一种实现,支持反复加锁

锁的公平性:

  • 公平:先对锁进行获取的请求先被满足
  • 不公平:先对锁进行获取的请求不一定先被满足

1. 实现可重入

只需要判断当前线程是否是获取了锁的线程,如果是,则同步状态加一

每次释放同步状态减一,减到0的时候设置获取锁的线程为null,此时允许其他线程获取

接下来来看看ReetrantLock的实现

2. 公平锁与非公平锁

继续观察nofairTryAcquire()方法,发现只要CAS成功,则线程直接获取到锁

而公平锁需要确定队列中没有前驱节点,即自己就是首节点

公平锁:确保线程的FIFO,先上下文切换开销大

非公平锁:可能造成线程饥饿,但线程切换少,吞吐量更大

IV. 读写锁

读写锁,是一种提供共享式和独占式两种方式的锁

  • 支持公平锁和非公平锁
  • 支持重进入
  • 支持锁降级

一个资源允许多个线程进行读取,此时写线程阻塞;而写线程执行时,所有读线程阻塞

1. 读写锁的实现

读写锁的同步状态是按位切割使用的

维护了一个int型的同步状态,32位

高16为读状态,低16位为写状态

1.1 写锁的获取

w是c与0x0000FFFF做与运算后的值,w=0有两种情况:

  1. 有读锁,低16位全0
  2. 无读锁也无写锁,需要后面的条件判断是否为当前线程

1.2 读锁的获取

和写锁的获取类似,需要判断先有没有写锁

不过读锁是共享式的,可以允许多个线程获取读锁

不过读锁也支持重进入,因此不光要维护获取读锁的总状态,还要维护每个线程获取读锁的状态

2. 锁降级

锁降级指:线程先获取写锁,然后再获取读锁,最后释放写锁,实现从写锁降到读锁

目的:保证读写操作的连贯性

使用场景:写操作执行完马上需要读一次,不加读锁的话可能会被其他写线程修改,再读数据可能就变了

V. LockSupport工具

用于阻塞和唤醒线程

VI. Condition接口

Condition接口依赖于Lock对象,用于实现等待-通知模式

核心API就是两个,这两个API的扩展可以增加超时时间,设置中断不敏感等等:

  • await()
  • signal()

1. 使用Condition实现一个阻塞队列

队列满的时候,填充操作阻塞;队列空的时候,取出操作阻塞

  1. public class BoundedQueue <T>{
  2. private Object[] items;
  3. private int addIndex, revIndex, count;
  4. private ReentrantLock lock = new ReentrantLock();
  5. private Condition empty = lock.newCondition();
  6. private Condition full = lock.newCondition();
  7. public BoundedQueue(int size){
  8. items = new Object[size];
  9. }
  10. /**
  11. * 添加元素
  12. * @param t
  13. */
  14. public void add(T t) throws InterruptedException {
  15. lock.lock();
  16. try{
  17. while(count == items.length){
  18. System.out.println("已满,请等待消耗");
  19. empty.await();
  20. }
  21. items[addIndex] = t;
  22. if(++addIndex == items.length) addIndex = 0;
  23. count++;
  24. full.signal();
  25. }finally {
  26. lock.unlock();
  27. }
  28. }
  29. /**
  30. * 取出元素
  31. * @return
  32. */
  33. public T remove() throws InterruptedException {
  34. lock.lock();
  35. try{
  36. while(count == 0){
  37. System.out.println("已空,请等待生产");
  38. full.await();
  39. }
  40. Object temp = items[revIndex];
  41. if(++revIndex == items.length) revIndex = 0;
  42. count--;
  43. empty.signal();
  44. return (T) temp;
  45. }finally {
  46. lock.unlock();
  47. }
  48. }
  49. }

2. Condition的实现分析

每个Condition会维护一个等待队列,一个锁支持支持多个等待队列

获取到锁的线程也就是同步队列的首节点

此时再调用await,则首节点进入等待队列,直到其他线程唤醒

相应的,调用signal则是将等待队列的首节点拆下来放到同步队列,唤醒线程开始自旋

当节点回到同步队列,之前调用的await()中的isOnsyncQueue()会返回true,结束等待,在调用acquireQueued()加入竞争

通过isHeldExclusively判断有没有拿到锁

02-Java中的锁详解的更多相关文章

  1. Java并发编程(十一)-- Java中的锁详解

    上一章我们已经简要的介绍了Java中的一些锁,本章我们就详细的来说说这些锁. synchronized锁 synchronized锁是什么? synchronized是Java的一个关键字,它能够将代 ...

  2. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

    原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...

  3. Java中日志组件详解

    avalon-logkit Java中日志组件详解 lanhy 发布于 2020-9-1 11:35 224浏览 0收藏 作为开发人员,我相信您对日志记录工具并不陌生. Java还具有功能强大且功能强 ...

  4. java中的注解详解和自定义注解

    一.java中的注解详解 1.什么是注解 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说注解就是源代码的元数据.比如,下面这段代码: @Override public Str ...

  5. Java中dimension类详解

    Java中dimension类详解 https://blog.csdn.net/hrw1234567890/article/details/81217788

  6. [转载]java中import作用详解

    [转载]java中import作用详解 来源: https://blog.csdn.net/qq_25665807/article/details/74747868 这篇博客讲的真的很清楚,这个作者很 ...

  7. JAVA中Object类方法详解

    一.引言 Object是java所有类的基类,是整个类继承结构的顶端,也是最抽象的一个类.大家天天都在使用toString().equals().hashCode().waite().notify() ...

  8. Java中反射机制详解

    序言 在学习java基础时,由于学的不扎实,讲的实用性不强,就觉得没用,很多重要的知识就那样一笔带过了,像这个马上要讲的反射机制一样,当时学的时候就忽略了,到后来学习的知识中,很多东西动不动就用反射, ...

  9. Java中的多线程详解

    如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个话其 ...

  10. Java中Volatile关键字详解

    一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值, ...

随机推荐

  1. cmake入门:01 构建一个简单的可执行程序

    一.目录结构 CMakeLists.txt:cmake 工程入口文件,包含当前目录下的工程组织信息.cmake 指令根据此文件生成相应的 MakeFile 文件. Hello.c: 源代码文件 bui ...

  2. test,exec,match,replace方法区别 正则

    这四种方法都是用来检测字符串是否包含某一子串或是否匹配否个正则表达式 test方法,匹配返回true,不匹配返回false match,匹配返回匹配到的数组(包含多次/g),匹配一次返回包含匹配子串的 ...

  3. display:flex;下的子元素width无效问题

    因为flex属性默认值为flex:0 1 auto;其中 1 为 flex中的 flex-shrink 属性. 该属性介绍: 一个数字,规定项目将相对于其他灵活的项目进行收缩的量. 根据上述介绍可以理 ...

  4. 定要过python二级 第11套

    1. 2.乃至好的代码片段与解决方法,我保存在了 H:盘中python中的:H:\python\python二级好的代码片段与错误解决 3.接着第一个点,为什么print(read(f))  把f 放 ...

  5. P4780-Phi的反函数【dfs】

    正题 题目链接:https://www.luogu.com.cn/problem/P4780 题目大意 给出\(n\),求一个最小的\(x\)满足\(\varphi(x)=n\). 若不存在或者大于\ ...

  6. P3337-[ZJOI2013]防守战线【单纯形】

    正题 题目链接:https://www.luogu.com.cn/problem/P3337 题目大意 \(n\)个地方可以建立塔也可以不建立塔,第\(i\)个位置建立需要消耗\(C_i\)元 \(m ...

  7. Python+requests环境搭建和GET基本用法

    Python+requests环境搭建 首先你得安装Python,然后安装requests模块(第3方模块,安装方法:pip install requests)  基本用法 get 请求(不带参数的) ...

  8. vite首次启动加载慢

    背景 随着vue3的到来,vite开始被各大vue3组件库使用,公司开始一个新项目,准备尝试用vite试一波. 问题发现 当把公司新项目移植到vite后,启动非常快,但发现页渲染时间慢了很多 可以看到 ...

  9. NOIP模拟73

    T1 小L的疑惑 解题思路 第一眼不是正解,又是 bitset 优化可以得到的 60pts 的部分分. 打着打着突然发现这个东西好像和之前做过的某个题有一些相似,试着打了一下. 然后样例过了,然后对拍 ...

  10. CTF入门记录(1

    (https://ctf-wiki.org) 00 基础了解 CTF简介 (wolai.com) 00-1 CTF题目类型 Web 大部分情况下和网.Web.HTTP等相关技能有关. Web攻防的一些 ...