【并发编程-2】JUC-1

news2024/11/23 16:45:35

Lock

互斥锁:

1、锁的可重入性:

当一个线程调用object.lock()获取到锁,进入临界区后,还可以再次调用object.lock()。

通常锁都应该设计为可重入,否则就会发生死锁。  比如synchronized就是可重入,在一个synchronized方法中可以继续调用另一个synchronized方法。

2、Lock:

基本认识:

public interface Lock {
    void lock();
    //可以被中断
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

ReentrantLock实现Lock接口,它的实现都在Sync类中:

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    public void lock() {
        sync.acquire(1);
    }
    public void unlock() {
        sync.release(1);
    }// ...
}

Sync是一个抽象类,它有两个子类FairSyncNonfairSync,分别对应公平锁和非公平锁。

如果一个线程来了不排队,直接去抢锁,就是非公平。 这也是默认的构造方法,目的是为了提高效率。

锁的基本原理:

Sync 的父类 AbstractQueuedSynchronizer,被称作队列同步器(AQS),它的父类是AbstractOwnableSynchronizer(AOS)。  看命名,都是Synchronizer结尾,因此,此锁具有备synchronized 功能,可以阻塞一个线程。    为了实现一把具有阻塞或唤醒功能的锁,需要几个要素:
  1. 需要一个state变量,标记该锁的状态。state变量至少有两个值:01。对state变量的操作, 使用CAS(Compare and Swap)保证线程安全。
  2. 需要记录当前是哪个线程持有锁。
  3. 需要底层支持对一个线程进行阻塞唤醒操作。
  4. 需要有一个队列维护所有阻塞的线程。这个队列也必须是线程安全的无锁队列,也需要使用 CAS

针对1和2,Sync的两个父类AOS、AQS已有对应的实现:

public abstract class AbstractOwnableSynchronizer implements
java.io.Serializable {
    private transient Thread exclusiveOwnerThread; // 记录持有锁的线程
}

public abstract class AbstractQueuedSynchronizer extends
AbstractOwnableSynchronizer implements java.io.Serializable {
    private volatile int state; // 记录锁的状态,通过CAS修改state的值。
}

state可以大于1,例如,同样一个线程,调用5lockstate会变成5;然后调用5unlockstate减为0。

  • state=0时,没有线程持有锁,exclusiveOwnerThread=null
  • state=1时,有一个线程持有锁,exclusiveOwnerThread=该线程;
  • state > 1时,说明该线程重入了该锁;

针对第3点:Unsafe类提供了阻塞或唤醒线程的一对操作,park/unpark。 LockSupport工具类进行了进一步封装:

public class LockSupport {
    // ...
    private static final Unsafe U = Unsafe.getUnsafe();
    public static void park() {
        U.park(false, 0L);
    }
    public static void unpark(Thread thread) {
        if (thread != null)
            U.unpark(thread);
    }
}

当一个线程中调用park(),该线程就会被阻塞; 然后另一个线程中调用

unpark(Thread thread),传入一个被阻塞的线程,就可以将其唤醒(notify只能唤醒一个不确定的线程)。

针对第4点:AOS这个父类中,还实现了一个双向链表的阻塞队列,存放阻塞的线程:

public abstract class AbstractQueuedSynchronizer {
    // ...
    static final class Node {
        volatile Thread thread; // 每个Node对应一个被阻塞的线程
        volatile Node prev;
        volatile Node next;
// ...
    }
    private transient volatile Node head;
    private transient volatile Node tail;
// ...
}

head指向第一个Node的位置,tail指向下一个要添加的位置。 初始为空,head和tail都指向null,入队时往tail处添加,tail往后移指向下一个null;出队时,将head指向的Node移除,head往后移。   所以,当head=tail=null时,代表队列为空。

ReentrantLock在公平性和非公平性上的实现差异:

非公平锁:如果state为0,直接将当前线程设置为锁持有者,并设置state的值;  如果state不是0,但锁的持有者是当前线程,直接更新state。

公平锁:如果state为0,要看看队列中有没有其他等待线程,如果没有才将当前线程设置为持有者;   如果state不为0,和上面一样。

阻塞队列与唤醒机制:

lock.lock()

调用lock.lock(),最终会到AQS中的核心方法,acquire:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

解析:

  1. 如果tryAcquire没有获取到锁,就调用acquireQueued去获取。
  2. 在acquireQueued中先调用addWaiter: 为当前线程生成一个Node,然后把Node放入双向链表的尾部。 此时还未阻塞,需要调用acquireQueued。  线程一旦进入acquireQueued方法,就会无限期阻塞,即使其他线程调用interrupt也无法唤醒,直到方法结束,也就是它获取到锁那一刻才会被唤醒。此时,会删除队列的第一个Node。

        阻塞方法parkAndCheckInterrupt,其实就是调用了LockSupport.park方法。

       3.此外,acquireQueued有个返回值,代表当前线程有没有中断标志(在阻塞期间,可能有其他线程给他发送过中断信号,但此时无法响应),如果有会调用selfInterrupt(),自己给自己发送一下中断信号,重新响应一下中断。

lock.unlock()

unlock 不区分公不公平,直接释放锁后,唤醒head节点,让其获取锁。
代码逻辑在AQS中:
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

如果尝试释放锁成功,就调用unparkSuccessor唤醒头节点,让其获取锁。

tryRelease中,就是判断当前线程是否持有锁,并state的值减到0为止。 参数中的releases,在上层调用unlock时默认传的1,因此,lock了几次,就要调用unlock几次,才能真正的释放锁。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

lockInterruptibly ():

ReentrantLock除了lock()方法,还可以调用lockInterruptibly (),此方法可以响应中断。 底层调用了AQS中的acquireInterruptibly:

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

里面的tryAcquire只是个模版方法,分别被FairSync和 NonfairSync实现。  当tryAcquire中没有获取到锁时,会执行doAcquireInterruptibly,判断如果有其他线程发了中断信号,则抛出异常,不会一直阻塞。

tryLock():

ReentrantLock中,其实用的比较多的,还有tryLock。 它是基于非公平锁的tryAcquire实现逻辑,如果拿到锁就返回true,否则返回false,不会一直阻塞等待。

读写锁:

与上面的互斥锁ReentrantLock相比,读写锁(ReadWriteLock)也是实现了Lock接口。 但是,它可以满足:读读不互斥(一个线程获取了读锁,其他线程还能获取读锁),读写互斥(一个线程获取了读锁,其他线程就不能获取写锁。 反之亦然。),写写互斥(一个线程获取了写锁,其他线程不能再获取写锁)。 ReadWriteLock也是个接口,具体逻辑由ReentrantReadWriteLock实现(RRWL)。  而在RRWL中,有两个内部类,读锁与写锁,也是实现了Lock。 因此,在使用读写锁时,要分别获取读锁与写锁:

ReadWriteLock readWriteLock = new ReentrantReadWriteLock ();
Lock readLock = readWriteLock . readLock ();
readLock . lock ();
// 进行读取操作
readLock . unlock ();
Lock writeLock = readWriteLock . writeLock ();
writeLock . lock ();
// 进行写操作
writeLock . unlock ();

实际上,两把锁都只是同一把锁的两个视图而已,他们只有一个sync对象, 所以,在同一个对象中,也才能实现读写互斥的逻辑:当对象中state=0时,说明没有线程持有锁;当state != 0时,要么有线程持有读锁,要么有线程持有写锁。再通过 sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。

从构造方法可以看出,共用了一个sync,sync也同样实现了公平,非公平的逻辑,并继承AQS。
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

因此,两把锁的逻辑实现,其实就是调用了sync的方法(AQS的方法,由多个sync继承实现):acquire/release(互斥锁和读写锁的写锁)、acquireShared/releaseShared(读写锁的读锁),公平和非公平(是否需要阻塞)在sync不同的子类中实现:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    // 写线程抢锁的时候是否应该阻塞
    final boolean writerShouldBlock() {
// 写线程在抢锁之前永远不被阻塞,非公平锁
        return false;
    }
    // 读线程抢锁的时候是否应该阻塞
    final boolean readerShouldBlock() {
// 读线程抢锁的时候,当队列中第一个元素是写线程的时候要阻塞(即便是非公平,也要排在写线程之后)
        return apparentlyFirstQueuedIsExclusive();
    }
}
static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    // 写线程抢锁的时候是否应该阻塞
    final boolean writerShouldBlock() {
// 写线程在抢锁之前,如果队列中有其他线程在排队,则阻塞。公平锁
        return hasQueuedPredecessors();
    }
    // 读线程抢锁的时候是否应该阻塞
    final boolean readerShouldBlock() {
// 读线程在抢锁之前,如果队列中有其他线程在排队,阻塞。公平锁
        return hasQueuedPredecessors();
    }
}

对于公平,都需要排队获取锁;   对于非公平,就要分情况了:

  1. 写锁:当state=0没有其他线程持有锁(或者state!=0,但是持锁的是自己),直接获取锁,不用排队。
  2. 读锁:如果队列的第一个是写线程,先让写线程获取锁,否则可能导致写线程一直获取不到。

Condition:

Condition本身也是一个接口,其功能和wait/notify类似,必须同Lock一起使用。 所以,Lock接口中,有一个和创建Conditon的方法。

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    // 所有的Condition都是从Lock中构造出来的
    Condition newCondition();
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
}
public interface Condition {
    void await() throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    void awaitUninterruptibly();
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();
}

我们知道,如果使用wait notify,是无差别唤醒。 假如只有一个生产者和一个消费者还好,如果有多个,可能出现生产者通知生产者、消费者通知消费者的问题(如果没有使用notifyAll,还可能出现死锁)。 而使用Condition,就可以精确唤醒,具体用法就是在Lock中new 两个Condition,分别给生产者和消费者使用(生产者使用condition1来等待,也可以唤醒condition2;  消费者则反过来)。

StampedLock:

StampedLock是在JDK8中新增的,可以支持读写不互斥。 

ReentrantReadWriteLock 采用的是 悲观读 的策略,当第一个读线程拿到锁之后,
第二个、第三个读线程还可以拿到锁,使得写线程一直拿不到锁,可能导致写线程 饿死 。虽然在其公平或非公平的实现中,都尽量避免这种情形,但还有可能发生。
StampedLock 引入了 乐观读 策略,读的时候不加读锁,读出来发现数据被修改了,再升级为 悲观读” ,相当于降低了 的地位,把抢锁的天平往 的一方倾斜了一下,避免写线程被饿死。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1184700.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据是什么?

1.什么是数据? 数据是什么?什么样的称之为数据呐?我想找个问题提出来,各位小伙伴一定会喷我,脸数据都不知道的还在写什么大数据?但是我们转身再想一想,数据,我们真的了解本质吗&…

5G边缘计算网关的功能及作用

5G边缘计算网关具有多种功能。 首先,它支持智能云端控制,可以通过5G/4G/WIFI等无线网络将采集的数据直接上云,实现异地远程监测控制、预警通知、报告推送和设备连接等工作。 其次,5G边缘计算网关可以采集各种数据,包…

虚拟机ping不通百度,SecureCRTPortable连接不上

虚拟机ping不通百度,SecureCRTPortable连接不上 文章目录 虚拟机ping不通百度,SecureCRTPortable连接不上1、打开虚拟网络编辑器2、更改设置3、NAT设置4、设置网关4、设置网关 cat /etc/sysconfig/network-scripts/ifcfg-ens33 TYPEEthernet PROXY_METHO…

Web Worker:JS多线程的伪解药?

前言 在前端开发领域,JavaScript 的单线程限制一直是一个难以忽视的挑战。当谈到解决JavaScript的单线程限制时,HTML5引入的Web Worker被普遍认为是一剂解药💊。同时,业界中大量的文章也是聚焦于讨论web worker的神奇力量。然而&…

Banana Pi BPI-M6开源硬件开发板介绍以及与 Raspberry Pi 5 的比较

Banana Pi BPI-M6 Banana Pi BPI-M6是Banana-Pi组织最新开发的一款类似于Raspberry Pi的单板计算机,具有相同的尺寸格式和相似的功能。 今天我想向您介绍这些功能,并将它们与新的 Raspberry 5 提供的功能进行一些比较。 Raspberry Pi是英国 Raspberry P…

Cesium教程(二十):动态加载图片

1、效果预览 备注: 箭头图片可以去这里下载:箭头下载地址 2、代码编写 创建容器 <div id="cesiumContainer" class="fullSize"

软件版本控制系统VCS工具——cvs vss svn git

版本控制 版本控制系统&#xff08;Version Control System&#xff0c;VCS&#xff09;是用于跟踪和管理源代码和文档的工具。可追踪和管理修改历史&#xff0c;包括修改的内容、时间、作者等信息。有助于团队协作、追踪变更、恢复历史版本等。VCS的主要目的是帮助团队协作开…

竞赛选题 深度学习机器视觉车道线识别与检测 -自动驾驶

文章目录 1 前言2 先上成果3 车道线4 问题抽象(建立模型)5 帧掩码(Frame Mask)6 车道检测的图像预处理7 图像阈值化8 霍夫线变换9 实现车道检测9.1 帧掩码创建9.2 图像预处理9.2.1 图像阈值化9.2.2 霍夫线变换 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分…

Pytest系列(16)- 分布式测试插件之pytest-xdist的详细使用

前言 平常我们功能测试用例非常多时&#xff0c;比如有1千条用例&#xff0c;假设每个用例执行需要1分钟&#xff0c;如果单个测试人员执行需要1000分钟才能跑完当项目非常紧急时&#xff0c;会需要协调多个测试资源来把任务分成两部分&#xff0c;于是执行时间缩短一半&#…

【gogogo专栏】golang并发编程

golang并发编程 并发编程的工具goroutine介绍协程管理器sync.WaitGroup channel介绍readChannel和writeChannelclose的用法select的用法 通讯示例总结 并发编程的工具 在golang中&#xff0c;并发编程是比较简单的&#xff0c;不像java中那么麻烦&#xff0c;golang天然的支持协…

便携式电能质量分析仪

产品简介 KDZD5000电能质量分析仪是我公司精心研制的现场测试的三相、多功能、智能化、人机操作简洁的综合型测试仪器。具有容易使用&#xff0c;超大液晶彩屏显示&#xff0c;高分辨率&#xff0c;中英文双语操作界面&#xff0c;防振结构外壳等特点。可同时测量4路电流&…

Cesium 展示——拖拽点移动相邻线也跟着更新

文章目录 需求分析1. 获取所要移动实体的 ID2. 移动点相邻线跟着更新移动3. 注意4. 其他需求 实现 拖拽点移动相邻线也跟着更新 拖拽前 点击拖拽 拖拽后 分析 1. 获取所要移动实体的 ID 在Cesium中获取指定(x, y)位置处的实体ID,你可以通过以下步骤实现: 首先,使用Cesi…

2023年【化工自动化控制仪表】考试报名及化工自动化控制仪表考试试卷

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年化工自动化控制仪表考试报名为正在备考化工自动化控制仪表操作证的学员准备的理论考试专题&#xff0c;每个月更新的化工自动化控制仪表考试试卷祝您顺利通过化工自动化控制仪表考试。 1、【单选题】CENTUMCS30…

【自定义控制器View的懒加载 Objective-C语言】

一、控制器的View懒加载 1.大家对这个懒加载是什么概念: 把上节课的代码command + C、command + V、保存一个副本,把副本命名为03-控制器的view的懒加载: 是不是用到的时候,再去加载,然后呢,只加载一次,对吧, 那这些重复的,我就删掉了啊, 删掉 控制器里面: 这个删…

PV与PVC

emptyDir存储卷 概述&#xff1a; 当Pod被分配给节点时&#xff0c;首先创建emptyDir卷&#xff0c;并且只要该Pod在该节点上运行&#xff0c;该卷就会存在。正如卷的名字所述&#xff0c;它最初是空的。Pod 中的容器可以读取和写入emptyDir卷中的相同文件&#xff0c;尽管该…

软件开发中常见的设计原则

软件开发中常见的设计原则 1. 单一责任原则2. 开放封闭原则3. 里氏替换原则4. 接口分离原则5. 依赖倒置原则6. 迪米特法则7. 合成复用原则8. 共同封闭原则9. 稳定抽象原则10. 稳定依赖原则 简写全拼中文翻译SRPThe Single Responsibility Principle单一责任原则OCPThe Open Clo…

Flink--Data Source 介绍

Data Source 简介 Flink 做为一款流式计算框架&#xff0c;它可用来做批处理&#xff0c;即处理静态的数据集、历史的数据集&#xff1b;也可以用来做流处理&#xff0c;即实时的处理些实时数据流&#xff0c;实时的产生数据流结果&#xff0c;只要数据源源不断的过来&#xff…

前端面试题之Javascript篇

一、JavaScript基础 1、数组有哪些方法 添加/删除元素 push() 向尾部添加元素pop() 从尾部提取一个元素shift() 从首端提取元素unshift() 从首端添加元素splice(start, deleteCount, item1...itemN) start表示开始计算的索引&#xff0c;deleteCount表示从start开始计算的元…

PTA_乙级_1006

思路&#xff1a;把数字的每一位都取出来&#xff0c;然后for循环把字符输入 #include <iostream> #include <string> using namespace std;int main() {int n;cin >> n;int b n / 100; // 计算百位数int s (n % 100) / 10; // 计算十位数int g n…

大学智能电表改造解决方案

随着科技的不断发展&#xff0c;我国高等教育院校在基础设施方面也在不断进行智能化升级。电力系统作为大学校园的重要组成部分&#xff0c;对其进行智能化改造已成为当前高校建设的热门话题。本文将详细介绍大学智能电表改造解决方案&#xff0c;以期为我国高校的电力系统智能…