A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

经常会看到说HashMap是线程不安全的,ConcurrentHashMap是线程安全的等等说法,不禁有个疑问,什么是线程安全?什么样的类是线程安全的?

1.什么是线程安全性(what)
线程安全定义,最核心是正确性,

正确性:多个线程访问某个类,不管怎么调度这些线程,其代码中不需要额外的同步或协同(synchronized),这个类依然有正确的行为。

线程安全类封装了必要的同步机制,在任何调用对象的public方法或读写pulic字段field时,都不会破坏对象的一致性状态(符合不变性条件或后验条件)

为什么一段看似正确的代码,多线程并发执行就会不正确了呢??大概有以下3种问题




[Java] 纯文本查看 复制代码
public class DoubleCheckSingleton {
    /**
     * 使用volatile,在多线程场景下,确保在判断null时,对所有线程可见
     */
    private static volatile DoubleCheckSingleton uniqInstance;
    /**
     * 构造器私有,防止外部实例化该类
     */
    private DoubleCheckSingleton() {}
    /**
     * 静态方法实例化,由于在类内部,可以调用构造器
     */
    public static DoubleCheckSingleton getInstance(){
        if (null == uniqInstance) { // 此处判断需要可见性volatile
            synchronized (DoubleCheckSingleton.class) {
                if (null == uniqInstance) {
                    //延迟初始化
                    uniqInstance = new DoubleCheckSingleton();
                }
            }
        }
        return uniqInstance;
    }
}

2.如何分辨一个类是否线程安全?(HOW)
3.为什么hashmap不安全 why

最简单的线程安全集合实现,可以发现是通过以上线程安全共享的方式实现安全。

[Java] 纯文本查看 复制代码
public class SafeSet {
    // final
    private final Set<Integer> myset = new HashSet<>(); //饿汉  一旦创建不会发布出去
 
    // synchronized 同步访问
    public synchronized void addInteger(Integer p){
        myset.add(p);
    }
    public synchronized boolean containsInteger(Integer p) {
        return myset.contains(p);
    }
}

3.1 插入HashMap.put

如下,put方法有很多先检查后操作(需要原子性执行)的代码,由于put()和putVal()代码没有同步,所有的线程可以执行到此处代码,

(1)table==null? 初始化线程A执行check操作后,发生线程切换,B也check table==null操作,A、B都会resize()更新table,产生更新丢失!

[Java] 纯文本查看 复制代码
if ((tab = table) == null || (n = tab.length) == 0)//(1)线程切换
            n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//(2)线程切换
    tab[i] = newNode(hash, key, value, null);

(2)tab==null?  A 线程和 B 线程计算出相同的哈希值对应了相同的数组位置,此时该位置还没数据,然后对同一个数组位置,两个线程会同时 写入新的头结点,那B的写入操作就会覆盖 A 的写入,造成 A 的写入操作丢失。


3.1.1 HashMap 在扩容的时候

  HashMap 插入后超过阈值会触发扩容resize操作,new一个新容量cap的数组,对原数组的键值对重新进行计算hash并写入新数组,然后指向新数组。

[Java] 纯文本查看 复制代码
 if (++size > threshold)// 线程切换
    resize();	

当A、B线程同时进来,检测到总数量超过阈值的时候就会同时触发 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组,结果最终只有最后一个线程生成的新数组被赋给该 map 底层,其他线程的均会丢失。


JDK1.7甚至在扩容时并发有可能形成链表回路,导致后续操作遍历链表时有死循环!具体请戳


3.2 HashMap 在删除数据的时候

  删除这一块可能会出现两种线程安全问题,

[Java] 纯文本查看 复制代码
if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {
    Node<K,V> node = null, e; K k; V v;
    // 找到对应节点
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//切换线程(2)
        node = p;
    else if ((e = p.next) != null) {
        if (p instanceof TreeNode)
            node = ((TreeNode<K,V>)p).getTreeNode(hash, key); // 红黑树查找
        else { // 链表查找
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    node = e;
                    break;
                }
                p = e;
            } while ((e = e.next) != null);
        }
    }
    if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v))))  {
        if (node instanceof TreeNode)
            ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); //删除树节点
        else if (node == p)
            tab[index] = node.next;	// 删除链表头
        else
            p.next = node.next; // 删除链表中间
        ++modCount;
        --size;
        afterNodeRemoval(node);
        return node;
    }
}
return null;

线程A判断得到了指定的数组位置i并进入了循环,此时,线程B已经删掉位置i数据了,然后线程A那边就没了。但是删除的话,没了倒问题不大,只是A返回的就是null

当A、B线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。

还有很多检查后操作的情况不能保证原子性操作,且有检查的时候不能保证可见性,检查到的值有可能是失效数,执行过程中不能保证其一致性状态,也不能保证符合程序设计者的预期!


且不安全地发布了内部字段,如keyset等,容易被其它现在在类外不可预期地修改!!


转自CSDN原文链接:https://blog.csdn.net/sarafina527/article/details/105040594



50 个回复

倒序浏览

感谢楼主的分享  棒棒棒
回复 使用道具 举报
666666666666666666666666666
回复 使用道具 举报
666666666666666666666666666666666
回复 使用道具 举报


感谢楼主的分享  棒棒棒
回复 使用道具 举报
厉害了                       
回复 使用道具 举报
66666666666666666
回复 使用道具 举报
感谢分享  棒棒哒
回复 使用道具 举报

感谢分享  棒棒哒
回复 使用道具 举报

感谢分享  棒棒哒
回复 使用道具 举报
回复 使用道具 举报
好人一生平安
回复 使用道具 举报


                                                                                                  
键盘敲烂,月薪过万
回复 使用道具 举报
可以的,奥利给!!!
回复 使用道具 举报
66666666666666666666666
回复 使用道具 举报
66666666666666666666666
回复 使用道具 举报
66666666666666
回复 使用道具 举报
666666666666
回复 使用道具 举报
加油加油加油!!!
回复 使用道具 举报
键盘敲烂,月薪过万
回复 使用道具 举报
123下一页
您需要登录后才可以回帖 登录 | 加入黑马