深入了解Lock同步锁的优化

news2024/12/23 6:21:32

大家好,我是易安。

今天我们来简单谈谈在JDK1.5之后,Java提供的Lock同步锁。

相对于需要JVM隐式获取和释放锁的Synchronized同步锁,Lock同步锁(以下简称Lock锁)需要的是显示获取和释放锁,这就为获取和释放锁提供了更多的灵活性。Lock锁的基本操作是通过乐观锁来实现的,但由于Lock锁也会在阻塞时被挂起,因此它依然属于悲观锁。我们可以通过一张图来简单对比下两个同步锁,了解下各自的特点:

alt

从性能方面上来说,在并发量不高、竞争不激烈的情况下,Synchronized同步锁由于具有分级锁的优势,性能上与Lock锁差不多;但在高负载、高并发的情况下,Synchronized同步锁由于竞争激烈会升级到重量级锁,性能则没有Lock锁稳定。

我们可以通过一组简单的性能测试,直观地对比下两种锁的性能,结果见下方,代码可以在 Github 上下载查看。

alt

通过以上数据,我们可以发现:Lock锁的性能相对来说更加稳定。

Lock锁的实现原理

Lock锁是基于Java实现的锁,Lock是一个接口类,常用的实现类有ReentrantLock、ReentrantReadWriteLock(RRW),它们都是依赖AbstractQueuedSynchronizer(AQS)类实现的。

AQS类结构中包含一个基于链表实现的等待队列(CLH队列),用于存储所有阻塞的线程,AQS中还有一个state变量,该变量对ReentrantLock来说表示加锁状态。

该队列的操作均通过CAS操作实现,我们可以通过一张图来看下整个获取锁的流程。

alt

锁分离优化Lock同步锁

虽然Lock锁的性能稳定,但也并不是所有的场景下都默认使用ReentrantLock独占锁来实现线程同步。

我们知道,对于同一份数据进行读写,如果一个线程在读数据,而另一个线程在写数据,那么读到的数据和最终的数据就会不一致;如果一个线程在写数据,而另一个线程也在写数据,那么线程前后看到的数据也会不一致。这个时候我们可以在读写方法中加入互斥锁,来保证任何时候只能有一个线程进行读或写操作。

在大部分业务场景中,读业务操作要远远大于写业务操作。而在多线程编程中,读操作并不会修改共享资源的数据,如果多个线程仅仅是读取共享资源,那么这种情况下其实没有必要对资源进行加锁。如果使用互斥锁,反倒会影响业务的并发性能,那么在这种场景下,有没有什么办法可以优化下锁的实现方式呢?

1.读写锁ReentrantReadWriteLock

针对这种读多写少的场景,Java提供了另外一个实现Lock接口的读写锁RRW。我们已知ReentrantLock是一个独占锁,同一时间只允许一个线程访问,而RRW允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁,一个是用于读操作的ReadLock,一个是用于写操作的WriteLock。

那读写锁又是如何实现锁分离来保证共享资源的原子性呢?

RRW也是基于AQS实现的,它的自定义同步器(继承AQS)需要在同步状态state上维护多个读线程和一个写线程的状态,该状态的设计成为实现读写锁的关键。RRW很好地使用了高低位,来实现一个整型控制两种状态的功能,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。

一个线程尝试获取写锁时, 会先判断同步状态state是否为0。如果state等于0,说明暂时没有其它线程获取锁;如果state不等于0,则说明有其它线程获取了锁。

此时再判断同步状态state的低16位(w)是否为0,如果w为0,则说明其它线程获取了读锁,此时进入CLH队列进行阻塞等待;如果w不为0,则说明其它线程获取了写锁,此时要判断获取了写锁的是不是当前线程,若不是就进入CLH队列进行阻塞等待;若是,就应该判断当前线程获取写锁是否超过了最大次数,若超过,抛异常,反之更新同步状态。

alt

一个线程尝试获取读锁时, 同样会先判断同步状态state是否为0。如果state等于0,说明暂时没有其它线程获取锁,此时判断是否需要阻塞,如果需要阻塞,则进入CLH队列进行阻塞等待;如果不需要阻塞,则CAS更新同步状态为读状态。

如果state不等于0,会判断同步状态低16位,如果存在写锁,则获取读锁失败,进入CLH阻塞队列;反之,判断当前线程是否应该被阻塞,如果不应该阻塞则尝试CAS同步状态,获取成功更新同步锁为读状态。

alt

下面我们通过一个求平方的例子,来感受下RRW的实现,代码如下:

public class TestRTTLock {

 private double x, y;

 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 // 读锁
 private Lock readLock = lock.readLock();
 // 写锁
 private Lock writeLock = lock.writeLock();

 public double read() {
  //获取读锁
  readLock.lock();
  try {
   return Math.sqrt(x * x + y * y);
  } finally {
   //释放读锁
   readLock.unlock();
  }
 }

 public void move(double deltaX, double deltaY) {
  //获取写锁
  writeLock.lock();
  try {
   x += deltaX;
   y += deltaY;
  } finally {
   //释放写锁
   writeLock.unlock();
  }
 }

}

2.读写锁再优化之StampedLock

RRW被很好地应用在了读大于写的并发场景中,然而RRW在性能上还有可提升的空间。在读取很多、写入很少的情况下,RRW会使写入线程遭遇饥饿(Starvation)问题,也就是说写入线程会因迟迟无法竞争到锁而一直处于等待状态。

在JDK1.8中,Java提供了StampedLock类解决了这个问题。StampedLock不是基于AQS实现的,但实现的原理和AQS是一样的,都是基于队列和锁状态实现的。与RRW不一样的是,StampedLock控制锁有三种模式: 写、悲观读以及乐观读,并且StampedLock在获取锁时会返回一个票据stamp,获取的stamp除了在释放锁时需要校验,在乐观读模式下,stamp还会作为读取共享资源后的二次校验。

我们先通过一个官方的例子来了解下StampedLock是如何使用的,代码如下:

public class Point {
    private double x, y;
    private final StampedLock s1 = new StampedLock();

    void move(double deltaX, double deltaY) {
        //获取写锁
        long stamp = s1.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            //释放写锁
            s1.unlockWrite(stamp);
        }
    }

    double distanceFormOrigin() {
        //乐观读操作
        long stamp = s1.tryOptimisticRead();
        //拷贝变量
        double currentX = x, currentY = y;
        //判断读期间是否有写操作
        if (!s1.validate(stamp)) {
            //升级为悲观读
            stamp = s1.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                s1.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

我们可以发现:一个写线程获取写锁的过程中,首先是通过WriteLock获取一个票据stamp,WriteLock是一个独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当没有线程持有读锁或者写锁的时候才可以获取到该锁。请求该锁成功后会返回一个stamp票据变量,用来表示该锁的版本,当释放该锁的时候,需要unlockWrite并传递参数stamp。

接下来就是一个读线程获取锁的过程。首先线程会通过乐观锁tryOptimisticRead操作获取票据stamp ,如果当前没有线程持有写锁,则返回一个非0的stamp版本信息。线程获取该stamp后,将会拷贝一份共享资源到方法栈,在这之前具体的操作都是基于方法栈的拷贝数据。

之后方法还需要调用validate,验证之前调用tryOptimisticRead返回的stamp在当前是否有其它线程持有了写锁,如果是,那么validate会返回0,升级为悲观锁;否则就可以使用该stamp版本的锁对数据进行操作。

相比于RRW,StampedLock获取读锁只是使用与或操作进行检验,不涉及CAS操作,即使第一次乐观锁获取失败,也会马上升级至悲观锁,这样就可以避免一直进行CAS操作带来的CPU占用性能的问题,因此StampedLock的效率更高。

总结

不管使用Synchronized同步锁还是Lock同步锁,只要存在锁竞争就会产生线程阻塞,从而导致线程之间的频繁切换,最终增加性能消耗。因此,如何降低锁竞争,就成为了优化锁的关键。

在Synchronized同步锁中,我们了解了可以通过减小锁粒度、减少锁占用时间来降低锁的竞争。我们知道可以利用Lock锁的灵活性,通过锁分离的方式来降低锁竞争。

Lock锁实现了读写锁分离来优化读大于写的场景,从普通的RRW实现到读锁和写锁,到StampedLock实现了乐观读锁、悲观读锁和写锁,都是为了降低锁的竞争,促使系统的并发性能达到最佳。

如果本文对你有帮助的话,欢迎点赞分享,这对我继续分享&创作优质文章非常重要。感谢 !

本文由 mdnice 多平台发布

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

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

相关文章

防止机械/移动硬盘休眠 - NoSleepHD

防止机械/移动硬盘休眠 - NoSleepHD 前言解决方案计算机硬盘移动硬盘 前言 机械硬盘休眠后唤醒需要一定时间,且频繁的启动和停止并不有利于硬盘的寿命,因此可根据自身需求防止机械硬盘休眠,下文以Win10系统为例介绍解决方案。 值得一提的是…

Java核心技术 卷1-总结-9

Java核心技术 卷1-总结-9 使用异常机制的技巧为什么要使用泛型程序设计定义简单泛型类泛型方法类型变量的限定 泛型类型的继承规则 使用异常机制的技巧 1.异常处理不能代替简单的测试。 使用异常的基本规则是:只在异常情况下使用异常机制。 2.不要过分地细化异常。…

第三章(3):深入理解Spacy库基本使用方法

第三章(3):深入理解Spacy库基本使用方法 本章主要介绍了Spacy库的基本使用方法,包括安装、加载语言模型、分句、分词、词性标注、停用词识别、命名实体识别、依存分析和词性还原等内容。重点介绍了每个步骤的具体实现方式和应用场…

【TortoiseGit】安装和配置

转自 【TortoiseGit】TortoiseGit安装和配置详细说明_No8g攻城狮的博客-CSDN博客 一、TortoiseGit 简介 TortoiseGit 是基于 TortoiseSVN 的 Git 版本的 Windows Shell 界面。它是开源的,可以完全使用免费软件构建。 TortoiseGit 支持你执行常规任务,…

出道即封神的ChatGPT,现在怎么样了?ChatGPT想干掉测试人员,做梦去吧

从互联网的普及到智能手机,都让广袤的世界触手而及,如今身在浪潮中的我们,已深知其力。 前阵子爆火的ChatGPT,不少人保持观望态度。现如今,国内关于ChatGPT的各大社群讨论,似乎沉寂了不少,现在…

Mosquitto vs NanoMQ | 2023 MQTT Broker 对比

引言 Mosquitto 和 NanoMQ 都是用 C/C 开发的快速轻量的开源 MQTT Broker,完全支持 MQTT 3.1.1 和 5.0。 虽然 Mosquitto 和 NanoMQ 都具有轻量级和低资源消耗的特点,但它们的架构设计却截然不同。Mosquitto 采用单线程模式,而 NanoMQ 则基…

数据结构:单向链表(无头非循环)

朋友们、伙计们,我们又见面了,本期来给大家解读一下数据结构方面有关链表的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C语言专栏:C语言:从入门到…

【云原生】prometheus监控告警之安装部署alertmanager实战

前言 🏠个人主页:我是沐风晓月 🧑个人简介:大家好,我是沐风晓月,阿里云社区博客专家😉😉 💕 座右铭: 先努力成长自己,再帮助更多的人 &#xff0c…

Python基础知识:绝对/相对路径等

1 Python处理相对/绝对路径 由于本人在导入数据时,十分喜欢相对路径(在数据的上一级文件中,新建文件夹保存处理整个代码处理过程),因此,将首先简单介绍下Python中相对/绝对路径的处理。 1.1 绝对路径 一…

状态压缩DP-蒙德里安的梦想

题意 求把 NM 的棋盘分割成若干个 12 的长方形,有多少种方案。 例如当 N2,M4 时,共有 5 种方案。当 N2,M3 时,共有 3 种方案。 如下图所示: 输入格式 输入包含多组测试用例。 每组测试用例占一行&#xff0…

Jupyter notebook安装教程

文章目录 前言一、安装步骤1、安装 Python 编译器2、安装 jupyter3、运行 Jupyter notebook 二、 更改打开文件位置和快捷启动方式1、更改打开文件位置2、创建快捷启动方式 前言 Jupyter Notebook 是以网页的形式打开,可以在网页页面中直接编写代码和运行代码&…

20230421 | 203. 移除链表元素、707. 设计链表、206. 反转链表

1、203. 移除链表元素 方法1:不添加虚拟节点方式,但是要注意处理删除头部的数据 时间复杂度 O(n) 空间复杂度 O(1) /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* List…

婚恋交友app开发中需要注意的安全问题

前言 随着移动设备的普及,婚恋交友app已经成为了人们生活中重要的一部分。但是,这些应用的开发者需要确保应用的安全性,以保护用户的隐私和数据免受攻击。本文将介绍在婚恋交友app开发中需要注意的安全问题。 在当今数字化时代,…

狗屁不通文章生成器下载-狗屁不通生成器网址

狗屁不通文章生成器 狗屁不通文章生成器(也称为“吹牛生成器”)是使用自然语言处理技术和机器学习算法生成随机文章的工具。这些文章往往没有意义,因为它们是从各种不相关的话语中随机组合而成的。 虽然这些文章看起来毫无意义,…

显卡GPU与CUDA

文章目录 1 什么是GPU1.1 独立显卡1.2 核心显卡 2 驱动3 深度学习显卡CUDA4 GPU VS CPU5 深度学习环境配置中各软件的关系windows下判断有无NVIDIA GPU16G512G8核14核 1 什么是GPU GPU就是显卡Graphics Processing Unit 图像处理单元显卡主要用于在屏幕上显示图像,…

ROS学习第二十六节——机器人仿真相关组件

1.URDF URDF是 Unified Robot Description Format 的首字母缩写,直译为统一(标准化)机器人描述格式,可以以一种 XML 的方式描述机器人的部分结构,比如底盘、摄像头、激光雷达、机械臂以及不同关节的自由度.....,该文件可以被 C 内置的解释器…

服务(第十篇)Nginx和tomcat反向代理(动静分离)

正向代理: 当用户想访问某一网址时,用户先访问代理服务器,然后由代理服务器向目标网址发送请求最终将数据返回代理服务器,最后代理服务器将数据返回给用户这一过程我们称之为正向代理。 反向代理:基本流程是与正向代理…

毕业-单片机-嵌入式~三年经历回顾

入行嵌入式软件开发 20年6月疫情第一次缓和、实操51单片机;20年9月郑州实习、温湿度采集类低功耗产品、初次接触ARM Cortex M0/M3 单片机;21年5月毕业来到杭州、不懂应届生的宝贵青春!匆匆忙忙进厂~人生中第一个项目:…

使用vscode、cmake配置c++开发环境

使用vscode、cmake配置c开发环境 下载软件安装包VS codegcc编译器cmake 设置环境变量VS code插件安装 下载软件安装包 VS code 根据自己电脑选择合适版本下载安装即可。 官网链接:https://code.visualstudio.com/download gcc编译器 我使用的是mingw-w64编译器…

kong(2):docker搭建kong环境

Kong 安装有两种方式一种是没有数据库依赖的DB-less 模式,另一种是with a Database 模式。我们这里使用第二种带Database的模式,因为这种模式功能更全。 1 docker安装Kong 1.1构建Kong的容器网络 首先我们创建一个Docker自定义网络,以允许容…