java线程安全hashMap和concurrenthashMap源码对比分析

怎样停止一个线程?

官方给出了两个api:

  1. stop
  2. interrupt
    但是stop是已经被废弃了。废弃原因是什么呢?
    线程stop.png
  3. 线程共享的东西有三种:cpu,共享内存,文件
  4. Thread1访问共享内存,并持有锁,线程2等待,此时阻塞
  5. Thread1释放锁,被销毁,此时Thread2立即持有共享内存的锁,如果Thread1有脏数据没有及时清理,这时候,Thread2运行时发现内存状态异常,可能会crash.

那么,怎样才是良好的中断线程的方式呢?

首先:Thread提供了两个方法:

  1. isInterrupted: 非静态方法,获取当前方法的中断状态,不清空
    • 调用的想蹭对象对应的线程
    • 可重复调用,中断清空钱一直返回true
  2. interrupted:静态方法,加锁 获取当前线程的终端状态,并清空
    • 当前运行的线程
    • 中断状态调用后清空,重复调用后续返回false.
      二者都是jni方法,所以在性能上可能不占用优势

另外一种方式是使用boolean标志位

public class InterruptedThread extends Thread {
    volatile boolean isStopped = false; // 保证可见性

    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 10000; i++) {
            if (isStopped) {
                return;
            }
        }
    }
    

}
 public static void main(String args[]) {
        InterruptedThread thread;
        thread = new InterruptedThread();
        thread.start();
        thread.isStopped = true;
    }

在使用boolean标志位的方式上,一定要注意volatile关键字的使用,保证可见性。

这两种方式有什么差异呢?

内容interruptboolean
系统方法
使用JNI
加锁
触发方式抛异常布尔值判断,也可以抛异常
  • 需要支持系统方法时(如thread.sleep)使用中断
  • 其他情况boolean标志位

如何写出线程安全的程序?

1. 知识点

  1. 是否对线程安全有初步了解
  2. 是否对线程安全的产生原因有思考
  3. 是否知道final,volatile 关键字的作用
  4. 是否知道1.5之前java DCL 为什么有缺陷
  5. 是否清楚的知道如何编写线程安全的程序
  6. 是否对ThreadLocal的使用注意事项有认识

2. 剖析

什么才是线程安全的?
volatile 关键字
每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中
WechatIMG5.jpeg

什么是线程安全–>可变资源(内存)线程间共享

1. 不共享资源
解决方法,纯函数,传一个参数进来,再返回一个参数出去,中间不涉及任何内存的访问。
2. 使用的内存变量与线程绑定
ThreadLoacal 就是典型的例子,ThreadLocal 中存储数据的类是ThreadLocalMap.Entry. 首先,通过线程对象获取对应的ThreadlocalMap,如果map不为null,再获取当点对象对应的数据。

 
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
        
   static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }        
        
     public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
        
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

对比THreadLocalMapWeakHashMap
对象持有弱引用弱引用
对象GC不影响不影响
应用清除1. 主动移除 2. 线程退出移除1. 主动移除 2. GC后移除(监听ReferenceQueue)
Hash冲突开放定址法单链表法
hash计算神奇数字的倍数对象 hashCode 在散列
使用场景对象较少通用

使用ThreadLocal 注意事项

  1. 声明为全局的静态final成员,因为在存取对象的时候,是通过使用threadLoacal对象为key的,如果频繁更换threadlocal对象,可能会导致设置的对象找不回来。其次就是可见性的问题了。

  2. 避免存储大量的对象。

  3. 用完后及时移除对象。没有监听referenceQueue的机制,如果一个线程一直存在,就导致这个引用一直不会被移除。

  4. 共享不可变资源

    1. final 关键字 有禁止重排序的功能
    2. volatile 关键字(从1.5 之后,语义增强,增加禁止重排序功能)
  5. 共享可变资源

    • 可见性
    • 操作原子性
    • 禁止重排序

保证可见性的方法

  • 使用final 关键字
  • 使用volatile 关键字
  • 加锁,释放锁时,会将高速缓存中的数据强制刷新到主存中

在1.5之前
分配内存,调用构造方法,赋值 三步

concurrentHashMap如何支持并发访问

CHM的并发优化历程

版本优化
JDK1.5分段锁,必要时加锁
JDK1.6优化二次hash算法
JDK1.7段懒加载,volatile&cas
JDK1.8摒弃段,基于hashMap原理实现并发

区别于hashTable :整个hash表锁
JDk1.5存在的问题
如果我的key是一个整数,hash值对应的高位,对于30000多一下对应的整数对应的hash值都是一样的,导致都存在了一个segment 中,使得整个chm退化成了hashTable
jdk1.6 分段锁的hash优化,保证了高位低位都均匀分布。

jdk1.7 分段锁懒加载

hashMap是一个散列结构先来看看几个重要的局部变量吧
在1.7中,hashmap的数据结构图
undefined
undefined

  1. 初始化桶大小,因为底层是数组,所以这是数组默认的大小。
  2. 桶最大值。
  3. 默认的负载因子(0.75)
  4. table 真正存放数据的数组。
  5. Map 存放数量的大小。
  6. 桶大小,可在初始化时显式指定。
  7. 负载因子,可在初始化时显式指定。
    整体结构是数组+链表的形式,在解决hash冲突的问题上,选用了下拉连法。这个处理方式有个弊端:当数据足够多时,hash冲突的概率增加,这时候的查询复杂度可能退化成O(n).

hashEntry

undefined
使用了final关键字修饰了hash,kay,防止指令重排序
使用volatile修饰value,next
UnSafe.getObjectVolatile.

put和get操作实现

  1. put方法
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
undefined

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

undefined

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  4. 最后会解除在 1 中所获取当前 Segment 的锁。

get方法

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

  • 1.7 以前,segment是直接初始化的。当构造完chm后,16个segment都被new 出来了,而在1.7时,是需要哪个用哪个。
  • 此时,就涉及到一个可见性的点,

CHM 1.8 实现

1.7 已经解决了并发问题,但是在查询效率上,会出现hashMap在1.7中遇到的同样问题,可能会退化成O(n)
在1.8中直接摒弃了Segment分段锁,而采用了CAS+synchronized来保证并发安全性。

put 方法

undefined

  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。
  3. f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get方法

undefined

  1. 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  2. 如果是红黑树那就按照树的方式获取值。
  3. 就不满足那就按照链表的方式遍历获取值。

CHM如何计数

  1. JDK5-7基于段元素个数求和,如果两次求和不同,二次不同就加锁。
  2. jdk1.8引入CounterCell,本质上也是分段加锁。

CHM是弱一致性

  1. 添加元素后不一定马上能读到
  2. 清空之后可能仍然会有元素
  3. 遍历之前的段元素的变化会读到
  4. 遍历之后的段元素变化读不到
  5. 遍历时元素发生变化不抛异常

hashtable的问题

  1. 大锁:对整个hashTable对象加锁
  2. 长锁:直接对方法加锁
  3. 读写锁共用:只有一把锁,从头到尾

CHM的解法

  1. 小锁: 分段锁(5-7),桶节点锁(8)
  2. 短锁:先尝试获取锁,失败在加锁
  3. 分离读写锁:失败在加锁(5-7),volatile读,cas写(7-8)

如何进行锁优化

  • 长锁不如短锁:尽可能只锁必要的部分
  • 大锁不如小锁: 尽可能对加锁的对象拆分
  • 公锁不如私锁:尽可能将锁的逻辑放到私有代码中。
  • 嵌套锁不如扁平锁
  • 分离读写锁: 尽可能将读锁和写锁分离。
  • 粗化高频锁: 尽可能合并处理频繁过短的锁
  • 消除无用锁:尽可能不加锁,或用volatile代替。

AtomicReference和AtomicRefereFiledUpdater有何异同

涉及知识点

  1. 原子操作的概念
  2. 是否熟悉AR和ARFU这两个类的用法和原理
  3. java对象的内存占用的认识

android 中如何写出优雅的代码

考察点

  1. 是否熟练编写异步和同步的代码
  2. 是否书序回调地狱
  3. 是否熟练使用RxJava
  4. 是否对Kotlin协程有了解
  5. 是否具备编写良好的代码的意识和能力

异步的目的是什么

  1. 提高CPU利用率
  2. 提高GUI程序的响应速度
  3. 异步不一定快

为了解决异步回调地狱的问题:有Rxjava

参考文献

  1. hashMap1.8分析
  2. concurrenthashMap详情介绍
  3. hashmap线程不安全,concurrentHashMap1.7与1.8对比
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值