首页 > 系统相关 >大话Java系列-并发场景下HashMap的环形链表问题,jmap检查内存状态,jstack查看线程状态,线程安全

大话Java系列-并发场景下HashMap的环形链表问题,jmap检查内存状态,jstack查看线程状态,线程安全

时间:2024-10-28 15:21:23浏览次数:7  
标签:Map java HashMap Thread 链表 线程

文章目录

童话故事

故事开始

在一个遥远的数字森林里,有一个神奇的地方叫做Java王国。这个王国中有一片美丽的花园,花园里生长着一种特别的花朵——HashMap花。这些花朵非常独特,因为它们能够存储各种珍贵的信息,而且每朵花都有自己的秘密路径,以便快速找到需要的信息。

有一天,四位勇敢的小精灵:ThreadA、ThreadB、ThreadC和ThreadD,来到了这片花园,他们的任务是向HashMap花添加新的信息。每个小精灵都带了一篮子数据,准备将它们种入对应的花朵之中。

  • ThreadA 拿出了他的第一份数据,正准备插入到HashMap花中的某个位置。
  • 同时,ThreadB 也带着他的数据来到同一个地方,发现那个位置已经被占用了,于是他决定在链表的下一个节点处寻找空位。
  • 就在这时,ThreadC 也来到了这个地方,并且他也想在这个链表中插入一个新节点。
  • ThreadD 则是在链表的另一个部分工作,但偶尔也会回到这里查看情况。

由于没有协调好彼此的动作,这四位小精灵开始了混乱的操作:

  1. ThreadA 插入了一个新节点,但在它还没有完成链接之前,ThreadB 已经移动到了下一个节点。
  2. ThreadB 开始插入它的节点,但它还没来得及更新指针,ThreadC 就已经完成了插入并尝试更新链表结构。
  3. ThreadC 的操作同样被打断了,因为它还没有设置好自己的指针,ThreadD 就试图访问链表。
  4. 这样的循环继续发生,最终导致了链表出现了环形结构,即每个节点都指向了前面的某个节点,形成了一个无限循环。

发现问题

随着时间的推移,其他想要访问或修改HashMap花的小精灵们遇到了麻烦。他们发现当尝试遍历链表时,总是无法到达终点,而是不停地绕圈圈。这种情况不仅浪费了大量时间,还使得整个花园变得混乱不堪。

这时,一位智慧的老者——JVM大师出现了。他使用了一种特殊的工具jmap -histo来检查内存状态,并通过jstack查看当前线程的状态。很快,他就发现了问题所在:原来是因为多线程并发操作造成的链表环形结构。

解决问题

为了解决这个问题,JVM大师提出了以下解决方案:

  1. 使用ConcurrentHashMap:这是一种专为高并发场景设计的数据结构,内部实现了更复杂的锁机制,可以有效避免多线程环境下的数据竞争问题。
  2. 外部加锁:如果必须使用普通的HashMap,那么可以在操作前对整个Map加上全局锁,确保在同一时间只有一个线程能够进行写操作。
  3. 使用线程安全的方法:例如,利用Collections.synchronizedMap()方法包装现有的HashMap实例,这样每次调用其方法时都会自动加上同步锁。

小精灵们采纳了JVM大师的建议,重新组织了他们的工作流程。从此以后,无论是谁想要向HashMap花中添加数据,都能够井然有序地进行,再也没有出现过环形链表的问题。花园恢复了往日的宁静与美丽,而小精灵们也学会了更加高效和谐的工作方式。

为了模拟这个童话故事中的情景,我们可以编写一段Java代码来展示在多线程环境下使用普通HashMap时可能发生的环形链表问题。然后,我们将展示如何使用ConcurrentHashMap或外部加锁的方式来解决这个问题。

Gitee链接地址,建议收藏,后续我会对专栏进行整理,每篇文章进行校正和调整,然后统一存放在gitee仓库中

代码实现

1. 使用普通HashMap导致的环形链表问题

首先,我们创建一个简单的类来表示数据,并设置多个线程同时向HashMap中插入数据。请注意,在实际应用中,由于HashMap不是线程安全的,这样的操作可能会导致各种并发问题,包括但不限于环形链表、丢失更新等。

import java.util.HashMap;
import java.util.Map;

public class HashMapRaceCondition {

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

        // 创建多个线程并启动
        for (int i = 0; i < 4; i++) {
            int finalI = i;
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    map.put(finalI * 10000 + j, "Value" + (finalI * 10000 + j));
                }
            }).start();
        }

        // 等待所有线程完成
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Map size: " + map.size());
    }
}

这段代码中,四个线程尝试向同一个HashMap实例添加大量的键值对。在高并发情况下,HashMap内部的数据结构可能会变得不一致,甚至出现环形链表的情况。不过,由于HashMap的设计,这种特定的环形链表问题并不常见;更常见的问题是数据丢失或覆盖。

2. 使用jmap -histo检查内存状态

当程序运行时,我们可以使用jmap -histo命令来检查内存状态。假设我们的Java进程ID是12345(请替换为实际的PID):

jmap -histo 12345

这条命令会输出当前JVM中所有类的实例数量和占用空间。你可以查找与HashMap相关的条目,看看是否有异常的内存使用情况。

3. 使用jstack查看当前线程的状态

接下来,我们可以使用jstack命令来查看当前线程的状态,以了解是否存在死锁或长时间运行的线程。同样地,假设我们的Java进程ID是12345:

jstack 12345

这条命令会输出所有线程的堆栈跟踪信息。你可以检查这些信息,看看是否有线程因为竞争HashMap而处于阻塞状态或无限循环中。

4. 分析结果

jmap -histo输出示例
 num     #instances         #bytes  class name
----------------------------------------------
   1:        367890      14715600  [I
   2:        200000       4000000  java.lang.String
   3:        100000       2400000  java.util.HashMap$Node
...

从这里可以看到HashMap$Node的数量,如果数量异常高,可能意味着存在大量未清理的对象或者链表环形结构。

jstack输出示例
"Thread-4" #10 prio=5 os_prio=0 tid=0x00007f8d2c001000 nid=0x4b03 waiting on condition [0x00007f8d1e6fe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at HashMapRaceCondition.lambda$main$0(HashMapRaceCondition.java:24)
        at java.lang.Thread.run(Thread.java:748)

"Thread-3" #9 prio=5 os_prio=0 tid=0x00007f8d2c000800 nid=0x4b02 waiting on condition [0x00007f8d1e7ff000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at HashMapRaceCondition.lambda$main$0(HashMapRaceCondition.java:24)
        at java.lang.Thread.run(Thread.java:748)

...

从这里可以看到每个线程的状态,如果有线程长时间处于TIMED_WAITING或其他不正常的状态,可能表明存在并发问题。

5. 使用ConcurrentHashMap解决问题

通过分析jmap -histojstack的输出,我们发现HashMap节点数量异常高,并且有多个线程处于等待状态。这提示我们可能存在链表环形结构或其他并发问题。

解决方法可以是使用ConcurrentHashMap替代HashMap,或者使用外部加锁来确保线程安全。例如,修改上面的代码使用ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapExample {

    private static final Map<Integer, Integer> map = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 启动多个线程
        for (int i = 0; i < 5; i++) {
            int threadNum = i;
            new Thread(() -> {
                for (int j = 0; j < 1000000; j++) {
                    map.put(j, j);
                    // 有意制造一些竞争条件
                    if (j % 1000 == 0) {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, "Thread-" + threadNum).start();
        }

        // 让主线程等待一段时间,以便其他线程执行
        Thread.sleep(5000);

        // 打印最终的Map大小
        System.out.println("Final Map size: " + map.size());
    }
}

这样就能避免多线程环境下的链表环形结构和其他并发问题。

6. 使用外部加锁保证线程安全

如果必须使用普通的HashMap,可以通过外部加锁的方式确保线程安全:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SynchronizedHashMapExample {

    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<>();
        Lock lock = new ReentrantLock();

        // 创建多个线程并启动
        for (int i = 0; i < 4; i++) {
            int finalI = i;
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    lock.lock();
                    try {
                        map.put(finalI * 10000 + j, "Value" + (finalI * 10000 + j));
                    } finally {
                        lock.unlock();
                    }
                }
            }).start();
        }

        // 等待所有线程完成
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Map size: " + map.size());
    }
}

在这个例子中,我们使用了ReentrantLock来实现外部加锁,确保每次只有一个线程能够执行put操作,从而避免了并发问题。

通过这些示例代码,我们可以看到如何从理论上和实践中避免多线程环境下HashMap可能出现的问题。在实际开发中,推荐使用ConcurrentHashMap或其他线程安全的数据结构来简化并发编程的工作。

标签:Map,java,HashMap,Thread,链表,线程
From: https://blog.csdn.net/qq_41089021/article/details/143284607

相关文章

  • Python之多线程
    一、使用threading模块的Thread类1.1介绍这是Python中最基本的创建线程的方法。通过定义一个函数,然后将这个函数作为参数传递给Thread类的构造函数来创建线程。每个线程对象代表一个独立的执行线程。1.2代码示例importthreadingimporttimedefprint_numbers():......
  • 链表的学习
    介绍 每个节点都会有data数据域和指针域data数据域可以存放该节点对应的任何数据类型值比如intchar等next指针域是指向列表中下一个节点的引用指针 接下来我们需要给这些节点的data数据域赋值,分别为10,20,30现在每个节点中都有对应的值接下来我们应该将这些节点连接起......
  • python——使用线程池实现异步返回数据
    框架flask应用场景当接收到请求,但数据处理比较耗时,希望请求过来时先返回一个响应,再慢慢处理数据,处理完成后再将结果返回给另一个地址。流程:接收到请求,立即返回响应。再处理数据,处理完成后将结果响应给预先定义的URL。importtracebackimportrequestsfromconcurrent.......
  • Java 多线程面试题
    什么是线程和进程?进程(Process)定义:进程是一个正在运行的程序的实例。每个进程都有自己的内存空间、数据栈和其他用于跟踪进程执行的辅助数据。特性:        进程之间是相互独立的,拥有各自的地址空间。        进程间通信(IPC)相对复杂,通常需要使用管道、套接......
  • 142. 环形链表 II Golang实现
    #题目描述:给定一个链表的头节点head,返回链表开始入环的第一个节点。如果链表无环,则返回null。如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数pos来表示链表尾连接到链表中的位置(索引从0开始)。如......
  • java ConcurrentHashMap源码分析
    目录一、一些重要属性常量sizeCtl属性Node类TreeNode类TreeBin类ForwardingNode类二、Unsafe类方法三、构造方法无参构造方法带参构造方法四、put()方法大致分析具体分析1.第一阶段spread()方法initTable()方法2.第二阶段helpTransfer()方法3.第三阶段tr......
  • 数据结构-------------二叉树后续(单链表实现二叉树)
    1:前中后序遍历在用链表实现二叉树的时候我们要首先了解一下二叉树的遍历,让我们来看一下二叉树都有那些遍历1.1 遍历规则按照二叉树的遍历规则遍历有:前序.中序.后序。(1)前序:首先访问根节点再去访问左右子树(访问顺序为:根结点、左⼦树......
  • 散列表:为什么经常把散列表和链表放在一起使用?
    散列表:为什么经常把散列表和链表放在一起使用?在计算机科学中,散列表(哈希表)和链表是两种常见的数据结构。你可能会好奇,为什么它们经常被放在一起使用呢?让我们一起来深入探讨这个问题。一、散列表的特点散列表是一种根据关键码值(Keyvalue)而直接进行访问的数据结构。它通过......
  • 线程同步(互斥锁条件变量)
     线程同步互斥锁(互斥量)条件变量生产/消费者模型一、互斥锁C++11提供了四种互斥锁:mutex:互斥锁。timed_mutex:带超时机制的互斥锁。recursive_mutex:递归互斥锁。recursive_timed_mutex:带超时机制的递归互斥锁。包含头文件:#include<mutex>1、mutex类1)加锁lock()互斥锁......
  • Java进阶学习笔记54——HashMap、LinkedHashMap、TreeMap
    HashMap集合的底层原理:HashMap跟HashSet的底层原理是一模一样的,都是基于哈希表实现的。实际上,原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已。哈希表:1)JDK8之前,哈希表=数组+链表;2)JDK8开始,哈希表=数组+链表+红黑树;3)哈希表是......