Java基础:为什么hashmap是线程不安全的?

原因

HashMap 是线程不安全的主要原因是它的内部结构和操作不是线程安全的。下面是一些导致 HashMap 线程不安全的因素:

  1. 非同步操作:HashMap 的操作不是线程同步的,也就是说,在多线程环境下同时对 HashMap 进行读写操作可能会导致数据不一致的问题。

  2. 非原子操作:HashMap 的操作不是原子性的,例如 put() 方法涉及到了多个步骤,包括计算哈希值、查找或插入元素等。如果多个线程同时执行这些操作,就有可能导致数据不一致的情况。

  3. 容量扩容:HashMap 在扩容时,需要重新计算元素的哈希值并重新分配存储位置,这个过程涉及到对原数组进行复制和重新插入元素的操作。如果在扩容期间有其他线程对 HashMap 进行并发修改,就可能导致数据丢失或出现异常。

综上所述,由于 HashMap 的非同步和非原子性操作,以及容量扩容的复制和插入过程,使得它在多线程环境下容易出现线程安全问题。如果多个线程同时对
HashMap 进行读写操作,可能会导致数据不一致、数据丢失或出现异常的情况。

为了在多线程环境下安全地使用 HashMap,可以采取以下几种方式:

  1. 使用同步机制:可以使用线程安全的 Map 实现,如 ConcurrentHashMap,或者通过在访问 HashMap 时使用 synchronized 或其他锁机制来确保同一时间只有一个线程能够修改 HashMap。

  2. 使用并发容器:可以使用线程安全的并发容器,如 ConcurrentMap 或 CopyOnWriteMap,它们提供了并发访问的能力,适用于读多写少的场景。

  3. 使用线程封闭:可以将 HashMap 封闭在单个线程中,通过使用 ThreadLocal 或将 HashMap 作为局部变量在每个线程中进行操作,从而避免多线程访问导致的线程安全问题。

总之,如果需要在多线程环境中使用 Map,应该考虑使用线程安全的 Map 实现或采取适当的同步机制来确保线程安全性。

举例佐证

假设有两个线程同时对一个 HashMap 进行读写操作,下面是一个简单的示例来说明 HashMap 的线程不安全性:

import java.util.HashMap;

public class HashMapExample {
    private static HashMap<Integer, String> map = new HashMap<>();

    public static void main(String[] args) {
        // 创建并启动两个线程
        Thread thread1 = new Thread(new WriteTask());
        Thread thread2 = new Thread(new ReadTask());
        thread1.start();
        thread2.start();
    }

    static class WriteTask implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                map.put(i, "Value " + i);
                System.out.println("Thread 1: Added " + i);
            }
        }
    }

    static class ReadTask implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                if (map.containsKey(i)) {
                    String value = map.get(i);
                    System.out.println("Thread 2: Read " + value);
                }
            }
        }
    }
}

在上述示例中,WriteTask 线程通过循环向 HashMap 中添加元素,而 ReadTask 线程通过循环从 HashMap 中读取元素。由于
HashMap 不是线程安全的,当两个线程同时进行读写操作时,就可能出现数据不一致的情况。

运行示例代码,你会发现在控制台输出中可能会出现如下情况:

Thread 2: Read Value 0
Thread 2: Read Value 2
Thread 1: Added 1
Thread 2: Read Value 1

在这个例子中,Thread 2 在读取到某个键的值之后,Thread 1 可能会同时修改这个键的值,导致 Thread 2 读取到的值与期望不一致。

因此,这个例子展示了 HashMap 在多线程环境下的线程不安全性,这也是为什么在并发场景中应该使用线程安全的 Map
实现或采取适当的同步机制来确保线程安全性。

想想看,上述代码有问题吗?

使用CountDownLatch解决

上述示例代码存在问题,可能导致 Thread 2 没有输出任何内容。原因是在 Thread 1 启动后,可能会在 Thread 2
开始执行之前完成所有的写操作,因此 Thread 2 没有机会读取到任何值。

为了解决这个问题,可以使用 CountDownLatch 来同步两个线程的执行,确保 Thread 2Thread 1
完成写操作后再开始读取。以下是修正后的示例代码:

import java.util.HashMap;
import java.util.concurrent.CountDownLatch;

public class HashMapExample {
    private static HashMap<Integer, String> map = new HashMap<>();
    private static CountDownLatch latch = new CountDownLatch(1);

    public static void main(String[] args) {
        // 创建并启动两个线程
        Thread thread1 = new Thread(new WriteTask());
        Thread thread2 = new Thread(new ReadTask());
        thread1.start();
        thread2.start();
    }

    static class WriteTask implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 1000; i++) {
                    map.put(i, "Value " + i);
                    System.out.println("Thread 1: Added " + i);
                }
            } finally {
                latch.countDown();
            }
        }
    }

    static class ReadTask implements Runnable {
        @Override
        public void run() {
            try {
                latch.await();
                for (int i = 0; i < 1000; i++) {
                    if (map.containsKey(i)) {
                        String value = map.get(i);
                        System.out.println("Thread 2: Read " + value);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

修正后的代码使用 CountDownLatchThread 1 完成写操作后释放等待,使得 Thread 2
可以开始读取操作。这样就可以确保在读取之前所有的写操作都已经完成,从而避免了数据不一致的问题。

现在运行示例代码,你会看到 Thread 2 输出了与 Thread 1 写入的相应值,确保了线程安全性。

请注意,这只是一个简单的示例来说明 HashMap 的线程不安全性,并非使用 HashMap 的推荐方式。在实际应用中,应该使用线程安全的 Map 实现,如
ConcurrentHashMap,来保证线程安全性。

使用join方法

使用join等待两个线程运行结束,依旧存在问题:

import java.util.HashMap;

public class HashMapExample {
    private static HashMap<Integer, String> map = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 创建并启动两个线程
        Thread thread1 = new Thread(new WriteTask());
        Thread thread2 = new Thread(new ReadTask());
        thread1.start();
        thread2.start();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();

        // 打印最终的Map内容
        System.out.println("Final Map:");
        for (Integer key : map.keySet()) {
            System.out.println("Key: " + key + ", Value: " + map.get(key));
        }
    }

    static class WriteTask implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                map.put(i, "Value " + i);
                System.out.println("Thread 1: Added " + i);
            }
        }
    }

    static class ReadTask implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                if (map.containsKey(i)) {
                    String value = map.get(i);
                    System.out.println("Thread 2: Read " + value);
                }
            }
        }
    }
}

在修正后的代码中,我们通过调用 Thread.join() 方法,使得主线程等待 Thread 1Thread 2
完成执行后再继续执行。然后,我们打印出最终的 HashMap 内容以验证线程安全性。

尽管代码中没有使用同步机制或其他线程安全的容器来确保线程安全性,但由于此示例中的读写操作相对简单,可能会在某些情况下产生正确的输出。但是,这并不代表
HashMap 是线程安全的。在更复杂的并发场景中,仍然存在竞态条件和数据不一致的风险。

为了在多线程环境中安全地使用 Map,推荐使用线程安全的 Map 实现,如 ConcurrentHashMap,或者采用适当的同步机制来确保线程安全性。

学习计划安排


我一共划分了六个阶段,但并不是说你得学完全部才能上手工作,对于一些初级岗位,学到第三四个阶段就足矣~

这里我整合并且整理成了一份【282G】的网络安全从零基础入门到进阶资料包,需要的小伙伴可以扫描下方优快云官方合作二维码免费领取哦,无偿分享!!!

如果你对网络安全入门感兴趣,那么你需要的话可以

点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

①网络安全学习路线
②上百份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥HW护网行动经验总结
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析

学习网络安全技术的方法无非三种:

第一种是报网络安全专业,现在叫网络空间安全专业,主要专业课程:程序设计、计算机组成原理原理、数据结构、操作系统原理、数据库系统、 计算机网络、人工智能、自然语言处理、社会计算、网络安全法律法规、网络安全、内容安全、数字取证、机器学习,多媒体技术,信息检索、舆情分析等。

第二种是自学,就是在网上找资源、找教程,或者是想办法认识一-些大佬,抱紧大腿,不过这种方法很耗时间,而且学习没有规划,可能很长一段时间感觉自己没有进步,容易劝退。

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

第三种就是去找培训。

image.png

接下来,我会教你零基础入门快速入门上手网络安全。

网络安全入门到底是先学编程还是先学计算机基础?这是一个争议比较大的问题,有的人会建议先学编程,而有的人会建议先学计算机基础,其实这都是要学的。而且这些对学习网络安全来说非常重要。但是对于完全零基础的人来说又或者急于转行的人来说,学习编程或者计算机基础对他们来说都有一定的难度,并且花费时间太长。

第一阶段:基础准备 4周~6周

这个阶段是所有准备进入安全行业必学的部分,俗话说:基础不劳,地动山摇
image.png

第二阶段:web渗透

学习基础 时间:1周 ~ 2周:

① 了解基本概念:(SQL注入、XSS、上传、CSRF、一句话木马、等)为之后的WEB渗透测试打下基础。
② 查看一些论坛的一些Web渗透,学一学案例的思路,每一个站点都不一样,所以思路是主要的。
③ 学会提问的艺术,如果遇到不懂得要善于提问。
image.png

配置渗透环境 时间:3周 ~ 4周:

① 了解渗透测试常用的工具,例如(AWVS、SQLMAP、NMAP、BURP、中国菜刀等)。
② 下载这些工具无后门版本并且安装到计算机上。
③ 了解这些工具的使用场景,懂得基本的使用,推荐在Google上查找。

渗透实战操作 时间:约6周:

① 在网上搜索渗透实战案例,深入了解SQL注入、文件上传、解析漏洞等在实战中的使用。
② 自己搭建漏洞环境测试,推荐DWVA,SQLi-labs,Upload-labs,bWAPP。
③ 懂得渗透测试的阶段,每一个阶段需要做那些动作:例如PTES渗透测试执行标准。
④ 深入研究手工SQL注入,寻找绕过waf的方法,制作自己的脚本。
⑤ 研究文件上传的原理,如何进行截断、双重后缀欺骗(IIS、PHP)、解析漏洞利用(IIS、Nignix、Apache)等,参照:上传攻击框架。
⑥ 了解XSS形成原理和种类,在DWVA中进行实践,使用一个含有XSS漏洞的cms,安装安全狗等进行测试。
⑦ 了解一句话木马,并尝试编写过狗一句话。
⑧ 研究在Windows和Linux下的提升权限,Google关键词:提权
image.png
以上就是入门阶段

第三阶段:进阶

已经入门并且找到工作之后又该怎么进阶?详情看下图
image.png

给新手小白的入门建议:
新手入门学习最好还是从视频入手进行学习,视频的浅显易懂相比起晦涩的文字而言更容易吸收,这里我给大家准备了一套网络安全从入门到精通的视频学习资料包免费领取哦!

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值