[Java] 什么是锁?什么是并发控制?线程安全又是什么?锁的本质是什么?如何实现一个锁?

news2024/11/27 0:26:29

文章目录

  • 前言
  • 并发控制
    • 并发访问控制是什么?
    • 如何实现并发访问控制?
    • 并发访问控制 与 线程安全
  • 锁是什么?
    • 1. 加锁操作
    • 2. 解锁操作
    • 锁状态是什么?
  • 如何实现一个锁?
  • 笔者相关博客连接
  • 结语

前言


多线程编程中,锁是最重要的一个概念,但也是最容易理解错误的概念之一,理解好锁和并发控制是掌握多线程编程的重中之重,笔者将用本文去讲解锁以及并发控制的本质,以及尝试去实现一个锁。

并发控制


在讲解锁之前,有必要先讲解其前置知识:并发控制。
并发控制,英:Concurrency Control,也被称为并发访问控制,英:Concurrency Access Control。

在多线程环境下,当多个线程同时访问共享数据(堆内存里的数据)时,因为线程缓存机制很容易发生数据错误。一个比较典型的例子就是多线程计数器。如下:

多线程不做并发访问控制导致的数据不一致错误
可以看到执行两次结果分别是 61011 和 57257,那么我们期待的结果很明显是100000。这就是不做并发访问控制的严重后果

并发访问控制是什么?

那么究竟如何理解并发访问控制呢?我们程序员主动的去控制多个线程去有序地访问共享数据的这么个处理呢被叫做并发访问控制。

如何实现并发访问控制?

前面我们提到了并发访问控制的本质其实是程序员主动的控制多线程有序地访问共享数据。那么如何实现并发访问控制呢?答案很简单,就是 利用锁来实现 多个线程在时间上有序地访问共享数据。

什么意思呢?试想一下一堆人不排队去领盒饭和一个有人组织排队去领盒饭的区别。一堆人不排队去领盒饭则是多线程不做并发访问控制的情况,有人组织排队去领盒饭呢,则是在这个资源的操作在很微小的时间尺度上做到了串行,等于是多线程做了并发访问控制。但也因为有排队所以其实数据整体处理效率是会降低的,这个是加锁必不可少的开销

并发访问控制的本质呢就是当某线程操作共享一定要获取到锁(资格)才进行修改,否则就一直等待直到获取到锁,修改完成后释放锁(资格),让其他线程也能去获取锁去进行数据的访问。

并发访问控制 与 线程安全

线程安全其实是并发访问控制的一个产物,一旦我们的组件对其内部的数据做了 完善的(注意是完善的) 并发访问控制,那么我们可以说这个组件是 (多)线程安全

那么是不是做了并发访问控制就一定线程安全呢?答案是不一定。如果组件的开发者对于共享数据的并发访问控制逻辑有漏洞,那么其也不能算是线程安全。比如:

做了并发访问控制但不是线程安全的案例

那么上面的代码呢,就是典型的做了并发访问控制,但是有漏洞(add方法的存在)。导致MutiThreadCounter组件其实并非是线程安全的。

所以其实锁并不是真正意义上的锁,你锁了其他线程就真的无法访问到数据了,而是抽象意义上的锁,你即使加锁了,开发者依然能够使用其他线程任意访问被锁保护的数据。线程安全的真谛是当开发者发现没有获取到锁时停止对数据的访问。所以不难想象锁是个类似符号一样的东西,只有修改了这个符号成功的线程才主动去修改线程则是锁工作的原理了(下面章节讲)

锁是什么?


上面我们提到了,多线程需要做并发控制来保证线程安全。而做并发控制需要依赖一种工具这个工具就叫。不难想象锁这个工具的最核心的两个功能如下:

  1. 加锁(Lock)
  2. 解锁(Unlock)

下面我们分别介绍一下这两个操作的核心思想。

1. 加锁操作

加锁操作是一个并发操作,意味着通常需要考虑多个线程同时会执行加锁操作。而常规锁(特殊设计的锁除外)的设计是同一时间只能有一个线程成功获取到锁(也叫锁竞争成功)

对于锁竞争成功的线程 锁这个工具类是直接返回 让线程能够继续执行指令。
对于锁竞争失败的线程 锁这个工具类是会阻塞当前线程 让线程卡在加锁的操作直到获取锁成功。

不难看出在这里锁工具类的职责就是帮我们去竞争锁以及在失败时阻塞线程(这是锁的开发者需要实现的)

加锁成功这个在程序实现上也是非常简单,无非就是标记当前线程为锁的主人。比如JUC里大名鼎鼎的AQS里就有相关的标记为主人的代码。

AQS的setExclusiveOwnerThread(Thread)方法

而线程阻塞的方式也很简单,有重量级的OS级别的实现也有轻量级的进程级别的实现。

  1. OS级别的重量级实现是:OS支持线程休眠然后通过内核唤醒线程的方式来实现,就比如Java的内置锁的重量级锁模式(Java关键字 synchronized)
  2. 进程级别的轻量级实现是:无限循环,直到获取锁成功。比如下面的截图里的for (;😉,也是出自AQS类。

在这里插入图片描述

两种实现在锁持有时间上的不同场景下,有不同的表现,比如可以看出轻量级实现是会一直循环获取锁的。这种情况下分配给线程的CPU时间片会全部用于执行锁获取代码,直到锁获取成功,这意味着较大的CPU使用率,这会使得在其他线程会长时间持有(占用)锁时,轻量级锁有明显的劣势。而重量级锁与轻量级锁相对,因为线程会休眠,休眠时是不会占用CPU资源的。但因为线程休眠到唤醒会有线程 上下文切换(Context Switch) 的开销,这个开销是比较昂贵的,通常是微秒(µs)级别的开销。所以如果锁持有时间很短的话是推荐使用无限循环这种实现方式,可以节省很多上下文切换的开销。Java里面内置锁有锁膨胀机制,会自动根据锁的使用情况去选择轻量级锁亦或是重量级锁。

2. 解锁操作

和加锁操作不同,解锁操作不是并发操作,不过其工作和加锁类似,加锁是标记当前线程为锁的主人,而解锁则是标记锁为无主状态(即:null)。

Java的ReentrantLock类中你能在解锁的流程中看到这样的设null的代码:

ReentrantLock类中的解锁代码

锁状态是什么?

前面我们提到了 锁是一个工具,用于帮助我们的开发者实现锁竞争以及竞争锁失败时阻塞线程的功能。无状态(Stateless)相信大家都很熟悉了,那么其相对的有状态(Stateful)相信大家也很熟悉。其实简单来说就是一个组件的属性从组件外部能观测到变化的,那么这个组件就是Stateful的。

锁这个工具也一样,我们不同线程都能观测到锁的当前状态。锁的状态(内部属性)也会因为不同的线程锁竞争成功而变化(比如刚才的setExclusiveOwnerThread方法会改变exclusiveOwnerThread属性的值一样)。

除开我们最基础的exclusiveOwnerThread属性属于广义的锁状态之外,还有一些锁会有特殊的信息需要保存。存储这些信息的属性也属于锁状态(狭义的锁状态)。大名鼎鼎的AQS内部呢就是维护了一个state属性

AQS的state属性

AQS提供了维护这个state的API接口,外部不同的锁设计需求则可以根据自己的需求去利用这32位的空间去存储不同的锁状态。就比如:

  1. ReentrantLock:可重入锁,state为0代表未上锁,state > 1 代表已上锁,已上锁时state也代表重入次数。
  2. ReentrantReadWriteLock:可重入读写锁,state的高16位用于存储读锁个数,state的低16位用于存储写锁重入次数信息。

如何实现一个锁?


看到现在,其实我们已经了解了锁最重要的信息。在Java中大部分锁的实现都是基于AQS实现的,不过既然AQS是JDK开发者实现的,其实我们自己也可以开发AQS,或者做一个简化版的,比如只有ownerThread属性的锁,那么我们来试一下实现一个自己的锁。

public final class CustomizeYourOwnLockSampe {
    
    private static final class CustomizeLock {

        private final AtomicReference<Thread> ownerThread = new AtomicReference<>();

        public void lock() {
            final Thread current = Thread.currentThread();
            for (;;) {
                if (null == ownerThread.compareAndExchange(null, current)) break;
            }
        }

        public void unlock() {
            final Thread current = Thread.currentThread();
            if (!ownerThread.get().equals(current)) throw new IllegalStateException("Current thread is not the owner thread of this lock instance.");
            ownerThread.set(null);
        }

    }

    static int count = 0;
    static CustomizeLock lock = new CustomizeLock();

    private static Thread genWorkerThread() {
        final Thread thread = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                lock.lock();
                count++;
                lock.unlock();
            }
        });
        return thread;
    }

    public static void main(String[] args) throws InterruptedException {
        final Thread thread1 = genWorkerThread();
        final Thread thread2 = genWorkerThread();

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(count);
    }
}

执行一下,可以看到结果就是期望中的10万。

自定义锁的实现

笔者相关博客连接


笔者在本章简单列举之前写过的相关文章,有兴趣的读者可以去额外阅读一下:

  1. 《[Database] 关系型数据库中的MVCC是什么?怎么理解?原理是什么?MySQL是如何实现的?》
  2. 《[Java] 乐观锁?公平锁?可重入锁?盘点Java中锁相关的概念》

结语


锁、并发控制和线程安全这几个概念是相辅相成的,通过本篇文章我们知道了锁其实是一种工具类,也知道其主要职责主要是负责维护锁状态以及加锁失败时阻塞线程,我们也简单地用Java自定义了一个我们自己的锁实现。理解锁的本质是理解多线程编程的基础。理解了锁的基础之后在未来的文章中笔者将会去带大家去实现自己的分布式锁

我是虎猫,希望本文对你有帮助。(=・ω・=)

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

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

相关文章

oracle (+)学习

最近工作需要将oracle的存储过程转化为hive的sql脚本。遇到很多不一样的地方&#xff0c;例如oracle连接中有()号的用法。 借鉴这篇文章&#xff0c;但是这个排版比较烂。。。 oracle ()的,Oracle中()的作用_大雪菜的博客-CSDN博客 先建表和插入数据 --生成部门表CREATE TA…

2014年蓝桥杯Java C组——猜年龄

2014年蓝桥杯Java C组——猜年龄 标题&#xff1a;猜年龄 小明带两个妹妹参加元宵灯会。别人问她们多大了&#xff0c;她们调皮地说:“ 我们俩的年龄之积是年龄之和的6倍”。 小明又补充说:“她们可不是双胞胎&#xff0c;年龄差肯定也不超过8岁啊。” 请你写出:小明的较小的…

状态模式

文章目录状态模式1.状态模式的本质2.何时选用状态模式3.优缺点4.状态模式的结构5.实现上下文中维护状态及转换状态上下文中维护状态处理类中转换状态状态模式 状态模式说白了就是不同的状态&#xff0c;执行不同的行为&#xff0c;也就是状态和行为分离 1.状态模式的本质 状态模…

Eclipse+Java+Swing+Mysql实现自助存取款机(ATM)系统

EclipseJavaSwingMysql实现自助存取款机ATM系统一、系统介绍1.系统功能2.环境配置3.数据库4.工程截图二、系统展示1.登录页1.1登录成功2.注册系统3.取款3.1取款成功4.存款4.1 存款成功5.转账6.余额查询7.退出系统三、部分代码DBUtil.javaLoginFrame.javaAccount.java四、其他获…

python编程 for循环注意点与大写转换案例

作者简介&#xff1a;一名在校计算机学生、每天分享Python的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 range方法 一for循环表达式 for循环表达式 与数字有关 与单位有关 前言…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java基于信息安全的无锡旅游服务系统5l83d

面对老师五花八门的设计要求&#xff0c;首先自己要明确好自己的题目方向&#xff0c;并且与老师多多沟通&#xff0c;用什么编程语言&#xff0c;使用到什么数据库&#xff0c;确定好了&#xff0c;在开始着手毕业设计。 1&#xff1a;选择课题的第一选择就是尽量选择指导老师…

MHA的故障切换你掌握了吗?

MHA的概述 什么是MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过程…

用Python构建Amazon产品推荐系统!这项目价值能有2000吗?

引言 该项目的目标是部分重建Amazon电子类产品的推荐系统。 现在是十二月&#xff01;你是什么类型的购物者&#xff1f;双十二都购买了吗&#xff1f;你是把当天想购买的所有产品都保存起来&#xff0c;还是宁愿打开网站&#xff0c;查看带有巨大折扣的现场优惠呢&#xff1…

从单服务器模式到负载均衡设计

从单服务器模式到负载均衡设计 作者&#xff1a;Grey 原文地址&#xff1a; 博客园&#xff1a;从单服务器模式到负载均衡设计 CSDN&#xff1a;从单服务器模式到负载均衡设计 单服务器模型是最简单的一种架构&#xff0c;参考如下图 用户访问一个 URL&#xff0c;URL 会…

java计算机毕业设计基于安卓Android的校园外卖点餐APP

项目介绍 餐饮行业是一个传统的行业。根据当前发展现状,网络信息时代的全面普及,餐饮行业也在发生着变化,单就点餐这一方面,利用手机点单正在逐步进入人们的生活。传统的点餐方式,不仅会耗费大量的人力、时间,有时候还会出错。Android系统伴随智能手机为我们提供了新的方向。手…

基于转换器 (MMC) 技术和电压源转换器 (VSC) 的高压直流 (HVDC) 模型(MatlabSimulink实现)

目录 1 概述 2 主要模块说明 2.1 简化电网 2.2 转换器 1 2.3 直流电路 2.4 控制器 2.5 示波器和测量 3 讲解 3.1 参数设置 3.2 SPS 比较 3.3 结果比较 3.4 参考文献 4 Matlab代码实现 1 概述 1000 MW HVDC-MMC 互连的 SPS 模型。本文基于模块化多电平转换器 (MMC)…

计算机网络-数据链路层:以太网协议、ARP协议、MAC、MTU

目录 一、以太网协议 1. 认识以太网 2. 协议格式 二、 MAC地址 1. 认识MAC地址 2. 对比MAC地址与IP地址 三、ARP协议 1. 认识ARP协议 2. ARP协议的作用 3. ARP协议的工作流程 4. ARP欺骗攻击 四、MTU 1. 认识MTU 2. MTU对IP协议的影响&#xff08;了解&#xff…

在线教育系统源码讲解与代码分析

目前&#xff0c;许多行业已经开始向直播领域靠拢&#xff0c;例如直播带货、教育直播、娱乐直播等领域&#xff0c;想要在此分一杯羹&#xff0c;以在线教育系统来说&#xff0c;在2020年以后便进入了“白热化”&#xff0c;更多的直播、教育展现在大众视野中。在粉丝经济的时…

Linux的进程空间管理

用户态和内核态的划分 进程的虚拟地址空间&#xff0c;其实就是站在项目组的角度来看内存&#xff0c;所以我们就从task_struct出发来看。这里面有一个struct mm_struct结构来管理内存。 struct mm_struct *mm; 在struct mm_struct里面&#xff0c;有这样一个成员变量&#…

如何利用ArcGIS实现电子地图可视化表达?分析空间数据?提升SCI论文的层次?探究环境与生态因子对水体、土壤、大气污染物等影响?

查看原文>>>如何利用ArcGIS探究环境与生态因子对水体、土壤、大气污染物等影响 如何利用ArcGIS实现电子地图可视化表达&#xff1f;如何利用ArcGIS分析空间数据&#xff1f;如何利用ArcGIS提升SCI论文的层次&#xff1f;制图是地理数据展现的直观形式&#xff0c;也是…

ARM存储模型(数据存储、指令存储)

目录 1、ARM数据存储 (1) ARM数据类型 (2) ARM数据存储的方式 2、ARM的指令存储 (1) 指令集的分类 (2) 为什么ARM指令集的PC值与低2位无关&#xff1f; 1、ARM数据存储 (1) ARM数据类型 ARM采用32位架构&#xff0c;即ARM一次可以处理32bit的数据&#xff0c;基本的数据…

Linux--进程间通信(2)--共享内存--1126

1.共享内存的原理 进程A先申请一段内存空间&#xff0c;其页表映射到物理空间&#xff0c;进程B通过A的页表映射&#xff0c;将B的页表也同样映射到同一块物理空间。 释放共享内存 各自修改各自的页表指向并释放共享内存 2.共享内存的建立过程 头文件 comm.hpp #pragma o…

Windows系统dos命令之cmd

目录1. 使用cd命令快速切换到指定的盘符中1.1 参数说明1.2 cd 切换盘符2. 使用cd命令切换到指定的目录中2.1 切换指定目录3. 使用cd命令退回到上一层目录3.1 使用命令 ”cd ..“4. 使用cd命令直接退回到当前根目录下4.1 使用命令cd \1. 使用cd命令快速切换到指定的盘符中 cd 是…

JUC并发编程第十一篇,Java对象的内存布局

JUC并发编程第十一篇&#xff0c;Java对象的内存布局一、对象在堆内存中的存储布局1、对象头对象标记Mark Word类元信息&#xff08;类型指针&#xff09;2、实例数据3、对齐填充二、对象标记&#xff08;MarkWord&#xff09;布局与验证代码验证&#xff08;JOL&#xff09;一…

重点| 系统集成项目管理工程师考前50个知识点(4)

本文章总结了系统集成项目管理工程师考试背记50个知识点&#xff01;&#xff01;&#xff01; 帮助大家更好的复习&#xff0c;希望能对大家有所帮助 比较长&#xff0c;放了部分&#xff0c;需要可私信&#xff01;&#xff01; 26、编制范围管理计划的输出&#xff1a; 范…