信号量与管程

news2024/12/22 22:31:59

前言

我们知道,在并发领域内,需要关注分工、同步与互斥,针对分工问题,就是将任务拆解,分配给多个线程执行,而在多线程执行的过程中,需要解决线程之间的协作与互斥问题进而保证并发安全。那么解决这类问题的方案是什么呢?没错就是信号量和管程。

信号量

简介

信号量的概念是由荷兰计算机科学家Edsger W. Dijkstra在1960年引入的。Dijkstra引入了P(Proberen,荷兰语中的"try")和V(Verhogen,荷兰语中的"increment")这两个操作,并使用它们来解决各种同步问题,如著名的哲学家进餐问题。

Dijkstra最初引入信号量的目的是为了管理稀缺的计算机资源,如打印机或磁带驱动器。但随着时间的推移,信号量被广泛应用于各种场景中,成为并发编程中的基石。

信号量有两种常见类型:

  • 二进制信号量:其值只能为0或1,类似于互斥锁,常用于资源的互斥访问。
  • 计数信号量:其值可以为任何非负整数,常用于管理有限的资源集,如线程池中的线程数量或数据库连接池中的连接数量。

实现原理

信号量模型比较简单,它由一个计数器、一个等待队列和三个方法组成,即如下图所示:
在这里插入图片描述

信号量模型维护一个计数器来决定进入临界区的线程数,init方法则是初始化计数器大小,P操作则是将计数器-1,V操作则是把计数器+1,信号量的运转流程如下图所示:
在这里插入图片描述

demo演示

package com.markus.concurrent;

import java.util.concurrent.Semaphore;

/**
 * @author: markus
 * @date: 2023/8/19 2:21 PM
 * @Description: 信号量demo
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class SemaphoreDemo {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        int count = 0;
        ShareObject shareObject = new ShareObject(semaphore, count);

        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                // 线程安全
                shareObject.increment();
            }
        });
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                shareObject.increment();
            }
        });
        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();
        System.out.println(shareObject.getCount());
    }
}

class ShareObject {
    private Semaphore semaphore;
    private int count;

    public ShareObject(Semaphore semaphore, int count) {
        this.semaphore = semaphore;
        this.count = count;
    }

    public void increment() {
        try {
            semaphore.acquire();
          	// 临界区 非原子性操作,如果不做同步互斥控制,会造成并发不安全的情况
            count += 1;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }

    public void unsafeIncrement() {
        count++;
    }

    public int getCount() {
        return this.count;
    }
}

管程

简介

管程的概念是在1970s由Edsger W. Dijkstra、C.A.R. Hoare和Per Brinch Hansen等计算机科学家独立提出的。其中,C.A.R. Hoare的论文“Monitors: An Operating System Structuring Concept”特别影响深远,他详细描述了管程的结构和特性,并提出了条件变量的概念。

管程的提出旨在简化并发编程中复杂的同步问题,提供一个更高级和更结构化的同步方法。与信号量相比,管程通常更易于理解和使用,因为它将同步机制与数据结构紧密集成,并自动处理互斥。

许多现代编程语言和操作系统都提供了原生或类似管程的支持。例如,Java的synchronized关键字和内置的对象锁提供了管程的基本功能(互斥),而Object类的wait(), notify(), 和notifyAll()方法则实现了条件变量的功能。

上面提到两个关键组件:

  • 互斥访问:管程确保其方法在任何时候都只能被一个线程执行。
  • 条件变量:允许线程等待特定条件成立或通知其他等待的线程条件已经改变。这是管程中的核心部分,常常用于线程间同步。

实现原理

与信号量不同,管程是将共享变量、同步队列封装了起来,并在此基础上增加了条件变量及其等待队列,管程模型如下图所示:

MESA模型

需要一提的是:上图是管程MESA模型的实现,还有另外一种模型可以实现管程:Hoare模型,MESA模型和Hoare模型的核心区别就在于:

  • 在Hoare模型中,当一个线程在条件变量上执行signal操作来唤醒另一个线程时,控制权会立即被传递给被唤醒的线程。这意味着,唤醒的线程立刻获得管程的锁并开始执行,执行signal操作的线程将被暂停,直到被唤醒的线程释放锁或进入等待状态。
  • 在MESA模型中,当线程被唤醒时,它并不立即重新获得管程的锁。相反,它被放入一个就绪队列,等待重新获得锁。这种行为可能会导致所谓的“叫醒后等待”(wakeup-wait)的情况,即一个被唤醒的线程可能在获得锁之前需要等待其他线程。

MESA模型的优势在于它通常更容易实现,并且可以减少上下文切换的数量。

Java选择MESA模型来实现其内置的管程(Monitor)机制主要基于以下几个原因:

  • 实现简便性:MESA模型简化了唤醒和调度的过程。在Hoare模型中,当一个线程执行signal操作时,它必须将锁传递给被唤醒的线程,这可能导致额外的上下文切换和调度复杂性。而在MESA模型中,执行signal操作的线程可以继续执行,直到它自然地释放锁。
  • 减少上下文切换:如前所述,MESA模型可以减少不必要的上下文切换,因为执行signal的线程不必立即放弃执行权。
  • 预测性:在多处理器系统上,MESA模型可以提供更好的性能和预测性。由于线程不需要立即传递控制权,这有助于在多处理器环境中实现更有效的锁缓存和减少锁迁移。
  • 假唤醒的处理:MESA模型天然地支持处理假唤醒(spurious wakeups)。线程在被唤醒后会重新检查等待的条件,这样可以确保即使因为假唤醒而被唤醒,线程也不会执行不应该执行的代码。

尽管MESA模型引入了所谓的"叫醒后等待"(wakeup-wait)的现象,但由于上述优点,Java开发者认为它是一个更好的选择。这也是为什么Java的Object.wait()Object.notify()/notifyAll()方法的行为与MESA模型相吻合。

Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。

demo演示

package com.markus.concurrent;

/**
 * @author: markus
 * @date: 2023/8/19 3:12 PM
 * @Description:
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class Synchronized4MonitorDemo {
    public static void main(String[] args) throws InterruptedException {
        ShareInteger shareInteger = new ShareInteger(0);
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                try {
                    shareInteger.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 100_000; i++) {
                try {
                    shareInteger.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();
        threadB.start();

        Thread.sleep(2000);
        System.out.println("主线程将count设置为501");
        shareInteger.setCount(501);
        // 等待两个线程执行完
        threadA.join();
        threadB.join();
        // 打印最终的加和结果
        System.out.println(shareInteger.getCount());
    }
}

class ShareInteger {
    private int count;

    public ShareInteger(int count) {
        this.count = count;
    }

    public void increment() throws InterruptedException {
        synchronized (this) {
            while (count <= 500) {
                System.out.println(Thread.currentThread().getName() + " 被阻塞");
                this.wait();
                System.out.println(Thread.currentThread().getName() + " 被唤醒");
            }
            count += 1;
        }
    }

    public void setCount(int count) {
        synchronized (this) {
            this.count = count;
            if (count > 500) {
                // 唤醒所有等待count>500条件的线程
                this.notifyAll();
            }
        }
    }

    public int getCount() {
        return count;
    }
}

信号量与管程的对比

管程和信号量都是用于处理并发问题的同步原语,但它们具有不同的特点和使用方法。下面是管程和信号量的优劣对比以及它们的使用场景:

  • 管程:
    • 优点:
      • 封装性:管程提供了良好的封装性,因为它将数据和对数据的操作包含在一个单一的结构或对象中。这使得管程在面向对象的环境中特别有用。
      • 简介性:管程自动处理锁的获取和释放,使得代码更简洁且易于理解。
      • 条件变量支持:管程内部的条件变量提供了一种强大的机制,允许线程在特定条件下等待或被唤醒。
    • 缺点:
      • 灵活性:相对于信号量,管程可能在某些特定场景下不那么灵活。
    • 使用场景:
      • 需要结构化并发控制的情境,尤其是在面向对象的设计中。
      • 当需要使用条件变量来处理复杂的同步条件时。
  • 信号量:
    • 优点:
      • 灵活性:信号量为并发控制提供了极大的灵活性。它可以用于实现互斥、同步,以及各种资源计数场景
      • 广泛性:信号量是很多操作系统和并发库的基石,它有着广泛的应用
    • 缺点:
      • 易出错:由于信号量的灵活性,使用它的代码可能更容易出错。例如,忘记释放信号量或不正确的信号量使用可能导致死锁
      • 缺乏封装:信号量不提供封装数据和操作的机制,可能导致数据不一致或竞争条件。
    • 使用场景:
      • 用于实现互斥锁和其他锁类型。
      • 管理有限资源的数量,如线程池中的线程或数据库连接。
      • 实现复杂的同步场景,如生产者-消费者问题。(这里并没有管程实现简单,并且使用不当还会出错)

管程与信号量在Java中都有相应的实现,基于不同的场景应用不同的模型,并不是说谁好谁不好,只能说在某种场景下,谁比谁更合适,例如实现一个限流器,信号量就优于管程;实现一个阻塞队列,管程就优于信号量

其他同步工具

下面罗列下其他同步工具,做一些简要介绍,后续会单拉出几篇文章做详细解释。

CAS

原子操作,它检查当前值是否与预期值匹配,如果匹配,则使用新值更新它。

读拷贝更新

一种同步机制,允许读取操作无锁并发地执行,而更新操作通过延迟回收机制避免与读取操作冲突。

读写锁

一种锁机制,允许多个读者并发访问,但在写入时保证独占访问。

障碍同步

一种同步原语,使一组线程在继续执行之前等待所有线程都到达某个点。

总结

总结起来,管程和信号量都是并发控制的核心工具,各自带有其独特的特点和使用方法。管程,通过其结构化和封装的特性,为复杂的同步问题提供了简单、直观的解决方案,尤其适用于面向对象的环境中。它们强调了数据和对数据的操作之间的紧密结合,确保数据的完整性和安全性。而信号量,作为一种更基础且灵活的同步原语,能够用于广泛的场景,从基本的互斥到复杂的协调任务。虽然信号量提供了更大的灵活性,但这种灵活性也可能带来更高的错误风险。因此,在选择适当的并发工具时,开发者需要根据特定的问题和需求来权衡。不管如何,了解这两个工具的工作方式及其优劣势是任何希望深入并发编程的开发者的基础任务。

参考

https://time.geekbang.org/column/intro/100023901?tab=catalog

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

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

相关文章

day-25 代码随想录算法训练营(19)回溯part02

216.组合总和||| 思路&#xff1a;和上题一样&#xff0c;差别在于多了总和&#xff0c;但是数字局限在1-9 17.电话号码的字母组合 思路&#xff1a;先纵向遍历第i位电话号码对于的字符串&#xff0c;再横向递归遍历下一位电话号码 93.复原IP地址 画图分析&#xff1a; 思…

OpenLayers实战,OpenLayers实现地图鼠标经过点要素时显示名称标注提示框,移出后隐藏

专栏目录: OpenLayers实战进阶专栏目录 前言 本章讲解OpenLayers实现地图鼠标经过点要素时显示名称标注提示框,移出后隐藏的功能。 二、依赖和使用 "ol": "^6.15.1"使用npm安装依赖npm install ol@6.15.1使用Yarn安装依赖yarn add olvue中如何使用:…

【Java基础】深入理解String、StringBuffer和StringBuilder的异同

文章目录 一、结论&#xff1a;二、可变性String&#xff08;不可变&#xff09;StringBuffer和StringBuilder&#xff08;可变&#xff09; 三、线程安全性String&#xff08;线程安全&#xff09;StringBuffer&#xff08;线程安全&#xff09;和StringBuilder&#xff08;线…

58.C++ STL标准模板库 STL概述 STL三大组件

一、初识STL 1.1 STL概述 长久以来&#xff0c;软件界⼀直希望建⽴⼀种可重复利⽤的东⻄&#xff0c;以及⼀种得以制造出”可重复运⽤的东⻄”的⽅法,让程序员的⼼⾎不⽌于随时间的迁移&#xff0c;⼈事异动⽽烟消云散&#xff0c;从函(functions)&#xff0c;类别(classes),函…

nginx 配置反向代理的逻辑原则案例(值得一看)

一 实操步骤 1.1 架构图 1.2 配置原则 匹配准则&#xff1a; 当proxy_pass代理地址端口后有目录(包括 / 和/xxx),相当于是绝对根路径&#xff0c;则 nginx 不会把 location 中匹配的路径部分代理走; 当proxy_pass代理地址端口后无任何内容&#xff0c;可以理解为相对路径…

matlab使用教程(19)—曲线拟合与一元方程求根

1.多项式曲线拟合 此示例说明如何使用 polyfit 函数将多项式曲线与一组数据点拟合。您可以按照以下语法&#xff0c;使用 polyfit 求出以最小二乘方式与一组数据拟合的多项式的系数 p polyfit(x,y,n), 其中&#xff1a; • x 和 y 是包含数据点的 x 和 y 坐标的向量 …

DP读书:鲲鹏处理器 架构与编程(七)ARMv8-A 体系结构

一小时速通ARMv8-A体系结构 一、ARMv8-A处理单元核心架构1. ARMv8-A架构的处理器运行模式a. ARMv8-A的执行架构A. AArch64 执行状态B. AArch32 执行状态 b. ARMv8-A架构支持的指令集c. ARMv8-A 支持的数据类型d. ARMv8-A 的异常等级与安全模型e. ARMv8-A的虚拟化架构f. ARMv8-A…

Vue项目商品购物车前端本地缓存逻辑(适用H5/ipad/PC端)——前端实现购物车删除商品、购物车增减数量,清空购物车功能

一、需求 1、用户选择商品&#xff0c;自动回显在购物车列表中&#xff1b; 2、同个商品追加&#xff0c;购物车列表数量叠加&#xff1b; 3、开启赠送&#xff0c;选中的商品&#xff0c;在购物车中另增一条数据&#xff0c;且购物车列表价格显示为0&#xff1b;其实际价格在…

【OpenVINOSharp】在英特尔® 开发者套件爱克斯开发板使用OpenVinoSharp部署Yolov8模型

在英特尔 开发者套件爱克斯开发板使用OpenVinoSharp部署Yolov8模型 一、英特尔开发套件 AIxBoard 介绍1. 产品定位2. 产品参数3. AI推理单元 二、配置 .NET 环境1. 添加 Microsoft 包存储库2. 安装 SDK3. 测试安装4. 测试控制台项目 三、安装 OpenVINO Runtime1. 下载 OpenVINO…

【无闪烁AI视频制作】

效果查看&#xff1a; https://tianfeng.space/wp-content/uploads/2023/08/8月16日-12.mp4 前言 现在其实有很多的AI视频了&#xff0c;效果也越来越逼真&#xff0c;来看看都是怎么制作的吧&#xff01; 安装 TemporalKit下载 https://github.com/CiaraStrawberry/Tempo…

STM32 F103C8T6学习笔记9:0.96寸单色OLED显示屏—自由取模显示—显示汉字与图片

今日学习0.96寸单色OLED显示屏的自由取模显示: 宋体汉字比较复杂&#xff0c;常用字符可以直接复制存下来&#xff0c;毕竟只有那么几十个字母字符&#xff0c;但汉字实在太多了&#xff0c;基本不会全部放在单片机里存着&#xff0c;一般用到多少个字就取几个字的模&#xff…

操作系统_内存管理(一)

目录 1. 内存管理 1.1 内存管理的概念 1.1.1 内存管理的基本原理和要求 1.1.1.1 程序的链接和装入 1.1.1.2 逻辑地址与物理地址 1.1.1.3 进程的内存映像 1.1.1.4 内存保护 1.1.1.5 内存共享 1.1.1.6 内存分配与回收 1.2 覆盖与交换 1.2.1 覆盖 1.2.2 交换 1.3 连续…

华为认证为什么现在这么受欢迎?

华为认证目前受欢迎的原因有很多&#xff0c;以下是其中一些主要原因&#xff1a; 高质量的认证培训&#xff1a;华为认证提供了一系列高质量的培训课程&#xff0c;涵盖了IT技术、网络安全、云计算等领域。这些培训课程由华为的技术专家和工程师团队设计和提供&#xff0c;内容…

Openlayers 实战 - 地图视野(View)- 图层 -(layer)- 资源(source)显示等级设置

Openlayers 实战 - 地图视野&#xff08;View&#xff09;- 图层 -&#xff08;layer&#xff09;- 资源&#xff08;source&#xff09;显示等级设置 问题原因核心代码完整代码&#xff1a;在线示例 在以往的项目维护中&#xff0c;出现一个问题&#xff0c;使用最新高清底图发…

基于traccar快捷搭建gps轨迹应用

0. 环境 - win10 虚拟机ubuntu18 - i5 ubuntu22笔记本 - USB-GPS模块一台&#xff0c;比如华大北斗TAU1312-232板 - 双笔记本组网设备&#xff1a;路由器&#xff0c;使得win10笔记本ip&#xff1a;192.168.123.x&#xff0c;而i5笔记本IP是192.168.123.215。 - 安卓 手机 1.…

PHP酒店点菜管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP 酒店点菜管理系统是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 代码下载 https://download.csdn.net/download/qq_41221322/88232051 论文 https://…

ARM体系结构学习笔记:NZCV

NZCV N: negative 算术逻辑运算单元运算结果为负1/正0 Z: zero 算术逻辑运算单元运算结果为零1/非零0 C: 3cases:1 0加法 Carray | Not Carray减法 Not Borrow | Borrow(Not Carray)移位 Bit Shifted | Rotated Out V: 后面详细阐述[外…

【C语言】数组概述

&#x1f6a9;纸上得来终觉浅&#xff0c; 绝知此事要躬行。 &#x1f31f;主页&#xff1a;June-Frost &#x1f680;专栏&#xff1a;C语言 &#x1f525;该篇将带你了解 一维数组&#xff0c;二维数组等相关知识。 目录&#xff1a; &#x1f4d8;前言&#xff1a;&#x1f…

【探索Linux】—— 强大的命令行工具 P.6(调试器-gdb、项目自动化构建工具-make/Makefile)

阅读导航 前言一、什么是调试器二、详解 GDB - 调试器1.使用前提2.经常使用的命令3.使用小技巧 三、项目自动化构建工具 - make/Makefile1. make命令⭕语法⭕常用选项⭕常用操作⭕make命令的工作原理⭕make命令的优势&#xff1a; 2.Makefile文件⭕Makefile的基本结构⭕Makefil…

jvm-运行时数据区概述及线程

1.运行时数据区内部结构 不同的jvm对于内存的划分方式和管理机制存在着部分差异 java虚拟机定义了若干种程序运行期间会使用到的运行时数据区&#xff0c;其中有一些会随着虚拟机的启动而创建&#xff0c;随着虚拟机的退出而销毁&#xff0c;另外一些则是与线程一一对应的&…