并发控制艺术:使用互斥锁精确解决原子性问题

1. 引入多线程的原子性问题

当我们谈论软件开发中的多线程编程时,原子性问题总是绕不开的一个重点。原子性操作指的是在一系列操作中,要么所有的操作全部完成,要么全部不执行,它是一个不可分割的整体。在多线程环境中,保持操作的原子性显得尤为关键,因为数据通常在多个线程之间共享,如果不正确地管理,就很容易发生数据不一致的问题。

1.1 原子性基本概念

原子性,从字面上理解就是“不可再分的”,在编程世界里,这意味着一次操作是完整的,不会被其他线程打断。例如,一个简单的赋值操作int a = 1;在单线程环境下是原子性的,但在多线程环境中,如果有多个线程同时对a进行赋值,则结果可能会出乎意料。

1.2 多线程环境下原子性问题

以一个简单的增量操作为例:count++。这个操作看似简单,但实际上它包含三个步骤:读取count的值、增加1、存储回count。多线程环境下,如果两个线程同时执行这个操作,他们读取的count值可能是一样的,那么最后count的值可能比预期少增加了1。

1.3 原子性问题实际案例分析

考虑一个在线票务系统,假设有若干张票,多个用户同时在线抢票。如果票量的更新不是原子性的,可能会造成超卖的现象。例如,当剩余票量为1时,本来应该只能售给一个用户,但由于票量更新操作不是原子性的,多个线程可能同时检测到票量为1,导致多个用户都认为自己成功购票。

2. 互斥锁基本概念

在并发编程中,为了维持数据的一致性和完整性,我们需要引入一种机制来保证在同一时间只有一个线程能够访问特定的资源或执行特定的操作。这种机制就是所谓的“互斥锁”(Mutex)。互斥锁可以提供一种锁定机制,保证在任意时刻只有一个线程可以持有该锁,这样就可以保证线程安全地访问共享资源。

2.1 锁的定义与作用

锁(Lock)是用于控制多个线程对共享资源访问的同步机制。它的基本工作原理是当某个线程要访问共享资源时,必须先获得锁,访问结束后释放锁。如果锁已经被其他线程占用,那么尝试获取锁的线程必须等待。

2.2 锁的类型简介

锁有多种类型,根据不同的应用场景和需求,开发者可以选择不同类型的锁来应对。最常见的互斥锁包括互斥量(Mutex)、读写锁(ReadWrite Lock)、自旋锁(Spinlock)等。互斥量能够提供基本的互斥功能,读写锁允许多个读操作同时进行但只允许一个写操作,自旋锁则在等待锁的释放时不断检查锁状态,适用于锁持有时间非常短的情况。

2.3 如何选择合适的锁

选择适当的锁类型对于性能和资源的有效管理至关重要。对于大多数的应用场景,互斥量是一个比较好的起点。但如果程序涉及到更复杂的数据结构或者高频的读取操作,可能需要考虑读写锁。自旋锁在多核处理器上效果较好,但在单核处理器上可能会导致CPU资源的浪费。总的来说,选择锁的类型应基于对程序并发模式的仔细分析和理解。

3. Java中互斥锁的实现

在Java中,互斥锁主要通过synchronized关键字和ReentrantLock类实现。这两种方式有不同的使用场景和特性,开发者可以根据需要选择最合适的锁类型。

3.1 synchronized关键字简介

synchronized是Java内置的同步机制,它提供了一种简单而安全的方式来防止多个线程同时访问同一个资源。synchronized关键字可以用来标记方法或代码块。

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

在上述代码中,通过synchronized关键字,我们保证了increment和getCount方法在同一时间内只能由一个线程访问。

3.2 ReentrantLock类的使用

ReentrantLock是java.util.concurrent.locks包中的一个类,它提供了与synchronized相似的同步功能,但它更加灵活。ReentrantLock提供了尝试锁定tryLock,可中断的锁定lockInterruptibly,以及有超时限制的锁定tryLock(long timeout, TimeUnit unit)等高级功能。

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

使用ReentrantLock,我们可以更加明确地控制锁的获取和释放。

3.3 使用场景对比

synchronized较为便捷,因为它是Java的关键字,不需要显式地创建锁对象。适合简单同步需求的场景。而ReentrantLock提供了更多高级功能,适用于需要更细粒度控制的同步任务,例如尝试获得锁的时候加入超时机制,因此,在更复杂的同步场景中更加合适。

4. synchronized详细剖析

在Java多线程编程中,synchronized是实现同步的基本手段之一。它提供了一种排他的锁定机制,每次只允许一个线程访问同步资源。

4.1 使用synchronized同步代码块

synchronized可以用来同步一个代码块,该代码块中包含的操作对共享资源的访问是互斥的。

public class SharedResource {
    private Object lock = new Object();

    public void performAction() {
        synchronized(lock) {
            // 访问或修改共享资源
        }
    }
}

在上面的代码片段中,任何线程想要执行performAction方法里的同步代码块,必须先获得lock对象的锁定。

4.2 使用synchronized同步方法

synchronized还可用于方法级别,使整个方法成为同步方法。

public class SharedResource {

    public synchronized void performSyncAction() {
        // 整个方法体是同步的
    }
}

当一个线程访问SharedResource的一个synchronized同步方法时,其它线程对同一个实例的所有synchronized同步方法的访问将被阻塞。

4.3 synchronized的底层原理

synchronized使用对象监视器(monitor)实现同步。每个使用synchronized声明的对象关联一个监视器锁,当多个线程尝试同时访问该监视器时,只有一个线程得以执行,其它线程将处于阻塞状态。

4.4 反编译分析synchronized的实现

通过反编译我们会发现synchronized方法会在字节码层面使用ACC_SYNCHRONIZED访问修饰符。对于同步块,字节码中会出现monitorenter和monitorexit两个指令分别用来实现监视器对象的获取和释放。
Java的synchronized在逻辑上是简单易用的,但实际实现却涉及复杂的JVM层面操作。了解这些内部机制可以帮助我们更好地使用synchronized关键字。

5. ReentrantLock的深入解析

ReentrantLock是Java中一个强大的互斥锁实现,提供了比内置的synchronized锁更多的功能和更复杂的操作。它是java.util.concurrent.locks包的一部分,可以帮助开发者更细粒度地控制加锁过程。

5.1 ReentrantLock的基本使用

ReentrantLock实现了Lock接口,并提供了与synchronized类似的互斥锁定功能,但它更加灵活,因为它可以尝试获取锁、定时获取锁以及获取可中断锁。

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int sum = 0;

    public void increment() {
        lock.lock();
        try {
            sum++;
        } finally {
            lock.unlock();
        }
    }
    
    public int getSum() {
        lock.lock();
        try {
            return sum;
        } finally {
            lock.unlock();
        }
    }
}

在这个例子中,我们使用ReentrantLock来确保increment和getSum方法的线程安全。

5.2 ReentrantLock的高级特性

公平锁(Fairness Policy)

ReentrantLock可以设置为公平锁,确保等待时间最长的线程优先获取锁。

private final ReentrantLock fairLock = new ReentrantLock(true);

条件变量(Condition)

ReentrantLock还提供了条件变量(Condition),让线程可以基于特定条件等待或唤醒。

private final Condition condition = lock.newCondition();

public void waitForCondition() throws InterruptedException {
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}

public void signalCondition() {
    lock.lock();
    try {
        condition.signalAll();
    } finally {
        lock.unlock();
    }
}

5.3 反编译分析ReentrantLock的实现

像同步块一样,我们可以通过反编译来查看ReentrantLock如何在字节码层面操作。即使我们不会在字节码中发现与互斥锁直接相关的命令,因为ReentrantLock是利用Java类库实现,而synchronized是基于JVM级别实现的。
ReentrantLock的使用在高级并发编程中非常有用,尤其是那些需要额外灵活性和控制的场景。了解不同锁的工作原理和它们的适用范围是每位并发程序员的宝贵技能。

6. 解决count+=1原子性问题

在并发程序中,简单的操作如count+=1可以引发复杂的原子性问题。这个操作实际上包含了三个独立的步骤:读取count值,增加1,写回count值。在多线程环境中,如果不使用同步机制,就可能导致非预期的结果。下面将介绍使用synchronized和ReentrantLock两种方式来解决这个问题。

6.1 count+=1操作缺乏原子性示例

假设有一个简单的Counter类,它只是简单地增加内部计数器。

public class Counter {
    private int count = 0;

    public void increment() {
        count++; // 这不是一个原子操作
    }

    public int getCount() {
        return count;
    }
}

若两个线程同时执行increment()方法,他们可能读取到相同的count值,并基于这个值执行加操作,然后写回同一个值。这就导致了实际上的增量操作只执行了一次。

6.2 使用synchronized解决方案

我们通过在increment方法前添加synchronized关键字来确保对count操作的原子性。

public synchronized void increment() {
    count++;
}

现在每次只有一个线程能够修改count,从而解决了原子性问题。

6.3 使用ReentrantLock解决方案

我们还可以选择使用ReentrantLock来实现相同的功能。

private final ReentrantLock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

使用ReentrantLock也可以保证在任何时候只有一个线程执行increment方法,从而保持操作的原子性。

6.4 性能对比与最佳实践

通常来说,synchronized在语法上更加简洁,但在某些复杂的同步场景下可能不够用。而ReentrantLock提供更多的控制,但也需要更加小心地管理锁的释放。在性能方面,两者可能会有所不同,但这通常取决于具体的应用场景和锁的争用程度。一个最佳实践是,默认使用synchronized,对于更复杂的同步需求,再考虑使用ReentrantLock。

7. 锁优化技术

锁是解决多线程同步问题的有效工具,但是它们也会引入性能开销。因此,了解如何优化锁的使用对于构建高效的并发程序至关重要。下面将介绍一些常用的锁优化技术。

7.1 锁消除技术

锁消除是一种JIT编译器的优化技术,它分析代码来确定锁操作是否不必要,并消除这些不影响正确性的锁。例如,编译器可能识别到某些锁定的对象仅由一个线程访问,从而安全地移除这些锁。

7.2 锁粗化技术

如果在一个方法中多次获得同一个锁,这通常会导致性能降低。锁粗化是另一种优化手段,它扩大锁定的范围,让锁的请求次数减少。比如,将锁的范围从循环体内移至循环体外。

7.3 轻量级锁和偏向锁技术介绍

轻量级锁

轻量级锁是一种减小同步开销的技术,特别是当锁竞争不激烈时。轻量级锁避免了在无竞争情况下使用重量级的操作系统互斥量。

偏向锁

偏向锁是针对锁竞争极少的情况优化的一种锁。它通过消除与锁相关的大部分同步开销,提高了程序性能。一旦锁被一个线程获取,它就进入“偏向”模式,该锁会被偏向于第一个获取它的线程,后续的锁操作只需进行较轻量级的检查。

8. 线程安全的最佳实践

编写线程安全的代码对于避免程序中出现数据竞争和并发错误至关重要。实现线程安全可以采用多种方法,包括使用锁、线程本地封闭技术、不变性模式和并发类库。

8.1 线程封闭技术

线程封闭是一种通过限制对象只能由一个特定线程访问来实现线程安全的技术。例如,使用ThreadLocal类让每个线程都有自己的一个实例副本,这样就不需要担心多线程间的并发问题了。

public class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
}

使用上述代码,每个线程都会有自己的SimpleDateFormat实例,防止了并发访问的问题。

8.2 不变模式

不变模式意味着一旦对象被创建后其状态就不能修改,因此它本质上是线程安全的。在Java中,我们可以通过使用final关键字来创建不变的对象。

public final class ImmutableValue {
    private final int value;
    
    public ImmutableValue(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

上述不变类无需同步即可在多线程中安全使用。

8.3 使用并发类库

Java的java.util.concurrent包提供了一系列并发工具类,比如ConcurrentHashMap、CopyOnWriteArrayList等,它们优化了同步处理,提高了性能。

ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key", 1);

使用以上的并发集合可以在不显式使用锁的情况下提供线程安全。在掌握了线程安全的最佳实践后,开发者应该根据程序的具体需求来选择最合适的方法。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

free5gc+ueransim操作

启动free5gc容器 cd ~/free5gc-compose docker-compose up -d 记录虚拟网卡地址&#xff0c;eth0 ifconfig 查看并记录amf网元的ip地址 sudo docker inspect amf "IPAddress"那一行&#xff0c;后面记录的即是amf的ip地址 记录上述两个ip地址&#xff0c;完成UER…

MCU通过UART/SPI等接口更新flash的方法

MCU可提供一种方便的方式来更新flash内容以进行错误修复bugfix或产品更新update。可以使用以下任何模式更新flash内容: •系统内编程(ISP,In-System Programming):用于使用内部bootloader程序和UART/SPI对片上闪存进行编程program或重新编程reprogram。 •应用程序内编程…

一毛钱不到的FH8208C单节锂离子和锂聚合物电池一体保护芯片

前言 目前市场上电池保护板&#xff0c;多为分体方案&#xff0c;多数场合使用没有问题&#xff0c;部分场合对空间有进一步要求&#xff0c;或者你不想用那么多器件&#xff0c;想精简一些&#xff0c;那么这个芯片就很合适&#xff0c;对于充电电池来说&#xff0c;应在使用…

AI论文速读 |2024[IJCAI]TrajCL: 稳健轨迹表示:通过因果学习隔离环境混杂因素

题目&#xff1a; Towards Robust Trajectory Representations: Isolating Environmental Confounders with Causal Learning 作者&#xff1a;Kang Luo, Yuanshao Zhu, Wei Chen, Kun Wang(王琨), Zhengyang Zhou(周正阳), Sijie Ruan(阮思捷), Yuxuan Liang(梁宇轩) 机构&a…

AI数据中心网络技术选型,InfiniBand与RoCE对比分析

InfiniBand与RoCE对比分析&#xff1a;AI数据中心网络选择指南 随着 AI 技术的蓬勃发展&#xff0c;其对数据中心网络的要求也日益严苛。低延迟、高吞吐量的网络对于处理复杂的数据密集型工作负载至关重要。本文分析了 InfiniBand 和 RoCE 两种数据中心网络技术&#xff0c;帮助…

91、动态规划-不同的路径

思路&#xff1a; 首先我们可以使用暴力递归解法&#xff0c;无非就是每次向下或者向右看看是否有解法&#xff0c;代码如下&#xff1a; public class Solution {public int uniquePaths(int m, int n) {return findPaths(0, 0, m, n);}private int findPaths(int i, int j,…

数据结构-线性表-应用题-2.2-12

1&#xff09;算法的基本设计思想&#xff1a;依次扫描数组的每一个元素&#xff0c;将第一个遇到的整数num保存到c中&#xff0c;count记为1&#xff0c;若遇到的下一个整数还是等于num,count,否则count--,当计数减到0时&#xff0c;将遇到的下一个整数保存到c中&#xff0c;计…

04.2.配置应用集

配置应用集 应用集的意思就是&#xff1a;将多个监控项添加到一个应用集里面便于管理。 创建应用集 填写名称并添加 在监控项里面找到对应的自定义监控项更新到应用集里面 选择对应的监控项于应用集

[疑难杂症2024-004] 通过docker inspect解决celery多进程记录日志莫名报错的记录

本文由Markdown语法编辑器编辑完成&#xff0e; 写作时长: 2024.05.07 ~ 文章字数: 1868 1. 前言 最近我负责的一个服务&#xff0c;在医院的服务器上线一段时间后&#xff0c;利用docker logs查看容器的运行日志时&#xff0c;发现会有一个"莫名其妙"的报错&…

Verilog中4bit超前进位加法器

4bit超前进位加法器的逻辑表达式如下&#xff1a; 中间变量GiAiBi&#xff0c;PiAi⊕BiGi​Ai​Bi​&#xff0c;Pi​Ai​⊕Bi​ 和&#xff1a;SiPi⊕Ci−1Si​Pi​⊕Ci−1​&#xff0c;进位&#xff1a;CiGiPiCi−1Ci​Gi​Pi​Ci−1​ 用Verilog语言采用门级描述方式&am…

Buuctf-Misc题目练习

打开后是一个gif动图&#xff0c;可以使用stegsolve工具进行逐帧看。 File Format:文件格式 Data Extract:数据提取 Steregram Solve:立体试图 可以左右控制偏移 Frame Browser:帧浏览器 Image Combiner:拼图&#xff0c;图片拼接 所以可以知道我们要选这个Frame Browser …

odoo实施之创建行业demo

创建数据库&#xff0c;添加公司数据 选择应用&#xff0c;获取15天免费试用 创建完成 设置客户公司logo 创建用户 更改用户语言 前置条件&#xff1a;配置邮件 开发模式下&#xff0c;额外信息 加载demo数据

微信小程序 手机号授权登录

手机号授权登录 效果展示 这里面用的是 uni-app 官方的登录 他支持多端发布 https://zh.uniapp.dcloud.io/api/plugins/login.html#loginhttps://zh.uniapp.dcloud.io/api/plugins/login.html#login 下面是代码 <template><!-- 授权按钮 --><button v-if&quo…

微软 AI 研究团队推出 SIGMA:一个开源研究平台,旨在推动混合现实与人工智能交叉领域的研究与创新

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

如何永久删除服务和相关文件夹

如何永久删除服务和文件夹&#xff1f; How can I remove the service and folder permanently? 以AlibabaProtect服务为例 takeown /f "C:\Program Files (x86)\AlibabaProtect sc delete AlibabaProtect我运行了上述操作&#xff0c;并通过任务管理器杀死了“阿里巴巴…

AI时代的就业转型与个人发展

AI时代的就业转型与个人发展&#xff1a;机遇与挑战并存 AI出现的背景&#xff1a;技术革命的浪潮 随着21世纪信息技术的突飞猛进&#xff0c;人工智能&#xff08;Artificial Intelligence, AI&#xff09;作为一场技术革命的产物&#xff0c;正逐渐从科幻小说走向现实世界的…

linux的信号量的使用

1.信号量 在多线程情况下&#xff0c;线程要进入关键代码就得获取信号量&#xff08;钥匙&#xff09;{sem_init(&sem, 0, 0);}&#xff0c;没有信号量的情况下就一直等待sem_wait(&sem)&#xff0c;只到别人把钥匙&#xff08;sem_post(&sem)&#xff09;给你。 …

淘宝数据分析——Python爬虫模式♥

大数据时代&#xff0c; 数据收集不仅是科学研究的基石&#xff0c; 更是企业决策的关键。 然而&#xff0c;如何高效地收集数据 成了摆在我们面前的一项重要任务。 本文将为你揭示&#xff0c; 一系列实时数据采集方法&#xff0c; 助你在信息洪流中&#xff0c; 找到…

Linux提示:mount: 未知的文件系统类型“ntfs”

mount: 未知的文件系统类型“ntfs” 在Linux系统中&#xff0c;如果遇到“mount: 未知的文件系统类型‘ntfs’”的错误&#xff0c;这通常意味着您的系统没有安装支持NTFS文件系统的软件。为了挂载NTFS文件系统&#xff0c;您需要安装ntfs-3g软件包。以下是如何在不同Linux发行…

【Git】Git学习-10-11:GitHub,SHH配置,克隆仓库

学习视频链接&#xff1a;【GeekHour】一小时Git教程_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1HM411377j/?vd_source95dda35ac10d1ae6785cc7006f365780 创建仓库 配置SSH密钥可以更加安全&#xff0c;方便地推送、拉取代码 根目录下&#xff0c;进入.ssh文件&am…