一种隐蔽性较高的Java ConcurrentModificationException异常场景
前言
在使用Iterator遍历容器类的过程中,如果对容器的内容进行增加和删除,就会出现ConcurrentModificationException异常。该异常的分析和解决方案详见博文《Java ConcurrentModificationException 异常分析与解决方案》和《解决ArrayList的ConcurrentModificationException》。本文展示一种隐蔽性较高的ConcurrentModificationException异常场景,并给出解决方案。
问题代码
代码示例如下:
- public class ThreadTest {
- private static Map<Pattern, Integer> ITEM_MAP = null; //private static ConcurrentMap<Pattern, Integer> ITEM_MAP = null;
- private static int getPortLevel(String portName) {
- int level = 0;
- if(ITEM_MAP == null) {
- ITEM_MAP = new HashMap<Pattern, Integer>(); //ITEM_MAP = new ConcurrentHashMap<Pattern, Integer>();
- ITEM_MAP.put(Pattern.compile("^Cpos+|^Pos+"), 1);
- System.out.println(""+Thread.currentThread().getName()+": cur="+ITEM_MAP.size());
- ITEM_MAP.put(Pattern.compile("^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$"), 2);
- ITEM_MAP.put(Pattern.compile("^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$"), 3);
- ITEM_MAP.put(Pattern.compile("^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$"), 4);
- ITEM_MAP.put(Pattern.compile("^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$"), 5);
- ITEM_MAP.put(Pattern.compile("^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$"), 6);
- ITEM_MAP.put(Pattern.compile("^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$"), 7);
- ITEM_MAP.put(Pattern.compile("^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$"), 8);
- System.out.println(""+Thread.currentThread().getName()+": Map="+ITEM_MAP); //此句可能抛出ConcurrentModificationException异常
- }
- //ph1:1353986467,ph2:1100489929,equals=false
- //System.out.println("ph1:"+Pattern.compile("^Cpos+|^Pos+").hashCode()+",ph2:"+Pattern.compile("^Cpos+|^Pos+").hashCode()+
- // ",equals="+(Pattern.compile("^Cpos+|^Pos+").equals(Pattern.compile("^Cpos+|^Pos+"))));
- Iterator<Entry<Pattern, Integer>> iter = ITEM_MAP.entrySet().iterator();
- System.out.println(""+Thread.currentThread().getName()+": Map size="+ITEM_MAP.size());
- while(iter.hasNext()) {
- Entry<Pattern, Integer> entry = iter.next(); //此句可能抛出ConcurrentModificationException异常
- if(entry.getKey().matcher(portName).find()) {
- level = entry.getValue();
- break;
- }
- }
- return level;
- }
- public static void main(String[] args) {
- Thread thread1 = new Thread(){
- @Override
- public void run() {
- System.out.println(""+Thread.currentThread().getName()+", Value="+getPortLevel("400GE1/2/3"));
- };
- };
- Thread thread2 = new Thread(){
- @Override
- public void run() {
- System.out.println(""+Thread.currentThread().getName()+", Value="+getPortLevel("400GE1/2/3"));
- };
- };
- thread1.start();
- thread2.start();
- }
- }
可见,getPortLevel()内ITEM_MAP的初始化类似懒汉式单例,因此存在多线程问题。
场景分析
在多线程环境下,上述代码运行结果多种多样,例如:
1) 线程0调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程1紧随其后调用getPortLevel(),并跳过初始化分支(map!=null),开始遍历;此时ITEM_MAP内刚插入一个键值对,线程1遍历匹配不到,返回Value=0。
线程0初始化ITEM_MAP完毕,开始遍历并匹配成功,返回Value=8。
- Thread-0: cur=1
- Thread-1: Map size=1
- Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^Cpos+|^Pos+=1, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2}
- Thread-0: Map size=8
- Thread-1, Value=
- Thread-0, Value=
2) 线程0和线程1同时调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
Pattern.compile()会new一个Pattern对象并返回,而相同模式字符串编译后的Pattern对象hashcode不同且equals返回false,因此会插入"重复"的key(Map size=15)。
线程0和线程1初始化ITEM_MAP完毕,开始遍历并匹配成功,返回Value=8。
- Thread-0: cur=1
- Thread-1: cur=1
- Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7}
- Thread-1: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7}
- Thread-1: Map size=
- Thread-0: Map size=
- Thread-0, Value=8
- Thread-1, Value=
注意,HashMap会根据key对象的hashcode和equals方法判断key的重复性。因此,使用HashMap时一般要覆写key对象的hashcode和equals方法并确保其正确性,以免插入“重复”的key。
3) 线程0调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程1紧随其后调用getPortLevel(),并跳过初始化分支(map!=null),开始遍历;此时线程0正在向ITEM_MAP内插入键值对,因遍历期间条目数改变而触发ConcurrentModificationException。
线程0初始化ITEM_MAP完毕,开始遍历并匹配成功,返回Value=8。
- Thread-0: cur=1
- Thread-1: Map size=1
- Exception in thread "Thread-1" Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^Cpos+|^Pos+=1, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2}
- Thread-0: Map size=8
- java.util.ConcurrentModificationException
- at java.util.HashMap$HashIterator.nextNode(Unknown Source)
- at java.util.HashMap$EntryIterator.next(Unknown Source)
- at java.util.HashMap$EntryIterator.next(Unknown Source)
- Thread-0, Value=8
4) 线程0和线程1同时调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程0初始化ITEM_MAP完毕(Map size=11),开始遍历;此时线程1正在向ITEM_MAP内插入键值对,因此线程0触发ConcurrentModificationException。
线程1初始化ITEM_MAP完毕(Map size=15),开始遍历并匹配成功,返回Value=8。
- Thread-0: cur=1
- Thread-1: cur=1
- Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^Cpos+|^Pos+=1}
- Thread-0: Map size=11
- Thread-1: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7}
- Thread-1: Map size=15
- Thread-1, Value=8
- Exception in thread "Thread-0" java.util.ConcurrentModificationException
注意,两个线程先后执行getPortLevel()内ITEM_MAP = new HashMap<Pattern, Integer>()语句时,后者new出的对象会覆盖前者。此时很可能丢失前者已插入的键值对,导致两个线程打印的MAP条目数不同。
5) 线程0和线程1同时调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程0初始化ITEM_MAP完毕,打印ITEM_MAP内容(内含遍历操作);此时线程1正在向ITEM_MAP内插入键值对,因此线程0触发ConcurrentModificationException。
线程1初始化ITEM_MAP完毕(Map size=15),开始遍历并匹配成功,返回Value=8。
- Thread-0: cur=1
- Thread-1: cur=1
- Thread-1: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2}
- Thread-1: Map size=15
- Exception in thread "Thread-0" java.util.ConcurrentModificationException
- at java.util.HashMap$HashIterator.nextNode(Unknown Source)
- at java.util.HashMap$EntryIterator.next(Unknown Source)
- at java.util.HashMap$EntryIterator.next(Unknown Source)
- at java.util.AbstractMap.toString(Unknown Source)
- at java.lang.String.valueOf(Unknown Source)
- at java.lang.StringBuilder.append(Unknown Source)
- at ThreadTest.getPortLevel(ThreadTest.java:16)
- Thread-1, Value=8
6) 改用ConcurrentHashMap(修改点如原代码第2行和第6行注释所示)后,虽然可消除ConcurrentModificationException异常,但仍存在前述插入"重复"key或某线程漏匹配的问题。
例如:
- Thread-1: Map size=0
- Thread-1, Value=
- Thread-0: cur=1
- Thread-0: Map={^(100GE)([0-9/]+)|(100GE)([0−9/]+):([0−9/]+)|(100GE)([0−9/]+):([0−9/]+)=6, ^(40GE)([0-9/]+)|(40GE)([0−9/]+):([0−9/]+)|(40GE)([0−9/]+):([0−9/]+)=4, ^(400GE)([0-9/]+)|(400GE)([0−9/]+):([0−9/]+)|(400GE)([0−9/]+):([0−9/]+)=8, ^(200GE)([0-9/]+)|(200GE)([0−9/]+):([0−9/]+)|(200GE)([0−9/]+):([0−9/]+)=7, ^Cpos+|^Pos+=1, ^(GE)([0-9/]+)|(GE)([0−9/]+):([0−9/]+)|(GE)([0−9/]+):([0−9/]+)=2, ^(50GE)([0-9/]+)|(50GE)([0−9/]+):([0−9/]+)|(50GE)([0−9/]+):([0−9/]+)=5, ^(10GE)([0-9/]+)|(10GE)([0−9/]+):([0−9/]+)|(10GE)([0−9/]+):([0−9/]+)=3}
- Thread-0: Map size=8
- Thread-0, Value=
修改方案
以下提供一种修改方案:
将ITEM_MAP的初始化放在static语句块内:
- private static final Map<Pattern, Integer> ITEM_MAP = new HashMap<Pattern, Integer>();
- static {
- ITEM_MAP.put(Pattern.compile("^Cpos+|^Pos+"), 1);
- ITEM_MAP.put(Pattern.compile("^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$"), 2);
- ITEM_MAP.put(Pattern.compile("^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$"), 3);
- ITEM_MAP.put(Pattern.compile("^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$"), 4);
- ITEM_MAP.put(Pattern.compile("^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$"), 5);
- ITEM_MAP.put(Pattern.compile("^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$"), 6);
- ITEM_MAP.put(Pattern.compile("^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$"), 7);
- ITEM_MAP.put(Pattern.compile("^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$"), 8);
- }
- private static int getPortLevel(String portName) {
- int level = 0;
- Iterator<Entry<Pattern, Integer>> iter = ITEM_MAP.entrySet().iterator();
- while(iter.hasNext()) {
- Entry<Pattern, Integer> entry = iter.next();
- if(entry.getKey().matcher(portName).find()) {
- level = entry.getValue();
- break;
- }
- }
- return level;
- }
或者直接在声明时快速初始化:
- private static final Map<Pattern, Integer> ITEM_MAP = new LinkedHashMap<Pattern, Integer>() {
- { //可将常见类型的端口放在MAP前面,遍历时利用LinkedHashMap的有序性提高遍历速度
- put(Pattern.compile("^Cpos+|^Pos+"), 1);
- put(Pattern.compile("^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$"), 2);
- put(Pattern.compile("^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$"), 3);
- put(Pattern.compile("^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$"), 4);
- put(Pattern.compile("^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$"), 5);
- put(Pattern.compile("^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$"), 6);
- put(Pattern.compile("^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$"), 7);
- put(Pattern.compile("^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$"), 8);
- }
- };
注意,采用该方案时,应确保其他地方不会对ITEM_MAP进行增、删操作(仅靠final修饰无法保证这点)。若出现该情况,通常意味着深层次的设计缺陷,而试图在编码层面修复往往适得其反。例如,作者遇到的一种错误的写法示例如下(实际代码很复杂):
- private LinkedHashSet<String> nameSet;
- public LinkedHashSet<String> getNames() {
- System.out.println(""+Thread.currentThread().getName()+", nameSet="+nameSet);
- if(CollectionUtils.isEmpty(nameSet)) {
- nameSet = new LinkedHashSet<String>();
- nameSet.add("Jack");
- nameSet.add("Jame");
- }
- //return nameSet;
- //无论是new时以nameSet初始化还是new出对象后调用addAll(nameSet),均可能因为容器内部迭代而触发ConcurrentModificationException。
- //但本例可保证外部不会直接修改nameSet,所以此处复制对象是安全的。
- LinkedHashSet<String> names = new LinkedHashSet<String>(nameSet);
- System.out.println(""+Thread.currentThread().getName()+", names="+names);
- return names;
- }
- public void addNames() {
- getNames().add("Lucy");
- getNames().add("Beth");
- }
- public void showNames() {
- for(String name : getNames()) {
- System.out.println("This is "+name);
- }
- }
当线程0调用showNames()的同时,线程1在调用addNames(),就可能导致ConcurrentModificationException异常。当然,本例过于简单,很难真正地触发异常,仅作示例而已。
注意getNames()内复制nameSet对象的写法。该写法试图修复ConcurrentModificationException异常,但因为每次调用都会重新new对象,实际上addNames()无法将Lucy和Beth添加到名字表里!可见,这种试图修复少见异常的尝试反而导致严重的逻辑错误。
一种隐蔽性较高的Java ConcurrentModificationException异常场景的更多相关文章
- Java ConcurrentModificationException异常原因和解决方法
Java ConcurrentModificationException异常原因和解决方法 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会抛出java.u ...
- Java并发编程:Java ConcurrentModificationException异常原因和解决方法
Java ConcurrentModificationException异常原因和解决方法 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会抛出java.u ...
- 【转】Java ConcurrentModificationException异常原因和解决方法
原文网址:http://www.cnblogs.com/dolphin0520/p/3933551.html Java ConcurrentModificationException异常原因和解决方法 ...
- 9、Java ConcurrentModificationException异常原因和解决方法
Java ConcurrentModificationException异常原因和解决方法 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会抛出java.u ...
- Java ConcurrentModificationException 异常分析与解决方案
Java ConcurrentModificationException 异常分析与解决方案http://www.2cto.com/kf/201403/286536.html java.util.Co ...
- 【转】Java ConcurrentModificationException 异常分析与解决方案--还不错
原文网址:http://www.2cto.com/kf/201403/286536.html 一.单线程 1. 异常情况举例 只要抛出出现异常,可以肯定的是代码一定有错误的地方.先来看看都有哪些情况会 ...
- (转)Java ConcurrentModificationException异常原因和解决方法
转载自:http://www.cnblogs.com/dolphin0520/p/3933551.html 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同时对其进行修改就会 ...
- Java ConcurrentModificationException异常原因和解决方法(转)
摘自:http://www.cnblogs.com/dolphin0520/p/3933551.html#undefined 在前面一篇文章中提到,对Vector.ArrayList在迭代的时候如果同 ...
- 编写高质量java代码151个建议
http://blog.csdn.net/aishangyutian12/article/details/52699938 第一章 Java开发中通用的方法和准则 建议1:不要在常量和变量中出现易混 ...
随机推荐
- 将两个DataTable合并成一个DataTable
转载自 http://blog.csdn.net/wangxiaojia42121/article/details/53330464 谢谢 //两个结构一样的DT合并DataTable DataTab ...
- Set集合架构和常用实现类的源码分析以及实例应用
说明:Set的实现类都是基于Map来实现的(HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的). (01) Set 是继承于Collection的接口.它是一个不允许 ...
- Updating and Publishing a NuGet Package - Plus making NuGet packages smarter and avoiding source edits with WebActivator
I wrote a post a few days ago called "Creating a NuGet Package in 7 easy steps - Plus using NuG ...
- unity-Profiler调试Android的正确姿势(mumu模拟器)
1. 前置条件 安卓的相关环境 java.ant.sdk.ndk 什么的都装好(其实这里只需要 sdk 里面的 adb),配好 adb 工具的环境变量(意思就是 cmd 里直接输 adb 命令即可) ...
- .Net转Java.04.踩到switch的坑
今天线上有个NullPointerException 的异常,我翻了一下代码,抛异常的竟然是switch语句 我有种不祥的预感,本地做了实验 结果是 Java的switch如果传入null值,会抛出 ...
- NoSQL简单介绍
这里介绍一下如今经常使用的NoSQL以及各自的特点. NoSQL是2009年突然发展起来的.如今趋于稳定的状态,市场上也有了一些比較成熟的产品. 传统的关系型数据库为了保证通用性的设计而带来了功能复杂 ...
- Github超棒资源汇总
Awesome List 中文资源大全 经典编程书籍大全 免费的编程中文书籍索引 awesome-awesomeness-zh_CN https://github.com/jnv/lists awes ...
- [Algorithm] Fibonacci Sequence - Anatomy of recursion and space complexity analysis
For Fibonacci Sequence, the space complexity should be the O(logN), which is the height of tree. Che ...
- [android警告]AndroidManifest.xml警告 Not targeting the latest versions of Android
警告:Not targeting the latest versions of Android; compatibility modes apply.Consider testing and upda ...
- 采石厂管理系统V3.0版本上线(采石厂车辆出入管理系统,石厂开票系统)
新版系统包含老版所有功能,软件基础功能请点击查看<采石管理系统,采石厂车辆出入管理系统> 新增功能点 近期对采石厂管理系统进行了升级和完善,系统更加灵活好用,应用场景更加广泛.主要更新一下 ...