并发编程(二):非线程安全集合类
来源:互联网 发布:网络电视直播pc版 编辑:程序博客网 时间:2024/06/09 17:13
前言
Java集合时所讲的ArrayList 、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都线程不安全的,当多个并发线程向这些集合中存取元素时,就可能会破坏这些集合的完整性。那么究竟是在什么情况下才会出现问题呢?线程安全就是说多线程访问同一代码(对象、变量等),不会产生不确定的结果;
线程不安全的集合类
ArrayList:
package 线程不安全;import java.util.ArrayList;import java.util.List;public class ArrayListInThread implements Runnable { //线程不安全 private List<String> threadList = new ArrayList<String>(); //线程安全 //private List<String> threadList = Collections.synchronizedList(new ArrayList<String>()); @Override public void run() { try { Thread.sleep(10); }catch (InterruptedException e){ e.printStackTrace(); } //把当前线程名称加入list中 threadList.add(Thread.currentThread().getName()); } public static void main(String[] args) throws InterruptedException{ ArrayListInThread listThread = new ArrayListInThread(); for(int i = 0; i < 10; i++){ Thread thread = new Thread(listThread, String.valueOf(i)); thread.start(); } //等待子线程执行完 Thread.sleep(2000); System.out.println(listThread.threadList.size()); //输出list中的值 for(int i = 0; i < listThread.threadList.size(); i++){ if(listThread.threadList.get(i) == null){ System.out.println();; } System.out.print(listThread.threadList.get(i) + " "); } }}
结果一:
9null null 0 2 1 6 7 8 9
结果二:
抛出异常:ArrayIndexOutofBoundsException异常;
现象:出现null值;
出现输出不全的现象;
抛出异常;
原因:
ArrayList中的add方法:
//添加元素e public boolean add(E e) { // 确定ArrayList的容量大小 ensureCapacity(size + 1); // Increments modCount!! // 添加e到ArrayList中 elementData[size++] = e; return true; } // 确定ArrarList的容量。 // 若ArrayList的容量不足以容纳当前的全部元素,设置 新的容量=“(原始容量x3)/2 + 1” public void ensureCapacity(int minCapacity) { // 将“修改统计数”+1,该变量主要是用来实现fail-fast机制的 modCount++; int oldCapacity = elementData.length; // 若当前容量不足以容纳当前的元素个数,设置 新的容量=“(原始容量x3)/2 + 1” if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3)/2 + 1; //如果还不够,则直接将minCapacity设置为当前容量 if (newCapacity < minCapacity) newCapacity = minCapacity; elementData = Arrays.copyOf(elementData, newCapacity); } }
赋值语句为:elementData[size++] = e,这条语句可拆分为两条: 1. elementData[size] = e; 2. size ++; 假设A线程执行完第一条语句时,CPU暂停执行A线程转而去执行B线程,此时ArrayList的size并没有加一,这时在ArrayList中B线程就会覆盖掉A线程赋的值,而此时,A线程和B线程先后执行size++,便会出现值为null的情况; 至于结果中出现的ArrayIndexOutOfBoundsException异常,则是A线程在执行ensureCapacity(size+1)后没有继续执行,此时恰好minCapacity等于oldCapacity,B线程再去执行,同样由于minCapacity等于oldCapacity,ArrayList并没有增加长度,B线程可以继续执行赋值(elementData[size] = e)并size ++也执行了,此时,CPU又去执行A线程的赋值操作,由于size值加了1,size值大于了ArrayList的最大长度, 因此便出现了ArrayIndexOutOfBoundsException异常。
LinkedList:
Java中LinkedList是线程不安全的,在多线程程序中有多个线程访问LinkedList的话会抛出ConcurrentModificationException;另外JDK代码里,ListItr的add(), next(), previous(), remove(), set()方法都会跑出ConcurrentModificationException。
LinkedList的底层方法:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
代码中,modCount记录了LinkedList结构被修改的次数。Iterator初始化时,expectedModCount=modCount。任何通过Iterator修改LinkedList结构的行为都会同时更新expectedModCount和modCount,使这两个值相等。
通过LinkedList对象修改其结构的方法只更新modCount。所以假设有两个线程A和B。A通过Iterator遍历并修改LinkedList,而B,与此同时,通过对象修改其结构,造成modCount加了两次,而expectedModCount只做了一次修改,形成modCount != expectedModCount;那么Iterator的相关方法就会抛出异常。这是相对容易发现的由线程竞争造成的错误。
HashSet:
测试代码:
想要实现的效果:
创建两个线程,共享一个target,这样共享线程内的实例变量,输出结果应该是set中有5000个整数;
package 线程不安全;import java.util.HashSet;import java.util.Set;public class TestHashSet implements Runnable{ // 实现Runnable 让该集合能被多个线程访问 Set<Integer> set = new HashSet<Integer>(); // 线程的执行就是插入5000个整数 @Override public void run() { for (int i = 0;i < 5000;i ++) { set.add(i); } } public static void main(String[] args){ TestHashSet run2 = new TestHashSet(); // 实例化两个线程 Thread t6 = new Thread(run2); Thread t7 = new Thread(run2); // 启动两个线程 t6.start(); t7.start(); // 当前线程等待加入到调用线程后 try { t6.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { t7.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } // 打印出集合的size System.out.println(run2.set.size()); }}
结果一:
结果二:
现象:好多结果都不是预想的5000;
分析原因:
打印结果大部分出现大于5000的情况。这就出现了之前提到的情况,证明了HashSet不是线程安全的类。 其实查看源代码发现HashSet内部维护数据的采用的是HashMap,根本原因是HashMap不是线程安全的类。导致了HashSet的非线程安全。
TreeSet:
TreeSet 底层是通过 TreeMap 来实现的(如同HashSet底层是是通过HashMap来实现的一样);
HashMap:
测试Demo :两个线程同时往声明的hashmap中存储数据;线程安全下,所有的map的key==value。
package 线程不安全;import java.util.HashMap;public class TestHashMap { public static final HashMap<String, String> firstHashMap=new HashMap<String, String>(); public static void main(String[] args) throws InterruptedException { //线程一 Thread t1=new Thread(){ public void run() { for(int i=0;i<25;i++){ firstHashMap.put(String.valueOf(i), String.valueOf(i)); } } }; //线程二 Thread t2=new Thread(){ public void run() { for(int j=25;j<50;j++){ firstHashMap.put(String.valueOf(j), String.valueOf(j)); } } }; t1.start(); t2.start(); //主线程休眠1秒钟,以便t1和t2两个线程将firstHashMap填装完毕。 Thread.currentThread().sleep(1000); for(int l=0;l<50;l++){ //如果key和value不同,说明在两个线程put的过程中出现异常。 if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){ System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l))); } } }}
结果:
经过多次测试后,发现如图:
分析:
HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,而在rehash的时候,如果有多个线程访问,就会容易导致出错。
通过查看HashMap底层的实现:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
其中addEntry()方法:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
从代码中,可以看到,如果发现哈希表的大小超过阀值threshold,就会调用resize方法,扩大容量为原来的两倍,而扩大容量的做法是新建一个Entry[]:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
结论:两个线程同时遇到HashMap的扩容(Rehash)情况下,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。
TreeMap:
测试Demo:如同HashMap一般,填充数据;
’`package 线程不安全;
import java.util.HashMap;import java.util.TreeMap;public class TestHashMap { // public static final HashMap<String, String> firstHashMap=new HashMap<String, String>(); public static final TreeMap<String, String> firstHashMap=new TreeMap<String, String>(); public static void main(String[] args) throws InterruptedException { //线程一 Thread t1=new Thread(){ public void run() { for(int i=0;i<25;i++){ firstHashMap.put(String.valueOf(i), String.valueOf(i)); } } }; //线程二 Thread t2=new Thread(){ public void run() { for(int j=25;j<50;j++){ firstHashMap.put(String.valueOf(j), String.valueOf(j)); } } }; t1.start(); t2.start(); //主线程休眠1秒钟,以便t1和t2两个线程将firstHashMap填装完毕。 Thread.currentThread().sleep(1000); for(int l=0;l<50;l++){ //如果key和value不同,说明在两个线程put的过程中出现异常。 if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){ System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l))); System.out.println("线程不安全!啊啊啊啊"); }else{ System.out.println("线程安全!"); } } }}
结果:
TreeMap的put方法的底层:
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; }}
分析:如HashMap一般,TreeMap的put方法中调用了Entry()方法,而且是新建Entry();
总结
在Java里,线程安全一般体现在两个方面:
1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable
(后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。
2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。
措施
如果程序中有多个线程可能访问这些集合,就可以用Collections提供的类方法,它们可以把这些集合包装成线程安全的集合。例如:
Collection synchronizedCollection(Collection c):返回指定collection对应的线程安全的Collection。
Static List synchronizedList(List list):返回指定List对象对应的线程安全的List对象;
Static Set synchronizedSet(Set s):返回指定set对象对应的线程安全的Set对象;等方法;
例如:
1.
//使用Collection的synchronizedMap方法将一个普通的HashMap包装成线程安全的类
HashMap m=Collection.synchronizedMap(new HashMap());
2.
list list =Collections.synchronizedList(new ArrayList)来创建一个ArrayList对象。
参考资料:
http://blog.csdn.net/zhangxin961304090/article/details/46804065
http://blog.csdn.net/zhouxinhong/article/details/7361233
http://blog.csdn.net/micro_hz/article/details/51839246
http://blog.csdn.net/qq991029781/article/details/50930209
- 并发编程(二):非线程安全集合类
- 并发编程(7)线程安全与非线程安全/同步与非同步
- java并发编程实战(二)—线程安全
- 如何在并发场景下,使用非线程安全的集合类
- [并发并行]_[Object-C]_[使用NSMutableArray等非线程安全集合类的注意事项]
- 线程安全(多线程与并发)与集合类
- 线程安全的并发集合类
- 线程安全的集合类(java并发编程第5章)
- JAVA并发编程(三)设计线程安全的类
- 《Java并发编程实战》读书笔记二:构建线程安全
- Java并发编程(二)--java线程安全的一些基础
- 线程并发工具--线程安全集合
- 线程安全与并发安全探究(二)
- Java并发编程实践笔记(二)——chapter1(线程安全)
- 探索并发编程(二)------写线程安全的Java代码
- 探索并发编程(二)------ 写线程安全的Java代码
- 探索并发编程(二)------写线程安全的Java代码
- 探索并发编程(二)------写线程安全的Java代码
- Ext JS 继承
- SpringBoot20-springboot的Web开发-WebSocket
- Xilinx Altera FPGA中的逻辑资源(Slices VS LE)比较 前言 经常有朋友会问我,“我这个方案是用A家的FPGA还是X家的FPGA呢?他们的容量够不够呢?他们的容量怎么比较
- html5新特性:利用history的pushState等方法来解决使用ajax导致页面后退和前进的问题
- 平时学习时的记录||code blocks中“替换”的方法
- 并发编程(二):非线程安全集合类
- hdu 2846 Repository
- 基于mini2451开发板的裸机开发-电子相册
- 一周搞定9轴MPU9250(无华)(2)--STM32CUBEMX软件学习
- N个数依次入栈,出栈顺序有多少种
- 笔记本或台式机充当无线路由器的方法
- Hadoop-Intellij-Plugin 项目简介
- Others10_玩转信用卡之信用卡黑话
- 14. Longest Common Prefix