Java并发编程:JDK同步容器的弊端及有效替代策略

2024-05-02 09:04

本文主要是介绍Java并发编程:JDK同步容器的弊端及有效替代策略,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 同步容器的常见问题概览

在使用Java编程时,我们经常会遇到需要在多线程环境下共享和操作数据集合的情况。为了处理这些情况,JDK提供了一系列的同步容器,例如Vector和Collections.synchronizedList。尽管这些同步容器为线程安全提供了一定程度上的保证,但在实际使用中,它们隐藏了许多陷阱和细节问题,尤其是当它们被不正确地使用时。
在仔细探讨这些问题之前,我们需要明白在多线程操作中,线程安全是指在多个线程访问数据时,可以保证数据的一致性和完整性。然而,即使是所谓的“线程安全”的同步容器也无法全面保证这一点。在接下来的章节中,我将逐一分析这些问题,并提供实际的代码示例说明问题并提出解决方法。

2. 坑一:竞态条件与同步容器

2.1 竞态条件说明

竞态条件是并发编程中一个常见的问题,它发生在当两个或更多的线程访问共享资源,并且至少有一个线程为了更改资源内容而进行写操作。如果没有适当的同步机制来控制这些线程的执行顺序,就会引发竞态条件,导致不可预知的结果和数据损坏。

2.2 同步容器中的竞态条件案例

举个简单的例子,让我们想象一个包含余额的账户对象,以及多个线程试图同时更新该账户余额。即便我们使用了Vector这样的同步容器来存储账户余额,仍然可能会遇到问题。

import java.util.Vector;public class AccountManager {private Vector<Double> accountBalances = new Vector<>();// ...public synchronized void updateAccountBalance(int accountIndex, double newBalance) {if (accountIndex < accountBalances.size()) {double currentBalance = accountBalances.get(accountIndex);// 模拟耗时操作simulateTimeConsumingOperation();accountBalances.set(accountIndex, currentBalance + newBalance);}}private void simulateTimeConsumingOperation() {// 模拟耗时操作,比如复杂的计算或IO操作try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}// ...
}

在上面的代码中,即使updateAccountBalance方法是同步的,但如果在耗时操作的间隙其他线程篡改了数据,我们依然会遇到竞态条件。

2.3 解决策略和代码示例

为了解决这个问题,我们可以引入更紧凑的锁,比如使用ReentrantLock,或者更彻底地使用Atomic类进行操作。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Vector;public class AccountManager {private Vector<AtomicLong> accountBalances = new Vector<>();private final Lock updateLock = new ReentrantLock();// ...public void updateAccountBalance(int accountIndex, double newBalance) {updateLock.lock();try {if (accountIndex < accountBalances.size()) {AtomicLong balance = accountBalances.get(accountIndex);// 是一个原子操作,无需模拟耗时操作balance.addAndGet((long) newBalance);}} finally {updateLock.unlock();}}// ...
}

在这个改进的例子中,我们通过使用ReentrantLock来确保在更新余额时不会被其他线程中断。同时使用AtomicLong保证了余额更新操作的原子性。这样不仅解决了竞态条件的问题,也提高了系统的执行效率。

3. 坑二:使用迭代器遍历容器时的问题

3.1 迭代器的弱一致性问题

在多线程环境中,使用迭代器遍历同步容器时,一个常见的问题是迭代器的弱一致性。这意味着迭代器可能无法反映出在遍历过程中容器的实时状态,尤其是当其他线程正在并发修改容器时。例如,其他线程可能已经添加或移除了元素,而迭代器却还在遍历旧的元素视图。

3.2 代码示例:迭代时的错误用法

下面展示了使用迭代器在同步容器Vector上进行遍历的错误方式。

import java.util.Iterator;
import java.util.Vector;public class ContainerTraversal {public static void main(String[] args) {Vector<Integer> numbers = new Vector<>();numbers.add(1);numbers.add(2);numbers.add(3);Iterator<Integer> iterator = numbers.iterator();while (iterator.hasNext()) {Integer number = iterator.next();// 如果另一个线程在这里修改了numbers,可能会导致不一致的现象doSomething(number);}}private static void doSomething(Integer number) {// 处理number}
}

如果在doSomething(number)方法执行期间,另一个线程更改了numbers容器的内容,那么会出现诸如ConcurrentModificationException之类的异常。

3.3 正确的迭代策略和代码示例

正确的做法是在遍历期间手动同步容器,或者使用并发容器来代替。

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;public class ContainerTraversal {public static void main(String[] args) {List<Integer> numbers = Collections.synchronizedList(new ArrayList<>());numbers.add(1);numbers.add(2);numbers.add(3);synchronized (numbers) {Iterator<Integer> iterator = numbers.iterator();while (iterator.hasNext()) {Integer number = iterator.next();doSomething(number);}}}private static void doSomething(Integer number) {// 处理number}
}

在这个例子中,我们首先使用了Collections.synchronizedList创建了一个同步的列表,并在遍历过程中对整个列表加锁,以避免在迭代过程中修改列表内容。

4. 并发容器作为替代方案

4.1 并发容器的简介

并发容器是专为多线程环境设计的数据结构,它们能够处理并发访问和修改的复杂性,从而提供比同步容器更高的线程安全性和性能。Java的java.util.concurrent包提供了多种并发容器,例如ConcurrentHashMap、CopyOnWriteArrayList等。

4.2 如何使用并发容器避免同步容器的坑

并发容器通过分段锁(Segmentation Lock),只在必要的时候进行加锁,这减少了锁竞争,从而提高了性能。例如,ConcurrentHashMap在内部使用了一个段数组来允许多个读取和写入操作并发进行,只要它们不是发生在同一个段上。

4.3 并发容器的使用示例

以下是使用ConcurrentHashMap的一个示例:

import java.util.concurrent.ConcurrentHashMap;public class ConcurrentContainerExample {public static void main(String[] args) {ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();// 使用多线程安全地更新mapmap.put("key1", "value1");map.put("key2", "value2");// 使用并发迭代器安全地遍历map.forEach((key, value) -> doSomething(key, value));}private static void doSomething(String key, String value) {// 处理键值对}
}

在这个例子中,ConcurrentHashMap确保了多个线程可以安全地同时读取和修改map,而无需担心竞态条件或迭代时的一致性问题。

5. 实战案例:优化旧系统中的同步容器

5.1 旧系统常见同步容器使用错误

在很多遗留系统中,由于历史原因,开发者可能使用了同步容器来保证数据安全。然而,这往往会导致性能瓶颈,尤其是在高并发情况下。

步骤和策略

当我们需要优化这些系统时,首先应该识别出那些在多线程环境下使用的同步容器,并评估是否有并发容器可以作为更好的替代品。接着,通过性能测试来确保并发容器提供了更好的性能同时不牺牲线程安全性。

实战改造代码示例

我们可以将使用Vector或Hashtable的代码改造成使用CopyOnWriteArrayList或ConcurrentHashMap。

import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;public class SystemOptimization {// 旧系统中可能使用的Vectorprivate Vector<Integer> oldVector = new Vector<>();// 新系统中使用的并发容器private CopyOnWriteArrayList<Integer> newConcurrentList = new CopyOnWriteArrayList<>();public void optimizeSystem() {// 用CopyOnWriteArrayList替换VectornewConcurrentList.addAll(oldVector);}// 其他的优化策略和代码...
}

在这个代码示例中,我们首先将oldVector中的内容复制到newConcurrentList,这是一个线程安全的并发容器,之后就可以安全地进行高并发操作了。

这篇关于Java并发编程:JDK同步容器的弊端及有效替代策略的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/953893

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory