likes
comments
collection
share

并发闲记(一)

作者站长头像
站长
· 阅读数 11

hash寻址的两种方式(hashMap和ThreadLocalMap)

数组的优势如果能定位直接可以取数,但是数组如果大小来回的变动就会很耗操作了。

链表的优势在于动态频繁的增加和删除,查找方面优势不大(当然有些改进的结构如:跳表。

树感觉是优化了链表的结构在查找和增加删除做了些均衡。

拉链结构是数组和链表的结合体。

  1. 拉链 熟悉的Hashmap存储用的是数组加链表
先用Hash定位,然后冲突用链表(冲突过多用到了红黑树解决查询效率)解决。
容量不够有扩容
  1. 开放寻址法 ThreadLocal直接是数组实现Map的
先用hash定位,冲突后向后寻址遇到null替换或者空设置,到最后了从头开始找。
为什么这里有个null
因为没有主动remove释放线程(这里是常看到的内存泄漏gc回收不掉,key是null但是value不是)
补救ThreadLocal其实在调用set(),getEntry(),remove()会做一次null的清理工作,降低内存泄漏风险。
ThreadLocalMap为什么设置key为弱引用?
如果强引用,这里key会有可达性垃圾回收的时候就回收不掉;但是弱引用回收掉后key为null了在后续补救措施里面会清理掉。另一个方面的原因是API的开放限制底层的操作尽可以能保证系统的正常运行。
这个也有扩容
  1. 为什么采用了不同的方法
ThreadLocalMap中的hash分布十分均匀冲突少,线性成本低并且经常需要清理无用的对象数组操作会更加的方便。
神奇的数字:0x61c88647
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT); 
}

4.线程间变量的共享传递:ThreadLocal把当前线程的变量传递解耦了,InheritableThreadLocal可以在父子线程间进行参数传递(但是对于线程池的复用之间解决是有问题的)。

`JDK`的[`InheritableThreadLocal`]类可以完成父线程到子线程的值传递。
但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;
这时父子线程关系的`ThreadLocal`值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的`ThreadLocal`值传递到任务执行时。

这里有个阿里的工具类TransmittableThreadLocal

并发小知识点

  1. synchronized和cas
synchronized是一种悲观锁且阻塞排他(状态变化包括无锁状态、偏向锁、轻量级锁和重量级锁)
cas是一种乐观锁且非阻塞。比较替换得到锁(熟悉的redis的setnx命令)。
cas这里有个ABA的问题解决办法需要引入版本号。(Atomic原子类的引用类型AtomicStampedReference)
为了提高效率有些引入了分段锁(常用的currenthashmap、jdk1.8添加的Atomic原子类的累加器LongAdder)可以竞争多个原子变量减少锁的冲突提高效率
  1. 原子操作的用途和类

写原子操作类是因为看到了Adder比之前的Atomic提高了并发性能里面用到了分段的原子操作。

1.  计数器:例如`AtomicInteger``AtomicLong`,它们可以用于高并发环境下的计数操作。由于其原子性,可以避免因并发导致的数据不一致问题。
2.  布尔值:例如`AtomicBoolean`,常用于多线程标记状态或控制流程。
3.  数组:例如`AtomicIntegerArray``AtomicLongArray`,适用于需要对数组元素进行原子操作的场景。
4.  引用:例如`AtomicReference``AtomicReferenceArray`,可用于更新对象的引用原子操作。
基本类型:AtomicInteger、AtomicBoolean、AtomicLong

数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

引用类型:AtomicReference、AtomicMarkableReference、AtomicStampedReference

注意:AtomicReference.compareAndSet比较的是否是相同的对象,不是调用对象的equal比较。
     
     AtomicStampedReference这里是有个版本控制的解决ABA问题
     compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)。
1)第一个参数expectedReference:表示预期值。
2)第二个参数newReference:表示要更新的值。
3)第三个参数expectedStamp:表示预期的时间戳。
4)第四个参数newStamp:表示要更新的时间戳。

对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

原子类型累加器:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、基类Striped64
  1. 控制并发的工具类
1.CountDownLatch 控制一组任务完成后执行后续任务,一般我是用在看一批多线程任务是否跑完。
2.CyclicBarrier  让一组线程等待一个公共的障碍点时同时执行
                 计数器释放后会重置
                 计数器到0的时候线程会同时执行,触发点是最后一个进去屏障的线程
3.Semaphore 信号量可加可减,可用于限流、生产者消费者问题
4.Exchanger 用于两个工作线程之间交换数据的封装工具
当一个线程到达 exchange 调用点时,如果其他线程此前已经调用了此方法,则其他线程会被调度唤醒并与之进行对象交换,然后各自返回;如果其他线程还没到达交换点,则当前线程会被挂起,直至其他线程到达才会完成交换并正常返回,或者当前线程被中断或超时返回。
1\3用的多点 2\4我基本没用

  1. 我用的后台任务的跑批
1.我一般做法数据库取一批数据,然后分组(列表分组用到工具包guava),丢给线程池执行多线程跑批,用CountDownLatch控制所有任务执行完毕后续操作。
里面如果有事务控制的话我用的原始的jdbc。
如何判断线程池线程执行完毕:法1.CountDownLatch
2.shutdown()后threadPool.isTerminated() 来判断线程池是否结束
3.while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) { }
4.FutrueTask和callable有返回值的

2.对于一些数据量大且需要比较匹配的数据(获取的数据尽可能减少程序的无用匹配比较)处理这个要尽可能根据业务来分组、分批处理。(这个碰到的不是太多没啥经验之前碰到了一个零售的稽核系统需要对账前期搞了点,应收(支付类型各种各样组合,人为设备借用),实收账、流水)