Java并发编程:解锁并发编程中的加锁技巧

1. 并发编程基础

并发编程是现代软件开发的核心之一,尤其在处理大规模用户访问的场景下。为了正确处理多线程编程的复杂性,理解基础概念至关重要。本章节将深入讨论并发编程的基础,包括并发与并行的区别,线程安全的基本概念,以及加锁机制的重要性。

1.1. 并发与并行的区别

并发(Concurrency)和并行(Parallelism)常常被混用,但它们描述的是两种不同的情况:

  • 并发指的是多个任务可以在重叠的时间段内启动、运行和完成。并发性关注的是系统如何有效地处理多个任务,使它们似乎是同时进行的。
  • 并行则更进一步,它涉及到同一时刻同时进行多项操作。并行性在多核处理器上最为常见,不同的处理器核心可以真正地在同一时间执行不同的任务。
// 演示Java中的并发执行
Runnable task = () -> {String threadName = Thread.currentThread().getName();System.out.println("Hello " + threadName);
};Thread thread = new Thread(task);
thread.start();System.out.println("Done!");

1.2. 线程安全的基本概念

线程安全是指程序在多线程环境下运行时,能够正确处理多个线程之间的共享资源访问,使得无论运行时环境如何调度这些线程,程序都能正确执行。
要实现线程安全,关键在于如何同步对共享资源的访问,确保在任意时刻只有一个线程可以访问该资源。这通常需要加锁机制来实现。

1.3. 加锁机制的重要性

在多线程环境中,安全地访问共享资源通常需要通过加锁来协调不同线程的操作。Java 提供了多种加锁机制,常见的如同步代码块(synchronized)和 ReentrantLock。

  • synchronized:Java 中的关键字,可以同步方法或者代码块。它保证了同一时刻只有一个线程可以执行被 synchronized 修饰的方法或代码块。
  • ReentrantLock:一个 Java 类,相比于 synchronized 提供了更高级的功能,如能够尝试非阻塞地获取锁,支持可中断的锁获取等。
// 使用 synchronized 关键字的例子
public synchronized void increment() {// 这个方法同一时间只能被一个线程访问
}// 使用 ReentrantLock 类的例子
private final ReentrantLock lock = new ReentrantLock();public void increment() {lock.lock();try {// 访问共享资源} finally {lock.unlock();}
}

2. 分析场景:加锁问题的深层原因

深入理解加锁问题,需要我们从多个角度对并发场景进行分析。加锁机制的设计和实现必须考虑到场景的特性,否则可能会引发死锁、饥饿或资源竞争等问题。本章我们将从以下三个常见问题分析开始,挖掘加锁问题的根源。

2.1. 死锁

当两个或更多的线程在执行过程中,因为争夺资源而相互等待对方释放资源,从而陷入无限等待的状态,这种现象称为死锁。死锁的产生通常需要四个必要条件同时满足:互斥条件、请求与保持、不剥夺条件和循环等待条件。
解决死锁的策略包括预防、避免以及检测和恢复。在Java中,可以通过锁排序、锁超时、死锁检测等机制来防止死锁。

2.2. 饥饿

饥饿是指在并发场景下,一个或多个线程因为各种原因长时间得不到所需的资源,无法继续执行。饥饿的原因可能是线程优先级设置不当,或者某些线程一直持有对资源的锁定,导致其他线程长时间等待。
合理地分配资源和调度线程是解决饥饿问题的关键,例如,公平锁(fair lock)可以确保先等待的线程先获得锁。

2.3. 资源竞争

在多线程环境中,当多个线程同时争夺有限的资源时,会出现资源竞争。资源竞争不仅降低了程序的效率,还可能导致数据不一致等严重问题。
避免资源竞争的方法包括减少对共享资源的访问,使用线程局部存储(Thread-Local Storage,TLS)以及构建无锁数据结构等。

// 死锁的示例
public class DeadLockDemo {private final Object lock1 = new Object();private final Object lock2 = new Object();public void method1() {synchronized (lock1) {synchronized (lock2) {// 执行相关操作}}}public void method2() {synchronized (lock2) {synchronized (lock1) {// 执行相关操作}}}
}

上述代码存在潜在的死锁风险,当一个线程试图按照method1的锁定顺序获取锁,而另一个线程试图按照method2的锁定顺序获取锁时,可能会导致死锁。

3. 没有直接业务关系的场景

在一些并发场景中,即使线程操作的业务没有直接关系,也可能由于共享同一资源造成意想不到的冲突和性能问题。本章节将探讨没有直接业务关系但因资源共享导致的并发问题,以及如何有效地解决这些问题。

3.1. 伪共享问题及解决方案

伪共享(False Sharing)是多线程编程中的一个隐蔽问题。当多个线程访问并修改相邻的内存位置时,即便这些变量彼此之间没有直接关联,它们也可能处于同一个缓存行中。这将导致缓存行无效化,进而影响性能。
为了解决伪共享问题,我们可以通过增大数组的间隔、使用@Contended标注或者其他避免伪共享的策略。

3.2. 锁颗粒度与性能的关系

锁的颗粒度决定了并发控制的精细程度。粗颗粒度的锁(例如,锁定整个数据结构)可能简化程序的并发控制,但也可能降低性能。细颗粒度的锁能够提供更高的并发级别,但管理起来更为复杂,也可能带来更高的开销。
选择正确的锁颗粒度是关键,通常需要在性能与易用性之间做出权衡。

3.3. 锁升级策略

为了优化性能,可以采取锁升级的策略,例如从乐观锁升级为悲观锁,或者从读锁升级为写锁。这要求系统能够根据当前的执行情况和资源竞争状况动态地调整锁的类型和级别。

// 简化示例:读写锁的使用
public class ReadWriteMap<K, V> {private final HashMap<K, V> map = new HashMap<>();private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private final Lock r = lock.readLock();private final Lock w = lock.writeLock();public V put(K key, V value) {w.lock();try {return map.put(key, value);} finally {w.unlock();}}public V get(K key) {r.lock();try {return map.get(key);} finally {r.unlock();}}
}

以上是一个使用读写锁来管理对HashMap的访问的简单示例。读写锁让多线程能同时读取数据,同时又能保证写操作的独占性。

4. 存在直接业务关系的场景

当线程之间存在直接的业务关系,对共享数据的操作尤其需要精心设计同步机制。本章将讨论在业务逻辑紧密相连的情况下,如何处理加锁问题,避免造成性能瓶颈和数据不一致。

4.1. 业务间关联性导致的死锁

在有业务关系的场景中,由于业务逻辑的相互依赖,更容易产生死锁。一旦发生,整个系统将无法进行下去。为了避免这种情况,需要设计一个全局的锁顺序,所有线程按照这个顺序获取锁,或者在设计阶段就使用无锁的设计模式。

4.2. 分布式锁的应用及问题

在微服务或分布式系统中,常常需要跨多个服务或节点进行资源同步,这就涉及到分布式锁。分布式锁相比本地锁更为复杂,它需要网络通信来协调锁状态,因此对可靠性和延迟要求更高。使用时应当考虑它的开销和复杂度。

4.3. 数据库锁机制的复杂性

数据库是业务中用于存储共享数据的常见组件。在大型应用中,数据库锁通常是处理并发控制的核心。数据库锁具有多种类型,如行锁、表锁及意向锁,它们在使用中需要仔细规划,以减少锁的竞争和提升并发性能。

// 分布式锁的示例代码
public class DistributedLock {private final String lockId;private final RedissonClient client;public DistributedLock(String lockId, RedissonClient client) {this.lockId = lockId;this.client = client;}public void lock() {RLock lock = client.getLock(lockId);lock.lock();}public void unlock() {RLock lock = client.getLock(lockId);lock.unlock();}
}

在上述代码中,Redisson 提供的 RLock 对象被用作分布式锁,确保跨节点间的操作协同。

5. 正确的加锁

在并发编程中,正确加锁至关重要。本章将详细讨论如何在Java中使用内置锁和显式锁,并针对不同的使用场景提供最佳实践。

5.1. 正确使用内置锁 synchronized

Java语言提供了内置锁机制synchronized,它依赖于对象内部的监视器锁(monitor lock)。每当线程进入synchronized标记的方法或代码块时,它将自动获得锁,并在退出时释放锁。这确保了临界区的代码在同一时间只能被一个线程执行。
使用synchronized时要注意避免持有锁的时间过长,以减少对其他线程的阻塞。

5.2. ReentrantLock 的正确姿势

与synchronized不同,ReentrantLock是一种显式锁。它提供了更灵活的锁定和解锁操作,允许尝试非阻塞地获取锁,支持中断锁等待的线程,以及实现公平锁等机制。
尽量在finally块内释放锁,以防止因异常而导致的锁无法正确释放的情况。

5.3. 使用 ReadWriteLock 优化读写性能

ReadWriteLock提供了一对锁 - 读锁和写锁。多个读线程可以同时持有读锁,而写锁则保证了写操作的独占性。当读操作远多于写操作时,使用ReadWriteLock可以大大提高性能。

5.4. 分布式锁的最佳实践

分布式锁必须要跨网络边界,因此应优先考虑锁的必要性和持有时间。使用分布式锁时应确保锁的释放机制健全,避免因为服务崩溃而导致的锁永远不释放的情况。同时,应当使用成熟的分布式锁实现,例如基于Redis或ZooKeeper的锁。

// synchronized的使用示例
public class SynchronizedCounter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}// ReentrantLock的使用示例
public class LockedCounter {private int count = 0;private final Lock lock = new ReentrantLock();public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}}
}

在上述示例中,SynchronizedCounter使用了synchronized关键字实现线程安全,而LockedCounter使用了ReentrantLock。选择哪一种方式取决于具体的应用场景和性能要求。

6. 实战案例:解决高并发下的加锁问题

理论知识和最佳实践对理解加锁至关重要,但是要真正掌握如何在实际开发中处理加锁问题,最有效的方法是通过实战案例来学习。接下来我们将通过一个典型的高并发场景来展示如何定位问题、制定策略,并通过代码实现来解决加锁问题。

6.1. 场景描述

想象我们有一个在线电商平台,用户可以浏览商品并下单购买。在一个促销活动期间,平台经历了巨大的访问压力,特别是对于某些热门商品,系统需要处理数以千计的并发请求。其中一个问题是,当多线程同时更新库存数据时,发生了数据不一致的情况。

6.2. 问题定位

通过分析日志和系统监控工具,我们确定问题出现在处理库存更新的服务中。系统中使用了数据库的乐观锁来防止数据冲突,但由于热点数据的频繁更新,乐观锁导致大量的事务冲突和重试,严重影响了性能。

6.3. 解决方案与代码实现

为了解决这个问题,我们决定采用基于Redis的分布式锁来控制对共享资源的访问。当一个线程需要更新库存时,它会首先尝试获取一个分布式锁,只有在获取锁之后才能执行更新操作。

public class InventoryService {private static final String INVENTORY_LOCK_KEY = "inventory_lock";private final RedissonClient redissonClient;public InventoryService(RedissonClient redissonClient) {this.redissonClient = redissonClient;}public boolean updateStock(String productId, int quantity) {RLock lock = redissonClient.getLock(INVENTORY_LOCK_KEY + ":" + productId);try {// 尝试获取锁,最多等待100秒,上锁以后10秒自动解锁if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {// 执行库存更新操作// ...return true;}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}return false;}
}

在上述代码中,我们使用RedissonClient创建一个针对每个商品ID的分布式锁。这个方法尝试获取锁,如果成功,更新库存;如果在指定时间内无法获取锁,这意味着其他线程正持有锁并操作库存,当前线程可能会选择重新尝试或者直接失败。

7. 避免常见加锁问题的技巧

加锁机制虽然是并发控制的基石,但如果不当使用,同样会引起线程安全问题,甚至影响性能。以下是一些避免常见加锁问题的技巧,让我们的应用更加稳健和高效。

7.1. 锁消除

锁消除是一种编译器优化技术,它识别并移除那些不可能存在共享数据竞争的代码块上的锁。例如,在局部变量的使用上往往就没有必要使用锁。

7.2. 锁粗化

通常,我们尽量减小锁的范围,以降低锁的竞争。但在某些情况下,频繁地加锁解锁同一个锁对象会导致额外的性能开销。在这种情况下,可以将多个连续的锁扩展成一个锁区块,这就是锁粗化。

7.3. 乐观锁与悲观锁的场景选择

乐观锁和悲观锁是处理并发控制的两种截然不同的策略。乐观锁适用于读多写少的场景,它假设冲突发生的概率较低,通过版本号或者CAS操作来实现。悲观锁则假设冲突很可能发生,因此每次读写都会先加锁。
合理的选择锁的策略可以大大提升应用性能。在具体实施时,还应该结合业务场景来决定使用哪种锁机制。

// 乐观锁的使用示例
public class OptimisticLock {private volatile int version = 0;public boolean tryUpdate(int newValue) {int currentVersion = version;// 模拟数据检查和准备更新数据的过程if (checkAndUpdate()) {if (currentVersion == version) {version++;// 更新数据return true;}}// 数据版本变化了,更新失败return false;}private boolean checkAndUpdate() {// 模拟数据检查逻辑return true;}
}

在这个乐观锁的示例中,我们通过比较数据版本号来确保数据在读取与更新之间没有被其他线程修改。

8. 未来趋势:无锁编程

传统的并发编程依赖于锁来同步线程,但锁机制也可能导致多种性能问题。随着硬件的发展和并发编程模型的演进,无锁编程逐渐成为了提升并发性能的一种趋势。本章我们将了解无锁编程,它的适用场景和具体实现。

8.1. 什么是无锁编程

无锁编程是一种不使用传统锁机制来实现线程同步的编程范式。它依赖于原子操作来确保线程安全,这样的操作可以直接由现代处理器提供支持。

8.2. 无锁编程的应用场景

无锁编程特别适用于读操作远多于写操作,并且写操作不频繁的场景。因为在这种情况下使用锁可能会严重影响性能。

8.3. CAS操作与无锁数据结构

CAS(Compare-And-Swap)操作是进行无锁编程的基础。它是一条原子指令,用来在内存中进行条件更新。如果内存位置的值与给定值相同,那么处理器自动将该内存位置的值更新为新值。
无锁数据结构如java.util.concurrent包中的ConcurrentLinkedQueue和ConcurrentHashMap,就是利用CAS操作来避免使用锁。

import java.util.concurrent.atomic.AtomicInteger;public class Counter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();}public int getCount() {return count.get();}
}

在上面的代码示例中,AtomicInteger内部使用CAS操作来线程安全地增加计数器,这个操作不需要使用显式的同步或锁。这使得性能得到很大提高,特别是在高度竞争的场景中。
无锁编程提供了一种新的思路来解决并发问题,未来可能会有更多的编程语言和工具来支持这种方式。

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

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

相关文章

富格林:着重杜绝欺诈阻挠被骗

富格林认为&#xff0c;在现货黄金市场中&#xff0c;存在着激烈的波动和风险。尽管相应的盈利潜力也很大&#xff0c;但需要注意的是&#xff0c;我们要着重掌握经验杜绝欺诈阻挠被骗的情况发生&#xff0c;并利用行情的变化为自己扩大收益。因此&#xff0c;了解一些现货黄金…

c与c++用法区别剖析 迟早得用到

从C语言过渡到c&#xff0c;肯定要掌握这些吧 1.面向对象编程&#xff1a; C语言是过程化编程语言&#xff0c;它主要关注函数和过程。C支持面向对象编程&#xff0c;提供类、继承、多态等特性。 // C中的类和对象 class Rectangle { private:int width, height; public:Rec…

超越视觉极限:深度学习图像超分辨率算法清单【第三部分】

超越视觉极限&#xff1a;深度学习图像超分辨率算法清单【第三部分】 简介2018年 - DBPN (Deep Back-Projection Networks)2018年 - RDN (Residual Dense Network)2018年 - SRRGAN (Super-Resolution Reconstruction Generative Adversarial Network)2019年 - FSRGAN (Fast Sup…

Spring AI多模态接口开发

文章目录 项目地址创建项目配置项目接口开发结果测试测试接口测试在线图片接口测试本地图片接口测试 项目地址 Spring AI项目开发 创建项目 打开IDEA创建一个新的spring boot项目&#xff0c;填写项目名称和位置&#xff0c;类型选择maven&#xff0c;组、工件、软件包名称可…

Ubuntu apt-get install 失败

一般方法&#xff0c;执行如下指令 sudo apt-get update# 某些系统程序才需要&#xff0c;比如make sudo apt-get install build-essentialsudo apt-get install make这一步如何能成功&#xff0c;则说明是使用方式错误&#xff0c;问题比较简单 如果继续失败&#xff0c;提示…

实现vant的年月日时分秒组件

方法&#xff1a;van-datetime-picker&#xff08;type&#xff1a;datetime&#xff09;和 van-picker结合实现。 <template><div class"datetimesec-picker"><van-datetime-pickerref"timePickerRef"type"datetime" //年月日时…

【系统架构师】-案例篇(十一)质量属性、瘦客户端与Web系统架构

1、架构评估中的质量属性 ① 性能&#xff0c;是指系统的响应能力&#xff0c;即要经过多长时间才能对某个事件做出响应&#xff0c;或者在某段时间内系统所能处理的事件的个数。 ② 可靠性&#xff0c;是软件系统在应用或系统错误面前&#xff0c;在意外或错误使用的情况下维…

Android 11 输入系统之InputDispatcher和应用窗口建立联系

InputDispatcher把输入事件传给应用之前&#xff0c;需要和应用窗口建立联系&#xff0c;了解了这个过程&#xff0c;就清楚了APP进程和InputDispatcher线程也就是SystemServer进程之间是如何传输数据了 我们向窗口addView的时候&#xff0c;都会调用到ViewRootImpl的setView方…

Canvas简历编辑器-我的剪贴板里究竟有什么数据

Canvas简历编辑器-我的剪贴板里究竟有什么数据 在这里我们先来聊聊我们究竟应该如何操作剪贴板&#xff0c;也就是我们在浏览器的复制粘贴事件&#xff0c;并且在此基础上聊聊我们在Canvas图形编辑器中应该如何控制焦点以及如何实现复制粘贴行为。 在线编辑: https://windrun…

Docker 部署 Nginx 实现一个极简的 负载均衡

背景: Nginx是异步框架的网页服务器&#xff0c;其常用作反向代理(负载均衡器)。在一般的小项目中, 服务器不多, 如果不考虑使用服务注册与发现, 使用Nginx 可以容易实现负载均衡。 在特此写一个快速入门 Nginx 的技术贴, 使用 Docker 部署 Nginx, 实现一个极简的加权轮询负载均…

现在的原创内容博客 SEO 最好就选谷歌和必应!

当我们在国内讨论搜索引擎优化的时候&#xff0c;我们经常讨论的是百度 SEO&#xff0c;很少提及 Bing 搜索与 Google 搜索&#xff0c;但随着跨境电商的崛起&#xff0c;在国内做外贸 SEO 的小伙伴越来越多&#xff0c;有效的了解 Bing 搜索与 Google 搜索的优化规则是很有必要…

2024年武侯区建设企业科技创新平台申报范围条件、奖励标准和材料

一、申报对象 支持企业围绕数字健康、消费电子、新型材料等重点领域&#xff0c;布局建设一批重点实验室、创新中心、企业技术中心等高端研发平台&#xff0c;着力突破产业关键技术。实施产业链人才开源计划&#xff0c;支持链主企业为上下游关联配套企业提供技术与人才支持、…

(第17天)栈与队列理论基础

目录 栈栈的逻辑结构基于逻辑结构的特性 栈的底层实现 队列队列的逻辑结构基于逻辑结构的特性 队列的底层实现 总结 栈 栈的逻辑结构 栈是一种先入后出的结构。 基于逻辑结构的特性 栈中的元素必须遵循先入后出的规则&#xff0c;因此栈提供pop()、push()接口进行对元素的操作…

开源aodh学习小结

1 介绍 aodh是openstack监控服务&#xff08;Telemetry&#xff09;下的一个模块&#xff0c;telemetry下还有一个模块ceilometer OpenStack Docs: 2024.1 Administrator Guides Get Started on the Open Source Cloud Platform - OpenStack Telemetry - OpenStack 1.1 代码仓…

softmax函数与交叉熵损失详解

文章目录 一、softmax函数1.1 引入指数形式的优点1.2 引入指数形式的缺点 二、交叉熵损失函数2.1 交叉熵损失函数2.2 softmax与交叉熵损失 参考资料 一、softmax函数 softmax用于多分类过程中&#xff0c;它将多个神经元的输出&#xff0c;映射到&#xff08;0,1&#xff09;区…

【C++ 内存管理】深拷贝和浅拷贝你了解吗?

文章目录 1.深拷贝2.浅拷贝3.深拷贝和浅拷贝 1.深拷贝 &#x1f34e; 深拷⻉: 是对对象的完全独⽴复制&#xff0c;包括对象内部动态分配的资源。在深拷⻉中&#xff0c;不仅复制对象的值&#xff0c;还会复制对象所指向的堆上的数据。 特点&#xff1a; &#x1f427;① 复制对…

蓝桥杯-移动距离(最简单的写法)

X星球居民小区的楼房全是一样的&#xff0c;并且按矩阵样式排列。 其楼房的编号为 1,2,3…当排满一行时&#xff0c;从下一行相邻的楼往反方向排号。 比如&#xff1a;当小区排号宽度为 6 时&#xff0c;开始情形如下&#xff1a; 1 2 3 4 5 6 12 11 10 9 8 7 13 14 15 … 我…

程序设计语言理论中的范畴论及其简单应用

程序设计语言理论中的范畴论及其简单应用 范畴论是一个深奥的数学分支&#xff0c;近年来在程序设计语言理论中得到了广泛的应用。本文将简要介绍范畴论的基本概念&#xff0c;并通过简单示例来说明其在程序设计中的应用。 范畴论的基本概念 范畴&#xff08;Category&#…

Vue3:数据交互axios

回调函数 > 回调函数: 一些特殊的函数,表示未来才会执行的一些功能,后续代码不会等待该函数执行完毕就开始执行了 1. Promise 1.1 简介 > 前端中的异步编程技术&#xff0c;类似Java中的多线程线程结果回调&#xff01; * Promise 是异步编程的一种解决方案&#xff0c…

记录一下 log4j的漏洞

目录 背景 bug的产生 bug复现 JNDI 网络安全学习路线 &#xff08;2024最新整理&#xff09; 学习资料的推荐 1.视频教程 2.SRC技术文档&PDF书籍 3.大厂面试题 特别声明&#xff1a; 背景 log4j这次的bug&#xff0c;我相信大家都已经知道了&#xff0c;仅以…