Java里HashMap多线程环境下可能引起的死循环的问题的测试与解决

问题

众所周知,HashMap是线程不安全的,在并发使用HashMap时很容易出现一些问题,其中最典型的就是并发情况下扩容之后会发生死循环,导致CPU占用100%。同时,这也是一个高频面试题。

原因

引发死循环的原因,是多线程环境下,HashMap在resize扩容的时候,对新数组重新rehash的时候导致的。可以看下HashMap的源码

其中,transfer方法的核心代码分为以下四个步骤:
1.Entry next = e.next;获取要转移的下一个节点next;
2.e.next = newTable[i];使用头插法将要转移的节点插入到newTable原有的单链表中;
3.newTable[i] = e;将newTable的hash桶的指针指向要转移的节点;
4.e = next;转移下一个需要转移的节点e。

测试代码

解决

使用J.U.C下面的ConcurrentHashMap代替HashMap在并发环境下也是线程安全的。

0

Java8异步编程类CompletableFuture使用

一、了解Future

CompletedFuture实现了Future接口,自Java 1.5以来有Future接口,Future类表示异步计算的未来结果,这个结果最终将在处理完成后出现在Future中。Future.isDone()方法用来检查计算是否完成,Future.get()获取计算完成返回结果,请注意,此方法会阻止执行,直到任务完成。Future.cancel(boolean)告诉执行程序停止操作并中断其底层线程。

执行结果

上面的例子虽然可以异步执行任务,但是却不能优雅的获取结果,只能是用阻塞或者轮询的方式来获取任务结果。轮询加阻塞,总是给人一种怪怪的感觉。

所以,在Java 8中, 新增加了一个包含50个方法左右的类: CompletableFuture,提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合CompletableFuture的方法。

二、 CompletableFuture 类介绍

2.1 创建 CompletableFuture

2.1.1 可以使用简单的构造方法

2.1.2 使用静态方法

2.2 获取计算结果

保留了原有的get()和get(long timeout, TimeUnit unit)方法,另外还新增了

get(long timeout, TimeUnit unit)表示只在限定时间内等待获取结果,超时就抛出异常。

getNow(T valueIfAbsent)表示如果已经计算完就立马返回结果或者抛出异常,否则就返回预设的valueIfAbsent。

join()也是用来获取返回的结果,如果有异常就抛出一个unchecked异常( CompletionException ),它和get()抛出的异常不一样,可以分别运行一下下面两行代码

第一张图是get()的结果,第二张图是join()的结果

2.3 计算完成后的动作处理

当CompletableFuture的计算结果完成,或者抛出异常的时候,我们可以执行特定的Action。主要是下面的方法:

带Async的方法,说明是以新的线程池运行,而具体是不是新的线程池,那就要看这里指定的线程池是不是和创建CompletableFuture时指定的一样,另外,如果是同一个线程池,也有可能被同一个线程选中。

一个简单的示例

2.4 计算结果转换

有的时候我们需要直接将计算的结果A类转换为B类。CompletableFuture由于是回调风格,我们可以不必阻塞的告诉CompletableFuture当计算完成的时候请执行某个function。而且我们还可以将这些操作串联起来,或者将CompletableFuture组合起来。

看一下转换方法有哪些

看下这个例子

2.5 纯消费执行

上面的方法是当计算完成的时候,会生成新的计算结果(thenApply, handle),或者返回同样的计算结果whenComplete,CompletableFuture还提供了一种处理结果的方法,只对结果执行Action,而不返回新的计算值,因此计算值为Void:

示例

0

如何合理的设置线程池ThreadPoolExecutor的大小

线程池究竟设置多大要看你的线程池执行的什么任务了,CPU密集型、IO密集型、混合型,任务类型不同,设置的方式也不一样

任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池

1、CPU密集型

尽量使用较小的线程池,一般Cpu核心数+1

因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销

2、IO密集型

方法一:可以使用较大的线程池,一般CPU核心数 * 2

IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间

方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

下面举个例子:

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) CPU数目

3、混合型

可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定

0

LongAdder与 AtomicLong的异同区别

1.AtomicLong 是基于 CAS 方式自旋更新的;LongAdder 是把 value 分成若干cell,并发量低的时候,直接 CAS 更新值,成功即结束。并发量高的情况,CAS更新某个cell值和需要时对cell数据扩容,成功结束;更新失败自旋 CAS 更新 cell值。取值的时候,调用 sum() 方法进行每个cell累加。
2.AtomicLong 包含有原子性的读、写结合的api;LongAdder 没有原子性的读、写结合的api,能保证结果最终一致性。
3.低并发场景AtomicLong 和 LongAdder 性能相似,高并发场景 LongAdder 性能优于 AtomicLong。

0

CPU访问中的时间空间局部性原理解析

一、定义

在CPU访问寄存器时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

  • 时间局部性(temporal locality):被引用过一次的存储器位置在未来会被多次引用(通常在循环中)。
  • 空间局部性(spatial locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

二、示例

在这个例子中,变量在内存中的存储结构如下:

可以看出从a[0]到a[4]的地址位是连续的,而且程序中访问这些变量频率较高,所以对与a来说,其具有良好的空间局部性,但是每个a成员只被访问一次,所以并不具有良好的时间局部性。

但是对于变量sum来说,其由于其是一个标量,也就是说通过其地址只能得到一个值,故其不具有良好的空间局部性,但是由于其访问频率较高,所以有良好的时间局部性。

通过以上结论可以推出,对于向量a来说,如果其访问顺序和存储顺序一致,那么a的变量之间距离太远,那么其空间局部性越差,因为CPU没办法在其附近找到其他变量,所以空间局部性和a的步长有很大的关系,步长越长,空间局部性越差

同理,如果一个变量被访问的频率越高,其时间局部性就越好

取指令的局部性

指令存在存储器中,CPU要读取指令必须取出该指令,所以就可以评价取指令的局部性。

在for循环中,循环体内的指令多次被执行,所以有良好的时间局部性,循环体中的指令是按顺序执行的,有良好的空间局部性(指令在存储器中是顺序存放的)。

小结

评价局部性的简单原则:

1.重复引用同一个变量具有良好的时间局部性。
2.对于步长为k的引用程序,步长越小,空间局部性越小。步长为1的引用具有良好的空间局部性。k越大,空间局部性越差。
3.对于取指令来说、循环有良好的时间和空间局部性。

0

详解单核多线程与多核多线程的区别

单核多线程与多核多线程

总有同学对于单核多线程和多核多线程有点误区,因为会听到一些同学问为什么单核能处理多线程,下面会通俗说明下。

线程和进程是什么

线程是CPU调度和分配的基本单位(可以理解为CPU只能看到线程)
进程是操作系统进行资源分配(包括cpu、内存、磁盘IO等)的最小单位

单核多线程

单核多线程指的是单核CPU轮流执行多个线程,通过给每个线程分配CPU时间片来实现,只是因为这个时间片非常短(几十毫秒),所以在用户角度上感觉是多个线程同时执行。

多线程上下文切换

在这里也引出多线程上下文切换,也就是CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。并且在切换前会保存上一个任务的状态,当切换回上一个任务时可以继续加载该任务的状态,从任务保存到再加载的过程就是一次上下文切换。

例如:我们在看一篇纯英文的文档的时候,由于经常会有单词不认识,我们需要在不认识的地方打个记号,然后去翻英汉词典,等把这个单词搞清楚后再返回去看那篇英文文档,从标记的地方继续往下看(这就是上下文切换的例子)。不过在看到一半然后去看别的文章,会影响到效率,多线程也一样,会影响到多线程执行速度。

多核多线程

多核多线程,可以把多线程分配给不同的核心处理,其他的线程依旧等待,相当于多个线程并行的在执行,而单核多线程只能是并发。

总结

  • 单CPU中进程只能是并发,多CPU计算机中进程可以并行。
  • 单CPU单核中线程只能并发,单CPU多核中线程可以并行。
  • 无论是并发还是并行,使用者来看,看到的是多进程,多线程。
+2