聊聊并发编程的10个坑( 四 )

在HashMapService类中定义了一个HashMap的成员变量,在add方法中往HashMap中添加数据 。在controller层的接口中调用add方法,会使用tomcat的线程池去处理请求,就相当于在多线程的场景下调用add方法 。
在jdk1.7中,HashMap使用的数据结构是:数组+链表 。如果在多线程的情况下,不断往HashMap中添加数据,它会调用resize方法进行扩容 。该方法在复制元素到新数组时,采用的头插法,在某些情况下,会导致链表会出现死循环 。
死循环最终结果会导致:内存溢出 。
此外,如果HashMap中数据非常多,会导致链表很长 。当查找某个元素时,需要遍历某个链表,查询效率不太高 。
为此,jdk1.8之后,将HashMap的数据结构改成了:数组+链表+红黑树 。
如果同一个数组元素中的数据项小于8个,则还是用链表保存数据 。如果大于8个,则自动转换成红黑树 。
为什么要用红黑树?
答:链表的时间复杂度是O(n),而红黑树的时间复杂度是O(logn),红黑树的复杂度是优于链表的 。
既然这样,为什么不直接使用红黑树?
答:树节点所占存储空间是链表节点的两倍,节点少的时候,尽管在时间复杂度上,红黑树比链表稍微好一些 。但是由于红黑树所占空间比较大,HashMap综合考虑之后,认为节点数量少的时候用占存储空间更多的红黑树不划算 。
jdk1.8中HashMap就不会出现死循环?
答:错,它在多线程环境中依然会出现死循环 。在扩容的过程中,在链表转换为树的时候,for循环一直无法跳出,从而导致死循环 。
那么,如果想多线程环境中使用HashMap该怎么办呢?
答:使用ConcurrentHashMap 。
7. 使用默认线程池我们都知道jdk1.5之后,提供了ThreadPoolExecutor类,用它可以自定义线程池 。
线程池的好处有很多,比如:

  1. 降低资源消耗:避免了频繁的创建线程和销毁线程,可以直接复用已有线程 。而我们都知道,创建线程是非常耗时的操作 。
  2. 提供速度:任务过来之后,因为线程已存在,可以拿来直接使用 。
  3. 提高线程的可管理性:线程是非常宝贵的资源,如果创建过多的线程,不仅会消耗系统资源,甚至会影响系统的稳定 。使用线程池,可以非常方便的创建、管理和监控线程 。
当然jdk为了我们使用更便捷,专门提供了:Executors类,给我们快速创建线程池 。
该类中包含了很多静态方法:
  • newCachedThreadPool:创建一个可缓冲的线程,如果线程池大小超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程 。
  • newFixedThreadPool:创建一个固定大小的线程池,如果任务数量超过线程池大小,则将多余的任务放到队列中 。
  • newScheduledThreadPool:创建一个固定大小,并且能执行定时周期任务的线程池 。
  • newSingleThreadExecutor:创建只有一个线程的线程池,保证所有的任务安装顺序执行 。
在高并发的场景下,如果大家使用这些静态方法创建线程池,会有一些问题 。
那么,我们一起看看有哪些问题?
  • newFixedThreadPool:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM 。
  • newSingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM 。
  • newCachedThreadPool:允许创建的线程数是Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM 。
那我们该怎办呢?
优先推荐使用ThreadPoolExecutor类,我们自定义线程池 。
具体代码如下:
ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数10, //maximumPoolSize 线程池中最大线程数60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue(500), //队列new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略顺便说一下,如果是一些低并发场景,使用Executors类创建线程池也未尝不可,也不能完全一棍子打死 。在这些低并发场景下,很难出现OOM问题,所以我们需要根据实际业务场景选择 。
8. @Async注解的陷阱之前在java并发编程中实现异步功能,一般是需要使用线程或者线程池 。
线程池的底层也是用的线程 。
而实现一个线程,要么继承Thread类,要么实现Runnable接口,然后在run方法中写具体的业务逻辑代码 。


推荐阅读