如何使用Java实现有效的并发处理?一文带你渗透!

2023-11-21 11:30:17 浏览数 (1)


theme: healer-readable

highlight: a11y-dark


哈喽,各位小伙伴们,你们好呀,我是喵手。

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

  随着互联网的蓬勃发展,现代软件系统对于并发性能的要求越来越高,如何学习和掌握并发编程技术成为了Java开发人员必备的技能之一。本文将介绍Java并发编程的相关概念、原理和实践技巧。

摘要

  本文旨在探讨Java并发编程的基本原理和应用场景。通过对Java并发包的源代码解析、应用场景案例的介绍以及优缺点的分析,帮助开发者更好地理解和掌握Java并发编程的相关知识。

Java之并发处理

简介

  Java是一门跨平台的编程语言,具有强大的面向对象特性和丰富的类库。Java并发编程是Java语言中的一个重要方向,主要涉及多线程、锁、原子操作、线程池等概念和技术,是Java程序员必须掌握的技能之一。

  Java并发编程的优势在于其良好的跨平台性、可靠性和高效性。Java提供了丰富的并发编程类库,包括java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks等,可以帮助开发者轻松实现高性能、高并发的程序。

源代码解析

  Java并发包的源代码解析是理解Java并发编程的关键之一。Java并发包中包含了很多有用的工具类和接口,如ConcurrentHashMap、CopyOnWriteArrayList、Semaphore等,本文将以ConcurrentHashMap为例,介绍其实现原理和使用方法。

  ConcurrentHashMap是一个线程安全的哈希表,它支持高并发的读和写操作,并且不需要加锁就可以实现高效的并发。

  ConcurrentHashMap的实现基于分段锁的思想,它将一个大的哈希表分成多个小的哈希表,每个小的哈希表都有自己的锁,读写操作只锁住对应的小哈希表,这样就降低了整个哈希表的锁竞争,提高了并发性能。

下面是ConcurrentHashMap的核心源码分析:

  1. ConcurrentHashMap的构造方法

ConcurrentHashMap有多个构造方法,其中最常用的是以下两个:

代码语言:java复制
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
          sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
          c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segment table
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment<?,?>[cap];
    for (int i = 0; i < ss.length;   i)
        ss[i] = new Segment<K,V>(loadFactor);
    this.segments = ss;
}

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}

  这两个构造方法都会创建多个Segment对象,每个Segment对象都代表了哈希表的一个小的分段。

  其中第一个构造方法还需要传入一个concurrencyLevel参数,用来指定分段的数量。如果传入的数量大于MAX_SEGMENTS,则会使用MAX_SEGMENTS。

  ConcurrentHashMap会根据concurrencyLevel计算出小分段的数量和大小,并创建对应数量的Segment对象。

  1. Segment的结构

  每个Segment对象内部都维护了一个哈希表,这个哈希表的实现和普通的哈希表类似,只是它的所有读写操作都需要加锁。

Segment的结构如下:

代码语言:java复制
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    transient volatile int count;
    transient int modCount;
    transient int threshold;
    transient volatile HashEntry<K,V>[] table;
    final float loadFactor;
}

  其中包括了count、modCount、threshold、table和loadFactor几个重要的成员变量。

  • count表示该Segment中的键值对数量;
  • modCount表示该Segment的结构上一次修改的次数;
  • threshold表示该Segment的扩容阈值;
  • table表示该Segment的哈希表;
  • loadFactor表示该Segment的负载因子。
  1. 数据的读写操作

  ConcurrentHashMap的put、get、remove等操作都会分成两个步骤:

  • 对应的Segment加锁;
  • 在加锁的Segment中进行数据读写操作。

  例如,ConcurrentHashMap的put方法就是首先根据给定的key计算出其对应的Segment,然后对该Segment加锁,最后在加锁的Segment中进行put操作。

put操作的核心代码如下:

代码语言:java复制
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // non-acq volatile read
         (segments, (j << SSHIFT)   SBASE)) == null) // 1st time access
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

  其中,UNSAFE是Java中的一个类,可以直接操作内存;segmentShift和segmentMask是用来计算哈希值对应的Segment编号的。ensureSegment方法会创建新的Segment对象。

  ConcurrentHashMap的get和remove操作的实现也类似,都需要先锁定对应的Segment,然后在锁定的Segment中进行操作。

  1. 数据迭代

  ConcurrentHashMap的迭代操作会比较复杂,因为在迭代期间可能会有新的数据被添加或删除。

  为了解决这个问题,ConcurrentHashMap采用了两种方法:

  • 每个Segment内部维护了一个modCount计数器,每次在Segment中进行数据修改时,都会增加modCount的值。在进行迭代操作时,记录下当前的modCount值,如果在迭代过程中发现modCount的值已经被修改过了,则需要重新开始迭代。
  • ConcurrentHashMap使用了分段的方式对哈希表进行管理,因此在进行迭代操作时,只需要对每个Segment进行迭代即可。由于每个Segment的操作是互相独立的,因此不会影响到其他Segment的迭代操作。

  ConcurrentHashMap的迭代操作有两种方式,一种是迭代器方式,另一种是并发流式处理方式。它们的实现方式都较为复杂,需要涉及到Segment的加锁和解锁、modCount的检查等操作。具体实现细节可以参考ConcurrentHashMap的源码。

  总之,ConcurrentHashMap的核心思想是分段锁,通过将一个大的哈希表分成多个小的哈希表,每个小的哈希表都有自己的锁,从而避免了整个哈希表的锁竞争,提高了并发性能。同时,ConcurrentHashMap还采用了一些特殊的策略来保证数据在迭代过程中的一致性。

  如下是部分源码截图:

在这里插入图片描述在这里插入图片描述

ConcurrentHashMap的实现原理

  ConcurrentHashMap是Java并发包中的一个线程安全的HashMap实现,其实现原理主要基于分段锁和volatile关键字。ConcurrentHashMap将一个大的HashMap分成多个小的HashMap,每个小的HashMap都有自己的锁,不同的线程可以同时操作不同的小的HashMap,从而提高了并发访问的效率。

  ConcurrentHashMap还使用了volatile关键字来保证对于同一个小的HashMap的操作是可见的,这样可以避免线程之间的数据不一致问题。

ConcurrentHashMap的使用方法

  ConcurrentHashMap的使用方法和HashMap类似,可以使用put、get、remove等方法。不同的是ConcurrentHashMap是线程安全的,可以保证多线程访问时数据的一致性和正确性。

代码语言:java复制
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.get("key");
map.remove("key");

应用场景案例

  Java并发编程的应用场景非常广泛,例如多线程下载、并行计算、高效数据结构等。本文介绍一个简单的应用场景——多线程统计单词出现次数。

  假设我们有一个非常大的文本文件,我们需要统计其中每个单词出现的次数。普通的方法是将文本文件读入内存,然后使用HashMap或者TreeMap等集合来统计词频。但是如果文本文件非常大,内存可能会不够用,或者读取文件的速度非常慢。这时候我们可以使用多线程来提高程序的效率。

  具体实现方法是将文本文件分成多个小的文件块,多个线程同时读取不同的文件块,并统计其中每个单词的出现次数。最后将所有线程统计的结果进行汇总即可。

优缺点分析

Java并发编程具有以下优点:

  • 提高程序的效率和性能,特别是在多核CPU的情况下。
  • 增强程序的可伸缩性,可以更好地满足不同规模的应用需求。
  • 提高程序的质量和可靠性,通过并发编程可以发现更多的程序错误和性能瓶颈。

Java并发编程也存在以下缺点:

  • 并发编程的复杂度比较高,需要开发人员具备专业的技能和经验。
  • 并发编程容易引发死锁、竞争和状态不一致等问题,需要开发人员进行仔细的设计和测试。
  • 并发编程对于CPU和内存的消耗较大,需要考虑好系统资源的利用和管理。

类代码方法介绍

  作为Java并发编程的核心工具类之一,ConcurrentHashMap提供了很多有用的方法和接口。下面简要介绍一些常用的方法:

  • put(K key, V value):将指定的值与指定的键相关联。
  • get(Object key):返回指定键所映射的值。
  • remove(Object key):从该映射中移除指定键的映射关系。
  • clear():从该映射中移除所有映射关系。
  • keySet():返回此映射中包含的键的Set集合。

测试用例

测试代码演示

  为了演示ConcurrentHashMap的使用方法,我们可以编写一个简单的测试用例。具体实现方法是创建一个ConcurrentHashMap对象,然后使用put、get、remove等方法来操作该对象,并通过JUnit测试来验证其正确性和性能。

代码语言:java复制
package com.example.javase.se.classes.synchronous;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author ms
 * @Date 2023-11-05 21:35
 */
public class ConcurrentHashMapMain {

    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // test put and get
        map.put("key1", 1);
        map.put("key2", 2);
        map.put("key3", 3);
        System.out.println(map.get("key1")); // expected output: 1
        System.out.println(map.get("key2")); // expected output: 2
        System.out.println(map.get("key3")); // expected output: 3

        // test remove
        map.put("key1", 1);
        map.put("key2", 2);
        map.remove("key1");
        System.out.println(map.get("key1")); // expected output: null
    }
}

测试结果

  根据如上测试用例,本地测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加更多的测试数据或测试方法,进行熟练学习以此加深理解。

在这里插入图片描述在这里插入图片描述

测试代码分析

  根据如上测试用例,在此我给大家进行深入详细的解读一下测试代码,以便于更多的同学能够理解并加深印象。

  如上测试用例代码演示了如何使用Java中的ConcurrentHashMap类来进行同步操作。首先,我们导入了Java的ConcurrentHashMap类。然后,在main方法中,我们创建了一个ConcurrentHashMap实例,并使用put方法向其中添加了三个键值对。接着,我们使用get方法获取了这三个键的对应值,并将其打印出来。随后,我们又重新向ConcurrentHashMap中添加了两个键值对,然后使用remove方法删除了一个键值对。最后,我们再次使用get方法获取了这个被删除的键的对应值,预计输出为null。

  ConcurrentHashMap是多线程安全的,所以在多线程环境下可以安全地访问和修改它的内容。需要注意的是,在删除键值对时,remove方法会返回对应键的值,如果键不存在,则返回null。

全文小结

  本文介绍了Java并发编程的基本概念、原理和实践技巧。通过对Java并发包的源代码解析、应用场景案例的介绍以及优缺点的分析,帮助开发者更好地理解和掌握Java并发编程的相关知识。同时,本文还简要介绍了ConcurrentHashMap的使用方法和常用方法,以及如何编写测试用例来验证其正确性和性能。

总结

  Java并发编程是Java开发人员必备的技能之一,本文详细介绍了Java并发编程的相关概念、原理和实践技巧,对于开发者掌握Java并发编程技术具有重要的参考价值。同时,本文也提供了ConcurrentHashMap的源代码解析、应用场景案例、优缺点分析、常用方法介绍和测试用例等内容,可以帮助开发者更好地理解和应用Java并发编程技术。

... ...

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

... ...

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。

⭐️若有疑问,就请评论留言告诉我叭。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞