HashMap的线程安全问题,可运行demo

前言

在jdk1.7中,hashMap的实现是数组加链表(jdk1.8冲突不强的情况下也是链表)。但是1.7中在转移数组的时候,链表转移方式是尾插法。[扩容的时候会转移数组]
尾插法将带来一定的并发问题,核心在于形成了一个闭环,导致while循环无法结束
由于使用hashMap去重现这个问题,不一定能重现,所以自己写了一个demo,一跑就知道尾插法的问题出在哪里

Demo

模拟代码假设旧的节点数组长度为10,现在要扩容到20。经过hash计算要将原来位于node[5]的链表移植到新的节点数组的newNode[hash=3]位置。
以下模拟了两种移植算法,头插法和尾插法,其中头插法会产生循环数组,导致死循环[好多地方管这叫死锁??没懂]。尾插法就不会。

public class HashMapSimulation {
    static Node[] newNodes = new Node[20];   
    public static void main(String[] args) throws InterruptedException {
        Node[] nodes = new Node[6];
        nodes[5]=new Node(1);
        nodes[5].next=new Node(2);
        nodes[5].next.next=new Node(3);
        
        //头插法,jdk1.7。导致循环数组----很多地方说是死锁,感觉叫法不太科学==
        new Thread(()->transByHead(nodes,5,3),"thread1").start();
        new Thread(()->transByHead(nodes,5,3),"thread2").start();

        //尾插法,不会导致循环列表
//        new Thread(()->transByTail(nodes,5,3),"thread3").start();
//        new Thread(()->transByTail(nodes,5,3),"thread4").start();

    }
    //将nodes数组中第idx个元素移动到新数组的hash下标处。
    public static void transByHead(Node[] nodes,int idx,int hash) {
        Node e = nodes[idx];
        int i=0;
        while(e!=null){
            Node next = e.next;
            try{
                if(i++==0){
                    if(Thread.currentThread().getName().equals("thread1")) //模拟死锁环境
                        Thread.sleep(10);
                    else Thread.sleep(5);
                }
            }catch (Exception ignored){}
            e.next=newNodes[hash];  //当前节点指向新数组位置的头结点
            newNodes[hash]=e;       //当前节点替换头结点
            e=next;
        }
    }
    //尾插法
    public static void transByTail(Node[] nodes,int idx,int hash){
        Node e = nodes[idx];
        Node last = newNodes[hash];
        int i=0;
        while(e!=null){
            Node next = e.next;
            try{
                if(i++==0){
                    if(Thread.currentThread().getName().equals("thread1"))
                        Thread.sleep(10);
                    else Thread.sleep(5);
                }
            }catch (Exception ignored){}
            if(last!=null)
                last.next=e;
            last=e;
            e=next;
        }
        last.next=null;
    }
}

class Node{
    int val;
    Node next;
    Node(int val){
        this.val=val;
    }
}

死循环分析

文字版:
最开始的链表为1->2->3.我们的线程1将cur指向1,next指向cur.next=2。然后线程1阻塞
然后线程2仍然将其的cur指向1,next指向cur.next=2。此时线程2继续执行,将链表1->2->3转移到新的数组,最后新的数组节点变成3->2->1的逆序形式[看代码画个图就知道了]
问题来了,此时线程1开始执行。注意我们的线程1中的cur指向1,next指向2.首先第一步将cur.next指向newNode[i],即指向了3,变成了3->2->1->3,,死循环就形成了,因为一旦形成了这样的循环链表,next永远不会为null,trans方法永远无法执行完毕

相反的,尾插法就没有这个问题。
同样,线程1将旧数组中的1->2->3转换到新的数组成为了1->2->3【这个结构实际上没有变,详见尾插法的代码】。此时线程2的cur指向1,next指向2,循环两次后next指向了null,线程2就也正常结束了。所以没问题

图形版
左图为线程执行前,两个线程都拿到了cur,next值。然后线程1正常执行完毕,由于采用了尾插法,得到的新链表如右图所示。注意到线程2的cur和next实际在链表中的位置变了,此时线程2开始执行,首先执行cur.next=newNode[i],一旦这条语句执行完毕,循环链表就形成了,而一旦形成了循环链表,hashmap卡死,最终程序崩溃
左图为线程执行前,两个线程都拿到了cur,next值。然后线程1正常执行完毕,由于采用了尾插法,得到的新链表如右图所示。注意到线程2的cur和next实际在链表中的位置变了,此时线程2开始执行,首先执行cur.next=newNode[i],一旦这条语句执行完毕,循环链表就形成了,而一旦形成了循环链表,hashmap卡死,最终程序崩溃

其他问题

说hashmap并发不安全,通常指两个方面,一个是上面说的循环链表卡死。二个是两个hash值相同的对象同时put,丢失数据,[还有一个A线程扩容的时候,B线程插入的所有数据都将丢失]
第二种情况可以观察下面这个demo:

public class ConCurrentPutTest {
    public static void main(String[] args) throws InterruptedException {
        Set<MapObj> set = new HashSet<>();
        CountDownLatch latch = new CountDownLatch(1000);
        IntStream.rangeClosed(1,1000).forEach(i->{
            new Thread(()->{
                latch.countDown();
                try {
                    latch.await();
                } catch (InterruptedException ignored) {}
                set.add(new MapObj());
            }).start();
        });
        Thread.sleep(10000);
        System.out.println(set.size());
    }
    static class MapObj{
        @Override
        public int hashCode(){   //hashcode一样会被放在一个链表(红黑树)里面[充分不必要]
            return 0;
        }
        @Override
        public boolean equals(Object obj){  //equals相同的话就会被判定是同样的对象,会覆盖
            return false;
        }
    }
}

输出如:
在这里插入图片描述
线程更多一点,更明显:
在这里插入图片描述
如果将程序改成concurrentHashMap就不会有问题——因为没有concurrentHashSet,这里就用map了:
在这里插入图片描述在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个基于 Spring Boot 的多数据源配置示例: 首先,在 `application.properties` 文件中配置数据源属性: ```properties # 主数据源配置 spring.datasource.primary.url=jdbc:mysql://localhost:3306/primary?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.primary.username=root spring.datasource.primary.password=root spring.datasource.primary.driver-class-name=com.mysql.jdbc.Driver # 第二个数据源配置 spring.datasource.secondary.url=jdbc:mysql://localhost:3306/secondary?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.secondary.username=root spring.datasource.secondary.password=root spring.datasource.secondary.driver-class-name=com.mysql.jdbc.Driver ``` 然后,创建多数据源配置类 `MultipleDataSourceConfig`: ```java @Configuration public class MultipleDataSourceConfig { @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") public DynamicDataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource, @Qualifier("secondaryDataSource") DataSource secondaryDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DatabaseType.PRIMARY, primaryDataSource); targetDataSources.put(DatabaseType.SECONDARY, secondaryDataSource); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(primaryDataSource); return dataSource; } } ``` 其中,`primaryDataSource()` 和 `secondaryDataSource()` 方法分别创建主数据源和第二个数据源,`dynamicDataSource()` 方法则创建一个动态数据源,将两个数据源加入到其中。 最后,创建 `DatabaseType` 枚举类表示数据库类型: ```java public enum DatabaseType { PRIMARY, SECONDARY } ``` 并创建 `DynamicDataSource` 类实现动态数据源: ```java public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DatabaseContextHolder.getDatabaseType(); } } ``` 在上面的类中,`determineCurrentLookupKey()` 方法根据当前线程的数据源类型,返回对应的数据源。 最后,创建一个 `DatabaseContextHolder` 类用于设置当前线程的数据源类型: ```java public class DatabaseContextHolder { private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>(); public static void setDatabaseType(DatabaseType databaseType) { contextHolder.set(databaseType); } public static DatabaseType getDatabaseType() { return contextHolder.get(); } public static void clearDatabaseType() { contextHolder.remove(); } } ``` 在使用多数据源的时候,需要在代码中手动设置当前线程的数据源类型,例如: ```java // 切换到主数据源 DatabaseContextHolder.setDatabaseType(DatabaseType.PRIMARY); // 执行查询语句 List<User> userList = userMapper.select(); // 切换到第二个数据源 DatabaseContextHolder.setDatabaseType(DatabaseType.SECONDARY); // 执行查询语句 List<Department> departmentList = departmentMapper.select(); // 切换回主数据源 DatabaseContextHolder.setDatabaseType(DatabaseType.PRIMARY); ``` 以上就是一个基于 Spring Boot 的多数据源配置示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值