多线程学习时常出现的问题(一)高并发下的ArrayList和并发下诡异的HasMap

简介: 多线程学习时常出现的问题(一)高并发下的ArrayList和并发下诡异的HasMap

高并发下的ArrayList

  • 我们都知道,ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错。究竟可能引起哪些问题呢?试看下面的代码:
public class ArrayListMultiThread{
static ArrayList<Integer> al = new ArrayList<Integer> (10);public static class AddThread implements Runnable {
eOverride
public void run(){
for (int i=0;i<1000000; i++){
al .add(i);
public static void main(String[] args) throws InterruptedException {
Thread tl=new Thread(new AddThread());
Thread t2=new Thread(new AddThread());t1.start();
t2.start();
t1.join();t2.join();
System.out.println(al.size();
  • 上述代码中,t1和t2两个线程同时向一个ArrayList中添加容器。它们各添加100万个元素,因此我们期望最后可以有200万个元素在 ArrayList中。但如果执行这段代码,则可能得到三种结果。
  • 第一,程序正常结束,ArrayList 的最终大小确实200万。这说明即使并行程序有问题,也未必会每次都表现出来。
  • 第二,程序抛出异常。
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException:22
at java.util.Arraylist.add(ArrayList.java: 441)
at geym.conc.ch2.notsafe.ArrayListMultiThread$AddThread.run(ArrayListMultiThread.java:12)
at java. lang. Thread.run (Thread.java:724)1000015
  • 这是因为ArrayList在扩容过程中,内部一致性被破坏,但由于没有锁的保护,另外一个线程访问到了不一致的内部状态,导致出现越界问题。
  • 第三,出现了一个非常隐蔽的错误,比如打印如下值作为ArrayList 的大小。

1246772

  • 这是由于多线程访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个线程也对ArrayList 中的同一个位置进行赋值导致的。如果出现这种问题,那么很不幸,你就得到了一个没有错误提示的错误。并且,它们未必是可以复现的。
  • 注意:改进的方法很简单,使用线程安全的Vector代替ArrayList即可。

并发下诡异的HasMap

  • HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也可能会遇到意想不到的错误。不过和ArrayList不同,HashMap的问题似乎更加诡异。
public class HashMapMultiThread {
static Map<string,String> map = new HashMap<String, String>();
public static class AddThread implements Runnable {
int start=0;
public AddThread(int start){
this.start=start;
)
coverride
public void run(){
for (int i = start; i<100000; i+=2){
map.put(Integer.toString(i), Integer.toBinarystring(i));
public static void main(String[] args) throws InterruptedException{
Thread tl=new Thread (new HashMapMultiThread.AddThread(0));
Thread t2=new Thread (new HashMapMultiThread.AddThread(1));
t1.start();t2.start();
t1.join();t2.join();
System.out.printin (map.size());
}}
  • 上述代码使用t1和 t2两个线程同时对HashMap进行 put()方法操作。如果一切正常,则得到的map.size()方法就是 100 000。但实际上,你可能会得到以下三种情况(注意,这里使用JDK 7进行试验)。
  • 第一,程序正常结束,并且结果也是符合预期的,HashMap 的大小为100 000。
  • 第二,程序正常结束,但结果不符合预期,而是一个小于100 000的数字,比如98868。第三,程序永远无法结束。

前两种可能和ArrayList 的情况非常类似,因此不必过多解释。

  • 而对于第三种情况,如果是第一次看到,我想大家一定会觉得特别惊讶,因为看似非常正常的程序,怎么可能就结束不了呢?

注意:请读者谨慎尝试以上代码,由于这段代码很可能占用两个CPU核,并使它们的CPU占有率达到100%。如果CPU性能较弱,则可能导致死机,因此请先保存资料,再进行尝试。

  • 打开任务管理器,你会发现,这段代码占用了极高的 CPU,最有可能的表示是占用了两个CPU核,并使得这两个核的CPU使用率达到100%。这非常类似死循环的情况。
  • 使用jstack 工具显示程序的线程信息,如下所示。其中 jps可以显示当前系统中所有的Java进程,而jstack可以打印给定Java进程的内部线程及其堆栈。
C:AUserslgeym >jps
14240 HashMapMultiThread1192 Jps
C:Userslgeym >jstack 14240
  • 我们会很容易找到t1、t2和 main线程。
"Thread-1" prio=6 tid=0x00bb2800 nid=0x16e0 runnable [0x04baf000]java.lang.Thread.state: RUNNABLE
at java.util.HashMap.put(HashMap.java: 498)
at geym.conc.ch2.notsafe.HashMapMultiThread$AddThread.run
(HashMapMultiThread.java:26)
at java.lang. Thread.run(Thread.java:724)
"Thread-0" prio=6 tid=0x00bb0000 nid=0x1668 runnable [0x04d7000]java.lang. Thread.State: RUNNABLE
at java.util.HashMap.put (HashMap.java: 498)
at geym.conc.ch2.notsafe.HashMapMultiThread$AddThread.run(HashMapMultiThread.java:26)
at java.lang. Thread.run (Thread.java:724)
"main" prio=6 tid=0x00cOcc00 nid=0x16ec in 0bject.wait()[0x0102000]java.lang. Thread.state: WAITING (on object monitor)
at java.lang.0bject.wait (Native Method)
- waiting on <0x24930280>(a java.lang. Thread)at java.lang. Thread.join (Thread.java:1260)- locked <0x24930280> (a java.lang. Thread)at java.lang. Thread.join(Thread.java: 1334)
at geym. conc.ch2.notsafe.HashMapMultiThread.main(HashMapMultiThread.java:36)

可以看到,主线程main正处于等待状态,并且这个等待是由于 join()方法引起的,符合我们的预期。而t1和t2两个线程都处于Runnable状态,并且当前执行语句为HashMap.put()方法。查看put(方法的第498行代码,如下所示:

for (Entry<K, V>e= table[i]; e != null;e= e.next){
0bject k;
if (e.hash == hash &&((k = e.key)-= key Il key.equals (k))){
v oldvalue = e.value;
e.value = value;
e.recordAccess(this);return oldvalue;
  • 可以看到,当前这两个线程正在遍历HashMap 的内部数据。当前所处循环乍看之下是一个迭代遍历,就如同遍历一个链表一样。但在此时此刻,由于多线程的冲突,这个链表的结构已经遭到了破坏,链表成环了!当链表成环时,上述的迭代就等同于一个死循环,图2.9展示了最简单的一种环状结构,key1和 key2互为对方的next元素。此时,通过next引用遍历,将形成死循环。
  • 死循环的问题一旦出现,着实可以让你郁闷一下,但这个死循环的问题在JDK 8中已经不存在了。由于JDK8对HashMap的内部实现做了大规模的调整,因此规避了这个问题。即使这样,贸然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方案就是使用ConcurrentHashMap 代替HashMap。

摘自JAVA高并发程序设计,推荐推荐

相关文章
|
2月前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
241 0
|
9天前
|
安全 Java 程序员
ArrayList vs Vector:一场线程安全与性能优化的世纪之争!
在 Java 面试中,ArrayList 和 Vector 是高频考点,但很多人容易混淆。本文通过10分钟深入解析它们的区别,帮助你快速掌握性能、线程安全性、扩容机制等核心知识,让你轻松应对面试题目,提升自信!
43 18
|
1月前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
204 59
|
1月前
|
安全 Java
线程安全的艺术:确保并发程序的正确性
在多线程环境中,确保线程安全是编程中的一个核心挑战。线程安全问题可能导致数据不一致、程序崩溃甚至安全漏洞。本文将分享如何确保线程安全,探讨不同的技术策略和最佳实践。
42 6
|
1月前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
61 6
|
1月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1月前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
1月前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
2月前
|
存储 安全 Java
代码审查:从 ArrayList 说线程安全
我们在编码和做代码审查的过程中,要对涉及到多线程使用的场景时刻绷着一根弦,将隐患拒之门外。
43 4
|
1月前
|
缓存 监控 Java
Java 线程池在高并发场景下有哪些优势和潜在问题?
Java 线程池在高并发场景下有哪些优势和潜在问题?