一、常见的思维误区---不可能是这里出的问题
创建对象报异常了?要是放在以前我绝对会说这是扯***,但遇到这个实际问题后我才发现是自己浅薄了。我之前认为这个错误不可能是创建对象的时候报出来的,是因为我们认为代码是下面这样的,而且是单线程运行的,哪怕多执行几遍也是单线程运行的,这样当然不会出问题呀!那什么场景下创建对象会报错呢?请继续往下看。
public static void Main(string[] args)
{
for (int i = 0; i < 20; i++)
{
TestClass.TestArrayListSingleThread();
}
}
public class TestClass
{
public static void TestArrayListSingleThread()
{
Hashtable hashTable = new Hashtable();
hashTable.Add("01", "01Value");
hashTable.Add("02", "02Value");
hashTable.Add("03", "03Value");
ArrayList arrayList = new ArrayList(hashTable.Values);
Console.WriteLine("hashTable.Count=" + hashTable.Count);
}
}
二、与常见逻辑相悖的多线程思维
实际上经过分析后,new ArrayList()报错的场景是下边这样的,把这个惊喜翻译出来就是:有61个线程在并发执行,其中60个线程在往hashTable中插入元素,1个线程利用hashTable创建ArrayList对象,这种并发程度报错的概率在90%以上。
public static void Main(string[] args)
{
for (int i = 0; i < 20; i++)
{
TestClass.TestArrayListMultiThread();
}
}
public class TestClass
{
public static void TestArrayListMultiThread()
{
Hashtable hashTable = new Hashtable();
hashTable.Add("01", "01Value");
hashTable.Add("02", "02Value");
hashTable.Add("03", "03Value");
//hashTable = Hashtable.Synchronized(hashTable);
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 100;
Parallel.Invoke(options,
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
#region 29遍相同的代码
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },//10
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },//10
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },//10
#endregion
() =>
{
ArrayList arrayList = null;
//lock (hashTable.SyncRoot)
//{
arrayList = new ArrayList(hashTable.Values);
Console.WriteLine("hashTable.Count=" + hashTable.Count);
//}
},
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
#region 29遍相同的代码
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },//10
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },//10
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); },
() => { hashTable.Add("for" + Guid.NewGuid(), "forValue"); }//10
#endregion
);
Console.WriteLine("执行完毕");
}
}
实际项目中也是单线程的写法,仅看代码我们会不自觉地陷入到单线程的思维中,因为一般定义和调用是分开的,不可能像我上边写的示例代码那样直接告诉你多线程的情况下会有问题,这需要我们自己总结分析,当单线程不会报错时跳出惯性思维,考虑一下会不会是异步、高并发导致了异常的发生,实际项目代码的具体截图如下:
现在只看自己写的代码已经找不出问题原因,那就去看源码吧,排查问题时首先要抱定一个信念"出了异常肯定有原因,只要深挖肯定能找到问题所在"。
三、源码分析
既然错误内容是“索引超出了数组界限”,那我们看源码的时候要重点关注数组相关的地方,下面从ArrayList的构造方法开始:
c.CopyTo(itemsToInsert, 0);中c是接口类型,要想继续跟下去需要知道c的实际类型,因为c是从new ArrayList(hashTable.Values)传过来的,实际上c就是hashTable.Values,我们只需要打断点看一下hashTable.Values的真实类型即可(System.Collections.Hashtable.ValueCollection)
下面以buckets的长度=3、array的长度=2为例进行推演:
i=3、--i>=0为true-->执行array.SetValue(lbuckets[2].val, 0);//正常执行
i=2、--i>=0为true-->执行array.SetValue(lbuckets[1].val, 1);//正常执行
i=1、--i>=0为true-->执行array.SetValue(lbuckets[0].val, 2);//2超出了array的最大索引导致报错
四、为什么是c.CopyTo(itemsToInsert, 0);出问题了呢
其实我并没有一眼就看出问题出在这里,而是经过测试后发现这个方法的报错与实际环境中的报错向吻合,因此判断这里是问题点。下面是实际场景中记录的异常日志截图
下面是测试SetValue()的异常截图,第二种情况与正式环境的异常相吻合,就这样找到了问题点
五、问题复现及解决方案
问题复现的代码就是【第二步】的代码,解决方法就是把代码中注释的内容放开。这个问题是用了挺长时间才分析出原因,一开始就没头绪,之后想到可以去看源码来找问题才慢慢找到了解决问题的途径,希望我的经历能对大家有所帮助,当真有用的话请点赞、评论、关注下,谢谢了。