怎么把自己做的网站,wordpress去掉版本号,网站建设的方案书,永年做网站多少钱来源 | 悟空聊架构本篇主要内容如下#xff1a;本篇主要内容本篇所有示例代码已更新到 我的Github本篇文章已收纳到我的Java在线文档线程不安全之ArrayList集合框架有Map和Collection两大类#xff0c;Collection下面有List、Set、Queue。List 下面有 ArrayList、Vector、Lin… 来源 | 悟空聊架构本篇主要内容如下本篇主要内容本篇所有示例代码已更新到 我的Github本篇文章已收纳到我的Java在线文档线程不安全之ArrayList集合框架有Map和Collection两大类Collection下面有List、Set、Queue。List 下面有 ArrayList、Vector、LinkedList。如下图所示集合框架思维导图JUC并发包下的集合类Collections有Queue、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentMapJUC包下的Collections我们先来看看 ArrayList。1.1、ArrayList 的底层初始化操作首先我们来复习下ArrayList的使用下面是初始化一个ArrayList数组存放的是 Integer 类型的值。new ArrayListInteger();
那么底层做了什么操作呢1.2、ArrayList 的底层原理1.2.1 初始化数组/*** Constructs an empty list with an initial capacity of ten.*/
public ArrayList() {this.elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
创建了一个空数组容量为0根据官方的英文注释这里容量应该为10但其实是0后续会讲到为什么不是10。1.2.2 ArrayList 的 add 操作public boolean add(E e) {ensureCapacityInternal(size 1); // Increments modCount!!elementData[size] e;return true;
}
重点是这一步elementData[size] e; size 和 elementData[xx]e这两个操作都不是原子操作不可分割的一个或一系列操作要么都成功执行要么都不执行。1.2.3 ArrayList 扩容源码解析1执行 add 操作时会先确认是否超过数组大小。ensureCapacityInternal(size 1);
ensureCapacityInternal方法2计算数组的当前容量 calculateCapacity。private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
minCapacity : 值为1elementData代表当前数组我们先看 ensureCapacityInternal 调用的 ensureCapacityInternal 方法calculateCapacity(elementData, minCapacity)
calculateCapacity方法如下private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}
elementData代表当前数组添加第一个元素时elementData 等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组。minCapacity等于1DEFAULT_CAPACITY等于10返回 Math.max(DEFAULT_CAPACITY, minCapacity) 10。小结所以第一次添加元素时计算数组的大小为10。3确定当前容量 ensureExplicitCapacity。ensureExplicitCapacity方法minCapacity 10elementData.length0小结因minCapacity elementData.length所以进行第一次扩容调用grow()方法从0扩大到10。4调用 grow 方法。grow方法oldCapacity0newCapacity10。然后执行 elementData Arrays.copyOf(elementData, newCapacity);将当前数组和容量大小进行数组拷贝操作赋值给elementData。数组的容量设置为10。elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值将会不一样。5然后将元素赋值给数组第一个元素且size自增1。elementData[size] e;
6添加第二个元素时传给ensureCapacityInternal的是2。ensureCapacityInternal(size 1)
size1size127第二次添加元素时执行 calculateCapacity。markelementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值不相等所以直接返回2。8第二次添加元素时执行 ensureExplicitCapacity。因minCapacity等于2小于当前数组的长度10所以不进行扩容不执行grow方法。mark9将第二个元素添加到数组中size自增1。elementData[size] e
10当添加第11个元素时调用grow方法进行扩容。markminCapacity11 elementData.length10调用grow方法。11扩容1.5倍。int newCapacity oldCapacity (oldCapacity 1);
oldCapacity10先换算成二级制1010然后右移一位变成0101对应十进制5所以newCapacity10515扩容1.5倍后是15。扩容1.5倍12小结1.ArrayList初始化为一个空数组。2.ArrayList的Add操作不是线程安全的。3.ArrayList添加第一个元素时数组的容量设置为10。4.当ArrayList数组超过当前容量时扩容至1.5倍遇到计算结果为小数的向下取整第一次扩容后容量为15第二次扩容至22...5.ArrayList在第一次和扩容后都会对数组进行拷贝调用Arrays.copyOf方法。1.3、ArrayList单线程环境是否安全场景我们通过一个添加积木的例子来说明单线程下ArrayList是线程安全的。将 积木 三角形A、四边形B、五边形C、六边形D、五角星E依次添加到一个盒子中盒子中共有5个方格每一个方格可以放一个积木。ArrayList单线程下添加元素代码实现1这次我们用新的积木类 BuildingBlockWithName这个积木类可以传形状shape和名字name/*** 积木类* author: 悟空聊架构* create: 2020-08-27*/
class BuildingBlockWithName {String shape;String name;public BuildingBlockWithName(String shape, String name) {this.shape shape;this.name name;}Overridepublic String toString() {return BuildingBlockWithName{ shape shape ,name name };}
}
2初始化一个ArrayListArrayListBuildingBlock arrayList new ArrayList();
3依次添加三角形A、四边形B、五边形C、六边形D、五角星E。arrayList.add(new BuildingBlockWithName(三角形, A));
arrayList.add(new BuildingBlockWithName(四边形, B));
arrayList.add(new BuildingBlockWithName(五边形, C));
arrayList.add(new BuildingBlockWithName(六边形, D));
arrayList.add(new BuildingBlockWithName(五角星, E));
4验证arrayList中元素的内容和顺序是否和添加的一致BuildingBlockWithName{shape三角形,nameA}
BuildingBlockWithName{shape四边形,nameB}
BuildingBlockWithName{shape五边形,nameC}
BuildingBlockWithName{shape六边形,nameD}
BuildingBlockWithName{shape五角星,nameE}
我们看到结果确实是一致的。小结单线程环境中ArrayList是线程安全的。1.4、多线程下ArrayList是不安全的场景如下20个线程随机往ArrayList添加一个任意形状的积木。多线程场景往数组存放元素1代码实现20个线程往数组中随机存放一个积木。多线程下ArrayList是不安全的2打印结果程序开始运行后每个线程只存放一个随机的积木。打印结果数组中会不断存放积木多个线程会争抢数组的存放资格在存放过程中会抛出一个异常: ConcurrentModificationException并行修改异常。Exception in thread 10 Exception in thread 13 java.util.ConcurrentModificationException
mark这个就是常见的并发异常java.util.ConcurrentModificationException1.5 那如何解决 ArrayList 线程不安全问题呢有如下方案用Vector代替ArrayList用Collections.synchronized(new ArrayList())CopyOnWriteArrayList1.6 Vector 是保证线程安全的下面就来分析vector的源码。1.6.1 初始化 Vector初始化容量为10public Vector() {this(10);
}
1.6.2 Add 操作是线程安全的Add方法加了synchronized来保证add操作是线程安全的保证可见性、原子性、有序性对这几个概念有不懂的可以看下之前的写的文章-》 反制面试官 | 14张原理图 | 再也不怕被问 volatile!Add方法加了synchronized1.6.3 Vector 扩容至2倍int newCapacity oldCapacity ((capacityIncrement 0) ? capacityIncrement : oldCapacity);
容量扩容至2倍注意capacityIncrement 在初始化的时候可以传值不传则默认为0。如果传了则第一次扩容时为设置的oldCapacitycapacityIncrement第二次扩容时扩大1倍。缺点虽然保证了线程安全但因为加了排斥锁synchronized会造成阻塞所以性能降低。1.6.4 用积木模拟Vector的add操作vector的add操作当往vector存放元素时给盒子加了一个锁只有一个人可以存放积木放完后释放锁放第二元素时再进行加锁依次往复进行。1.7 使用 Collections.synchronizedList 保证线程安全我们可以使用Collections.synchronizedList方法来封装一个ArrayList。ListObject arrayList Collections.synchronizedList(new ArrayList());
为什么这样封装后就是线程安全的源码解析因为Collections.synchronizedList封装后的listlist的所有操作方法都是带synchronized关键字的除iterator()之外相当于所有操作都会进行加锁所以使用它是线程安全的除迭代数组之外。加锁mark注意当迭代数组时需要手动做同步。官方示例如下synchronized (list) {Iterator i list.iterator(); // Must be in synchronized blockwhile (i.hasNext())foo(i.next());
}
1.8 使用 CopyOnWriteArrayList 保证线程安全1.8.1 CopyOnWriteArrayList思想Copy on write写时复制一种读写分离的思想。写操作添加元素时不直接往当前容器添加而是先拷贝一份数组在新的数组中添加元素后在将原容器的引用指向新的容器。因为数组时用volatile关键字修饰的所以当array重新赋值后其他线程可以立即知道volatile的可见性。读操作读取数组时读老的数组不需要加锁。读写分离写操作是copy了一份新的数组进行写读操作是读老的数组所以是读写分离。1.8.2 使用方式CopyOnWriteArrayListBuildingBlockWithName arrayList new CopyOnWriteArrayList();
1.8.3 底层源码分析CopyOnWriteArrayList的add方法分析add的流程先定义了一个可重入锁 ReentrantLock。添加元素前先获取锁lock.lock()。添加元素时先拷贝当前数组 Arrays.copyOf。添加元素时扩容1len 1。添加元素后将数组引用指向新加了元素后的数组setArray(newElements)。为什么数组重新赋值后其他线程可以立即知道因为这里的数组是用volatile修饰的哇又是volatile这个关键字真妙^_^ private transient volatile Object[] array;
1.8.4 ReentrantLock 和synchronized的区别划重点相同点1.都是用来协调多线程对共享对象、变量的访问。2.都是可重入锁同一线程可以多次获得同一个锁。3.都保证了可见性和互斥性。不同点1.ReentrantLock 显示的获得、释放锁 synchronized 隐式获得释放锁。2.ReentrantLock 可响应中断 synchronized 是不可以响应中断的为处理锁的不可用性提供了更高的灵活性。3.ReentrantLock 是 API 级别的 synchronized 是 JVM 级别的。4.ReentrantLock 可以实现公平锁、非公平锁。5.ReentrantLock 通过 Condition 可以绑定多个条件。6.底层实现不一样 synchronized 是同步阻塞使用的是悲观并发策略 lock 是同步非阻塞采用的是乐观并发策略。1.8.5 Lock和synchronized的区别1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别。2.Lock 是一个接口而 synchronized 是 Java 中的关键字 synchronized 是内置的语言实现。3.synchronized 在发生异常时会自动释放线程占有的锁因此不会导致死锁现象发生而 Lock 在发生异常时如果没有主动通过 unLock()去释放锁则很可能造成死锁现象因此使用 Lock 时需要在 finally 块中释放锁。4.Lock 可以让等待锁的线程响应中断而 synchronized 却不行使用 synchronized 时等待的线程会一直等待下去不能够响应中断。5.通过 Lock 可以知道有没有成功获取锁而 synchronized 却无法办到。6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。线程不安全之 HashSet有了前面大篇幅的讲解 ArrayList 的线程不安全以及如何使用其他方式来保证线程安全现在讲HashSet应该更容易理解一些。2.1 HashSet的用法用法如下 SetBuildingBlockWithName Set new HashSet();
set.add(a);
初始容量10负载因子0.75当元素个数达到容量的75%启动扩容2.2 HashSet的底层原理 public HashSet() {map new HashMap();
}
底层用的还是HashMap()。考点为什么HashSet的add操作只用传一个参数value)而HashMap需要传两个参数key和value)2.3 HashSet的add操作private static final Object PRESENT new Object();public boolean add(E e) {return map.put(e, PRESENT)null;
}
考点回答 因为HashSet的add操作中key等于传的value值而value是PRESENTPRESENT是new Object();所以传给map的是 keye, valuenew Object。Hash只关心key不考虑value。为什么HashSet不安全底层add操作不保证可见性、原子性。所以不是线程安全的。2.4 如何保证线程安全1.使用 Collections.synchronizedSetSetBuildingBlockWithName set Collections.synchronizedSet(new HashSet());2.使用 CopyOnWriteArraySetCopyOnWriteArraySetBuildingBlockWithName set new CopyOnWriteArraySet();
2.5 CopyOnWriteArraySet 的底层还是使用的是 CopyOnWriteArrayListpublic CopyOnWriteArraySet() {al new CopyOnWriteArrayListE();
}线程不安全之HashMap3.1 HashMap 的使用同理HashMap和HashSet一样在多线程环境下也是线程不安全的。MapString, BuildingBlockWithName map new HashMap();
map.put(A, new BuildingBlockWithName(三角形, A));
3.2 HashMap线程不安全解决方案1.Collections.synchronizedMapMapString, BuildingBlockWithName map2 Collections.synchronizedMap(new HashMap());
2.ConcurrentHashMapConcurrentHashMapString, BuildingBlockWithName set3 new ConcurrentHashMap();
3.3 ConcurrentHashMap原理ConcurrentHashMap它内部细分了若干个小的 HashMap称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项并不是将整个 HashMap 加锁而是首先根据 hashcode 得到该表项应该存放在哪个段中然后对该段加锁并完成 put 操作。在多线程环境中如果多个线程同时进行put操作只要被加入的表项不存放在同一个段中则线程间可以做到真正的并行。其他的集合类LinkedList: 线程不安全同ArrayListTreeSet线程不安全同HashSetLinkedHashSet线程不安全同HashSetTreeMap同HashMap线程不安全HashTable线程安全。总结本篇第一个部分详细讲述了ArrayList集合的底层扩容原理演示了ArrayList的线程不安全会导致抛出并发修改异常。然后通过源码解析的方式讲解了三种方式来保证线程安全Vector是通过在add等方法前加synchronized来保证线程安全。Collections.synchronized()是通过包装数组在数组的操作方法前加synchronized来保证线程安全。CopyOnWriteArrayList通过写时复制来保证线程安全的。第二部分讲解了HashSet的线程不安全性通过两种方式保证线程安全Collections.synchronizedSetCopyOnWriteArraySet第三部分讲解了HashMap的线程不安全性通过两种方式保证线程安全Collections.synchronizedMapConcurrentHashMap另外在讲解的过程中也详细对比了ReentrantLock和synchronized及Lock和synchronized的区别。彩蛋聪明的你一定发现集合里面还漏掉了一个重要的东西那就是Queue。期待后续么更多阅读推荐云起云涌PaaS 体系架构与运维系统上云实践该买哪家二手手机呢程序员爬取京东告诉你17 年安全界老兵专注打造容器安全能行吗字节跳动斩获支付牌照欲建金融帝国技术实力配得上野心吗腾讯微博即将关停十年了你用过吗