首页 > 其他分享 >HashMap线程不安全实例(jdk1.7)

HashMap线程不安全实例(jdk1.7)

时间:2024-03-10 23:22:31浏览次数:25  
标签:map HashMap jdk1.7 next 线程 数组 new 断点

一、前言

jdk1.7中,以put()方法举例,线程不安全的一些情况:

1,初始化HashMap的桶数组的时候,一个线程初始化了桶数组并插入了第一个元素,但是另一个线程不知道初始化好了,也执行了初始化的操作,清除了前面线程已经插入的元素;

2,两个线程同时触发扩容,在翻转同个桶位上的链表时,链表形成环,类似循环依赖;

 

二、初始化桶数组的例子

1,设计思路

  将HashMap的put()方法分为两个步骤,步骤1打探桶数组是否为空,步骤2根据打探的结果(空就初始化数组,不空则使用已有的数组)插入元素。在步骤1和步骤2间打断点,详细流程如下:

a,准备一个空map;

b,创建线程0,向map中插入元素,走到断点处,发现map的桶数组为空;

c,创建线程1,向map中插入元素,走到断点处,发现map的桶数组为空;

d,选择线程1,前进走过断点,完成步骤2,初始化桶数组,并插入了一个元素;

e,选择线程0,前进走过断点,执行步骤2,虽然此时另一个线程已经初始化过桶数组了,但是线程0是不知道的,它只打探一次,不管过多久都使用自己的消息,所以接下来它会把桶数组初始化,reset,归零。最后再插入自己的元素。

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

        final Map<Integer, String> map = new HashMap<>();

        // 初始值2,线程执行完后减1
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                map.put(1, "春");
                countDownLatch.countDown();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                map.put(2, "花");
                countDownLatch.countDown();
            }
        });

        thread1.start();
        thread2.start();

        Map<Integer, String> testDebugMap = new HashMap<>();
        testDebugMap.put(3, "秋");
        System.out.println("测试debug有没有阻塞其他线程:" + testDebugMap);

        // 等两个线程内部逻辑都执行结束后,再执行下面的代码
        countDownLatch.await();
        System.out.println(map);

 2,设置断点

在map的put()方法中,在(1,设计思路)步骤1和步骤2之间设置断点;

右键断点(行号后面的红点)进行详细配置:Suspend选择Thread;Condition填入!"main".equals(Thread.currentThread().getName());

Suspend=Thread:有线程走到这儿的时候,只停住当前线程,当前线程停住的这段时间其他线程该怎么走就怎么走,除非其他线程后来也有走到这儿的了,也会停住;线程与线程之间互不影响;

Suspend=All:有线程走到这儿的时候,世界按下暂停键,所有线程停止运行,留在原地;但是解除暂停键后,其他线程即使走到这儿也不会停住了;

Condition:进一步加个条件,此处配合Suspend=Thread表示线程走到此处是否停住还要看是否满足这个条件。

3,线程0走到断点处停住

点击debug按钮,开始调试;

线程0走到断点处停住了,如下图所示

 4,线程1也走到断点处停住

线程1走到断点处也停住了,如下图所示

5,主线程走到CountDownLatch的等待处停住

主线程在countDownLatch.await()处停住了,需要等线程0和1都执行完毕了,才会执行下面的代码,如下图所示

6,断点配置的其他说明

Suspend配置成Thread后,可以选择thread0走两步,再选择thread1走几步,还能再选回thread0走几步,线程之间不受影响;

假如Suspend配置成All后,先走到此处的线程停住,同时其他线程都停在了其他地方;随便选哪个线程往下走,其他线程就会直接走到最后;就只是一个暂停键的作用,暂停用过之后,那个断点就没用了。

7,选择线程1,完成桶数组的初始化,并插入第一个元素

如下图,鼠标左键依次点击控制台的“Debugger”——>"倒三角“——>”线程1“,从而选择线程1

点击步进或者快进,让线程1走完;此时HashMap的桶数组插入了一个元素,如下图

8,选择线程0

鼠标左键依次点击控制台的“Debugger”——>"倒三角“——>”线程0“,从而选择线程0;

发现HashMap的桶数组因为线程1先前的操作存在元素了,如下图

9,线程0步进一步

点击步进按钮,执行断点处的操作,发现线程0初始化了HashMap的桶数组,即又让桶数组回到了初始化状态,全是null,如下图

 10,线程0执行剩下的代码

点击步进或者快进,让线程0走完剩下的代码;

最终结果是HashMap中只有线程0的元素,因为线程1的元素被线程0初始化桶数组的时候清除了;但是size=2,因为初始化的是桶数组,并没有初始化size,如下图

11,主线程走完

线程0和线程1走完后,主线程也被唤醒了,走完了剩下的代码;

打印结果也和上面分析一致,如下图

 12,其他情况

方法只要不是加了锁,操作共有资源的时候,容易发生线程安全问题,如下面图示的情况等

 

三、扩容翻转链表形成环的例子

1,设计思路

线程0和线程1同时触发扩容翻转,在翻转时,两个线程配合好,使链路出现环路。

2,准备工作

a,准备两个key,使它们落在同一个桶位上,且扩容后还在一个桶位上;同理,再准备三个key,使它们落在同一个桶位上,且扩容后还在一个桶位上,且和前面步骤的桶位有所区分

    public static void main(String[] args) {
        // 打印和1(key)落在同一桶位的key,且扩容后也在同一桶位
        printSameBucketKey(1);

        System.out.println("++++++++++++++++++++++++++++++++++++++++++");

        // 打印和2(key)落在同一桶位的key,且扩容后也在同一桶位
        printSameBucketKey(2);
    }

    /**
     * 打印和targetKey落在同一桶位的key,且扩容后也在同一桶位
     *
     * @param targetKey
     */
    public static void printSameBucketKey(int targetKey) {
        for (int i = 1; i < 100; i++) {
            if (targetKey % 8 == hash(i) % 8 && targetKey % 16 == hash(i) % 16) {
                System.out.println(i);
            }
        }
    }

    // 直接copy的源码,为jdk1.7中hashMap计算hash值的方法
    public static int hash(Object k) {
        int h = 0;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

所以,key为1和16时,在map容量为8时在一个桶位,且容量为16时也在一个桶位;

key为2、19、32时,在map容量为8时在一个桶位,且容量为16时也在一个桶位;

b,测试代码:

        // 初始容量为8,且元素数量达到8*0.3=2时,满足扩容的两个条件之一
        final Map<Integer, String> map = new HashMap<>(8, 0.3f);

        // 在指定桶位上,先后插入两个个元素,按照“头插法”,最后结果为:“花”->"春“->”null“
        map.put(1, "春");
        map.put(16, "花");

        // 在另一个桶位xxx上,插入一个元素,此时不会触发扩容,因为插入前该桶位为空
        map.put(2, "扩容翻转");

        // 初始值2,线程执行完后减1
        final CountDownLatch countDownLatch = new CountDownLatch(2);

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 在桶位xxx上插入元素,同时满足扩容的两个条件,即map的元素数量达到2,且插入前桶位不为空
                map.put(19, "扩容翻转1");
                countDownLatch.countDown();
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 在桶位xxx上插入元素,同时满足扩容的两个条件,即map的元素数量达到2,且插入前桶位不为空
                map.put(32, "扩容翻转2");
                countDownLatch.countDown();
            }
        });

        thread1.start();
        thread2.start();

        // 等两个线程内部逻辑都执行结束后,再执行下面的代码
        countDownLatch.await();
        System.out.println(map);

c,断点设置

设置在transfer()方法中,遍历链表的地方,如下图所示;

断点配置:Suspend=Thread;Condition=线程的名称不为main

d,链表翻转逻辑预知

简化代码并举例

public class MyNode {
    public int key;
    public String value;
    public MyNode next;

    public MyNode(int key, String value, MyNode next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public static void main(String[] args) {
        // “花” -> “春” -> null
        MyNode node = new MyNode(1, "花", new MyNode(16, "春", null));

        // 新的桶数组
        MyNode[] newTable = new MyNode[1];

        MyNode e = node;
        System.out.println("翻转前:" + e);
        while (null != e) {
            MyNode next = e.next;
            e.next = newTable[0];
            newTable[0] = e;
            e = next;
        }

        System.out.println("翻转后:" + newTable[0]);
    }

    @Override
    public String toString() {
        return "MyNode{" +
                "key=" + key +
                ", value='" + value + '\'' +
                ", next=" + next +
                '}';
    }
}

第一次while循环

执行next = e.next;

执行e.next = newTable[0];

执行newTable[0] = e;

执行e = next;

第二次while循环,执行next = e.next;

执行e.next = newTable[0];

执行newTable[0] = e;

执行e = next;

第三次while循环

while条件(e!=null)不满足,跳出while循环;

打印桶数组,和上图结果一样,桶位上的链表发生了翻转

提示:可以使用亿图软件画图,赋值的时候,只需要通过调整箭头指向来模拟。

3,点击debug按钮,开始调试程序

点击debug按钮,线程0和线程1都走到了断点处,如下图

4,选择线程0,步进一步

执行next = e.next;

5,选择线程1,完成翻转操作

选择线程1,快进或跳过断点,执行到最后;

根据“d,链表翻转逻辑预知”的经验,完成翻转后,结果如下图;并同步修改了table(map的桶数组)

6,选择线程0,步进执行翻转操作

因为线程1的操作,堆中各个对象的关系发生了改变,如下图

执行e.next = newTable[0];

无变化;

执行newTable[0] = e;

执行e = next;

第二次while循环,执行next = e.next;

执行e.next = newTable[0];

无变化

执行newTable[0] = e;

执行e = next;

第三次while循环,执行next = e.next;

执行e.next = newTable[0];

 至此,链表出现了环路。

 

四、总结

1,初始化桶数组的例子

  是一个覆盖的问题,提交svn、git代码的时候经常发生。比如放假前,领导安排了一个任务,和张三、李四说,放假后,你们在家如果谁闲了的话就去完成这个任务。张三打了一天篮球回到家,更新了下代码发现李四没提交代码,觉得他应该是有事在忙,自己就开始在本地写代码。李四也打了一天麻将回来了,也更新了下代码发现张三没提交代码,也觉得对方应该是有事在忙,自己就开始在本地写代码,李四自己写的快就提交了。张三写的慢,可能边写边看电视了,很晚才提交代码。殊不知,张三覆盖了李四的代码。造成了人力的浪费。

2,扩容翻转链表形成环的例子

 

 

 如果有一个球,后面是一个环,前面是一个钩子;有多个这样的球,互相钩连在一起。最后有一条链子,链子的最后一个球是null球。还要有一个单独的null球。再来三个人,每个人抓住其中一个球,然后按照上表中4个步骤顺序操作,有时候人要换个球抓,有时候球前面的钩子要解开然后钩到另外一个球上。

标签:map,HashMap,jdk1.7,next,线程,数组,new,断点
From: https://www.cnblogs.com/seeall/p/18063073

相关文章

  • Linux多线程-线程同步
    线程同步当多个线程同时对一个共享数据进行操作时,会导致数据竞争,下面例子展示了数据竞争的情况:1#include<pthread.h>2#include<stdio.h>3#include<stdlib.h>4#include<string.h>5#include<unistd.h>67staticintval=0;8void*threadEntry(void*......
  • Linux多线程
    线程的概念线程是指程序中的一条执行路径。在一个进程中,至少有一个线程,称为主线程,通过主线程可以派生出其他子线程。Linux系统内核只提供了轻量级进程(light-weight process)的支持,并未实现线程模型。Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。进程是......
  • Java面试必考题之线程的生命周期,结合源码,透彻讲解!
    写在开头在前面的几篇博客里,我们学习了Java的多线程,包括线程的作用、创建方式、重要性等,那么今天我们就要正式踏入线程,去学习更加深层次的知识点了。第一个需要学的就是线程的生命周期,也可以将之理解为线程的几种状态,以及互相之间的切换,这几乎是Java多线程的面试必考题,每一年都......
  • 分布式锁——JVM锁、MySQL锁解决多线程下并发争抢资源
    分布式锁——JVM锁、MySQL锁解决库存超卖问题引入库存扣案例需求背景电商项目中,用户购买商品后,会对商品的库存进行扣减。需求实现根据用户购买商品及商品数量,对商品的库存进行指定数量的扣减publicStringdeductStock(LonggoodsId,Integercount){//1.查询商品......
  • 多进程、多线程知识再整理
    #threading模块'''cpython全局解释器锁导致同时只能有一个线程执行python,利用多cpu执行cpu密集型任务使用多进程,密集io型可以使用多线程并发classthreading.Thread(group=None,target=None,name=None,args=(),kwargs={},*,daemon=NoneThread类代表一个在独立控制线......
  • 手撕Java多线程(四)线程之间的协作
    线程之间的协作当多个线程可以一起去解决某个问题时,如果某些部分必须在其他部分之前完成,那么就需要对线程进行协调。join()在线程中调用另一个线程的join()方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。对于以下代码,虽然b线程先启动,但是因为在b线程中调用了a线程的join......
  • 在Java中,HashMap中是用哪些方法来解决哈希冲突的?
    HashMap中调用hashCode()方法来计算hashCode。由于在Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产生。采用链地址法解决冲突。HashMap底层是数组+链表+红黑树(JDK1.8)来实现的,根据key的hash值查找对应的位桶。1.当前索引数组为空,则......
  • IDEA使用与多线程
    IDEA缩写和快捷键psvm全称publicstaticvoidmainsout全称publicstaticvoidmainalt+enter处理异常s.out自动打印sctrl+art+t给整段代码加框如try-catch一、概念进程、程序和进程程序(program)是为完成任务、用某种语言编写的一组指令的集合。即指一段静态的代码,......
  • 同个线程里,如果线程正在忙过程中,定时器时间到了会被延迟触发吗?
    同个线程里,如果线程正在忙过程中,定时器时间到了会被延迟触发吗?在同一线程中,如果线程正在忙过程中,定时器的触发事件会被延迟,直到线程空闲下来才会被触发。这是因为在QT中,线程和定时器的处理都是通过事件循环来完成的。当线程处于忙碌状态时,事件循环将会被阻塞,直到线程执行完当前的......
  • 面试准备不充分,被Java守护线程干懵了,面试官主打一个东西没用但你得会
    写在开头面试官:小伙子请聊一聊Java中的精灵线程?我:什么?精灵线程?啥时候精灵线程?面试官:精灵线程没听过?那守护线程呢?我:守护线程知道,就是为普通线程服务的线程嘛。面试官:没了?守护线程的特点,怎么使用,需要注意啥,Java中经典的守护线程都有啥?我:不知道。。。这的天,面试一个10K的工作,......