From 330f24b7abf1cb8ff1c31a6c4e52fa952a80432c Mon Sep 17 00:00:00 2001 From: panchaoyuan Date: Fri, 3 Jan 2020 16:49:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=80=81K=E7=AC=AC=E5=9B=9B=E5=91=A8?= =?UTF-8?q?=E4=BD=9C=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6\237\347\220\206\345\210\206\346\236\220" | 43 +++++++++++++++++++ ...2\220\347\240\201\350\247\243\346\236\220" | 27 ++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 "week_04/49/ConcurrentHashMap\346\272\220\347\240\201\345\222\214\345\216\237\347\220\206\345\210\206\346\236\220" create mode 100644 "week_04/49/CopyOnWriteArrayList\346\272\220\347\240\201\350\247\243\346\236\220" diff --git "a/week_04/49/ConcurrentHashMap\346\272\220\347\240\201\345\222\214\345\216\237\347\220\206\345\210\206\346\236\220" "b/week_04/49/ConcurrentHashMap\346\272\220\347\240\201\345\222\214\345\216\237\347\220\206\345\210\206\346\236\220" new file mode 100644 index 0000000..df3fd7c --- /dev/null +++ "b/week_04/49/ConcurrentHashMap\346\272\220\347\240\201\345\222\214\345\216\237\347\220\206\345\210\206\346\236\220" @@ -0,0 +1,43 @@ +ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现。 + +哈希表是中非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理 +ConcurrentHashMap、HashMap和HashTable的区别: +1、HashMap :HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。 +2、HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是①.HashTable不允许key和value为null;②.HashTable是线程安全的。 +但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。 +3、ConcurrentHashMap则可以支持并发的读写。跟1.7版本相比,1.8版本又有了很大的变化,已经抛弃了Segment的概念,虽然源码里面还保留了,也只是为了兼容性的考虑。 + +hash table虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 +- Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。 +- ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处 是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分 数据 + +重要的成员变量和类介绍: +1、table:默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。 +2、nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。 +3、sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。 +-1 代表table正在初始化 +-N 表示有N-1个线程正在进行扩容操作 +其余情况: +①、如果table未初始化,表示table需要初始化的大小。 +②、如果table初始化完成,表示table的容量,默认是table大小的0.75倍,用这个公式算0.75(n - (n >>> 2))。 +4、Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。 +5、ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动 +6、TreeBin:用作树的头结点,只存储root和first节点,不存储节点的key、value值。 +7、Node:这是构成每个元素的基本类 + +重要的方法介绍: +1、put(K key, V value):直接调用putVal(K key, V value, boolean onlyIfAbsent)方法, +假设table已经初始化完成,put操作采用CAS+synchronized实现并发插入或更新操作: +- 当前bucket为空时,使用CAS操作,将Node放入对应的bucket中。 +- 出现hash冲突,则采用synchronized关键字。倘若当前hash对应的节点是链表的头节点,遍历链表,若找到对应的node节点,则修改node节点的val,否则在链表末尾添加node节点;倘若当前节点是红黑树的根节点,在树结构上遍历元素,更新或增加节点。 +- 倘若当前map正在扩容f.hash == MOVED, 则跟其他线程一起进行扩容 + +2、get(Object key):读取操作,不需要同步控制 +1)空tab,直接返回null +2)计算hash值,找到相应的bucket位置,为node节点直接返回,否则返回null + +3、clear():清空map +遍历tab中每一个bucket, +1)当前bucket正在扩容,先协助扩容 +2)给当前bucket上锁,删除元素 +3)更新map的size diff --git "a/week_04/49/CopyOnWriteArrayList\346\272\220\347\240\201\350\247\243\346\236\220" "b/week_04/49/CopyOnWriteArrayList\346\272\220\347\240\201\350\247\243\346\236\220" new file mode 100644 index 0000000..4cecde0 --- /dev/null +++ "b/week_04/49/CopyOnWriteArrayList\346\272\220\347\240\201\350\247\243\346\236\220" @@ -0,0 +1,27 @@ +CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝数组(快照)上进行的,也就是写时拷贝策略, +读操作时无锁的ArrayList。并发包中并发List只有CopyOnWriteArrayList这一个。 +每个CopyOnWriteArrayList对象里面有一个array数组对象用来存放具体元素,ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改, +这里ReentrantLock是独占锁,同时只有一个线程可以获取。 + +方法介绍: +1、无参构造函数CopyOnWriteArrayList():创建了一个0个元素的数组。设置一个容量为0的Object[];ArrayList会创造一个容量为10的Object[]。 + +2、add(E e)方法介绍:在数组末尾添加元素。 +1)获取锁 +2)上锁 +3)获取旧数组及其长度 +4)创建新数组,容量为旧数组长度+1,将旧数组拷贝到新数组 +5)将要增加的元素加入到新数组的末尾,设置全局array为新数组 +CopyOnWriteArrayList中添加元素函数有add(E e) , add(int index , E element) ,addIfAbsent(E e) , addAllAbsent(Collection c) 等操作,原理一致。 + +3、get(int index):根据下标获取元素,获取不需要加锁。 +1)获取数组array +2)根据索引获取元素 + +4、set(int index, E element):修改指定元素。 +首先获取了独占锁控制了其他线程对array数组的修改,然后获取当前数组,并调用get方法获取指定位置元素。 +如果指定的位置元素与新值不一致则创建新数组并拷贝元素,在新数组上修改指定位置元素值并设置新数组到array。 +如果指定位置元素与新值一样,则为了保障volatile语义,还是需要重新设置下array,虽然array内容并没有改变。 + +CopyOnWriteArrayList使用写时拷贝的策略来保证list的一致性,而获取-拷贝-写入 三步并不是原子性的,所以在修改增删改的过程中都是用了独占锁, +并保证了同时只有一个线程才能对list数组进行修改。另外CopyOnWriteArrayList提供了弱一致性的迭代器,保证在获取迭代器后,其他线程对list的修改该不可见,迭代器遍历时候的数组是获取迭代器时候的一个快照 -- Gitee From d06a7ecdc67e38ef29fe28580f0bb8f2fb3081b2 Mon Sep 17 00:00:00 2001 From: laok <244704761@qq.com> Date: Sun, 5 Jan 2020 15:07:36 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E8=80=81K=E7=AC=AC=E5=9B=9B=E5=91=A8?= =?UTF-8?q?=E4=BD=9C=E4=B8=9A=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2\220\347\240\201\350\247\243\346\236\220" | 42 +++++++++++++++++++ ...6\237\347\220\206\345\210\206\346\236\220" | 25 +++++++++++ ...6\237\347\220\206\345\210\206\346\236\220" | 31 ++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 "week_04/49/ArrayBlockingQueue\346\272\220\347\240\201\350\247\243\346\236\220" create mode 100644 "week_04/49/ConcurrentLinkedQueue\345\216\237\347\220\206\345\210\206\346\236\220" create mode 100644 "week_04/49/DelayQueue\345\216\237\347\220\206\345\210\206\346\236\220" diff --git "a/week_04/49/ArrayBlockingQueue\346\272\220\347\240\201\350\247\243\346\236\220" "b/week_04/49/ArrayBlockingQueue\346\272\220\347\240\201\350\247\243\346\236\220" new file mode 100644 index 0000000..c1e32fe --- /dev/null +++ "b/week_04/49/ArrayBlockingQueue\346\272\220\347\240\201\350\247\243\346\236\220" @@ -0,0 +1,42 @@ +ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。 +底层以数组的形式保存数据(实际上可看作一个循环数组)。常用的操作包括 add ,offer,put,remove,poll,take,peek。 +ArrayBlockingQueue内部维护看一个数组,其内的方法别加了ReentrantLock锁来保证线程安全。 + +阻塞队列其实就是生产者-消费者模型中的容器。当生产者往队列中添加元素时,如果队列已经满了,生产者所在的线程就会阻塞,直到消费者取元素时 notify 它; +消费者去队列中取元素时,如果队列中是空的,消费者所在的线程就会阻塞,直到生产者放入元素 notify 它。具体到 Java 中,使用 BlockingQueue 接口表示阻塞队列。 + +重要属性介绍: +1、final Object[] items;//底层存储元素的数组。为final说明一旦初始化,容量不可变,所以是有界的。 + +2、int takeIndex;//下一个take, poll, peek or remove操作的index位置 + +3、int putIndex;//下一个put, offer, or add操作的index位置 + +4、final ReentrantLock lock;//用于并发控制:使用经典的双Condition算法 + +5、private final Condition notEmpty;//获取操作等待条件 + +6、private final Condition notFull;//插入操作等待条件 + + +重要方法介绍: +1、构造函数ArrayBlockingQueue(int capacity):默认的构造函数只指定了队列的容量capacity,设置为非公平的线程访问策略。 +2、构造函数 ArrayBlockingQueue(int capacity, boolean fair):使用 ReentrantLock 创建了 2 个 Condition 锁。 +3、offer(E e): +①判断添加的数据是否为空 +②添加重入锁 +③判断队列长度,如果队列长度等于数组长度,表示满了直接返回 false +④否则,直接调用 enqueue 将元素添加到队列中 +4、take():take 方法是一种阻塞获取队列中元素的方法. +它的实现原理很简单,有就删除没有就阻塞,注意这个阻塞是可以中断的,如果队列没有数据那么就加入 notEmpty条件队列等待(有数据就直接取走,方法结束),如果有新的put 线程添加了数据,那么 put 操作将会唤醒 take 线程,执行 take 操作 + +总结: +BlockingQueue接口提供了3个添加元素方法: +add:添加元素到队列里,添加成功返回true,如果队列已满则抛出IllegalStateException异常。不常用。 +offer:添加元素到队列里,添加成功返回true,如果队列已满添加失败,返回false +put:添加元素到队列里,添加成功返回true,如果队列已满则阻塞直到队列可用 + +同时,BlockingQueue接口提供了3个获取(并删除)元素的方法。 +remove:移除一个指定元素. +poll:返回队列头部元素。队列为空时,返回null +take:队列为空时,会阻塞直到有数据加入到队列中 \ No newline at end of file diff --git "a/week_04/49/ConcurrentLinkedQueue\345\216\237\347\220\206\345\210\206\346\236\220" "b/week_04/49/ConcurrentLinkedQueue\345\216\237\347\220\206\345\210\206\346\236\220" new file mode 100644 index 0000000..4bb79b5 --- /dev/null +++ "b/week_04/49/ConcurrentLinkedQueue\345\216\237\347\220\206\345\210\206\346\236\220" @@ -0,0 +1,25 @@ +ConcurrentLinkedQueue是线程安全的无界非阻塞队列,其底层数据结构是使用单向链表实现,入队和出队操作是CAS来保证线程安全的。 +ConcurrentLinkedQueue的结构由头节点Node head和尾节点 Node tail组成的,都是使用volatile修饰的。每个节点由节点元素item和指向下一个节点的next引用组成,组成一张链表结构。 +ConcurrentLinkedQueue继承自AbstractQueue类,实现Queue接口。 + +常用方法介绍: +1、boolean add(E e):将指定元素插入此队列的尾部,当队列满时,抛出异常 +2、boolean contains(Object o):判断队列是否包含次元素 +3、boolean isEmpty():判断队列是否为空 +4、boolean offer(E e):将元素插入队列尾部,当队列满时返回false +5、E peek():获取队列头部元素但不删除 +6、E poll():获取队列头部元素,并删除 +7、boolean remove(Object o):从队列中移指定元素 +8、int size():返回此队列中的元素数量,需要遍历一遍集合。判断队列是否为空时,不推荐此方法 +9、无参构造函数ConcurrentLinkedQueue():默认头尾节点都是指向 item 为 null 的哨兵节点(头结点等于尾结点) +10、有参构造函数ConcurrentLinkedQueue(Collection c):创建一个最初包含给定 collection 元素的 ConcurrentLinkedQueue,按照此 collection 迭代器的遍历顺序来添加元素。 + +总结 +CoucurrentLinkedQueue的结构由头节点和尾节点组成的,都是使用volatile修饰的。每个节点由节点元素item和指向下一个节点的next引用组成。 +入队:先检查插入的值是否为空,如果是空则抛出异常。然后以死循坏的方式执行一直到入队成功,整个过程大概就是把tail结点的next指向新结点,然后更新tail为新结点即可。但是tail结点并不是每次都是尾结点。所以每次入队都要通过tail定位尾结点。 +出队:出队操作就是从队列里返回一个最早插入的节点元素,并清空该节点对元素的引用。并不是每次出队都更新head节点,当head节点有元素时,直接弹出head节点的元素,并以cas方式设置节点的item为null,不会更新head节点。只有当head节点没有元素值时,出队操作才会更新head节点,这种做法是为了减少cas方式更新head节点的消耗,提供出队的效率 + + + + + diff --git "a/week_04/49/DelayQueue\345\216\237\347\220\206\345\210\206\346\236\220" "b/week_04/49/DelayQueue\345\216\237\347\220\206\345\210\206\346\236\220" new file mode 100644 index 0000000..b8cd5b7 --- /dev/null +++ "b/week_04/49/DelayQueue\345\216\237\347\220\206\345\210\206\346\236\220" @@ -0,0 +1,31 @@ +DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素,如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。 +也就是说只有在延迟期到时才能够从队列中取元素。DelayQueue主要用于两个方面: 1.缓存:清掉缓存中超时的缓存数据 2.任务超时处理 。 + +Delayed 接口有一个getDelay 方法接口,该方法用来告知延迟到期有多长的时间,或者延迟在多长时间之前已经到期。 +为了排序Delayed 接口还继承了Comparable 接口,因此必须实现compareTo(),使其可以进行元素的比较。 +DelayQueue 实现了BlockingQueue接口,该接口中定义了阻塞的方法接口, +DelayQueue 继承了AbstractQueue,具有了队列的行为。 +DelayQueue 通过组合一个PriorityQueue 来实现元素的存储以及优先级维护,通过ReentrantLock 来保证线程安全,通过Condition 来判断是否可以取数据。 + +重要属性介绍: +1、private final long delayTime; //延迟时间 +2、private final long expire; //到期时间 +3、private final transient ReentrantLock lock = new ReentrantLock(); //可重入锁 +4、private final PriorityQueue q = new PriorityQueue();//存储元素的优先级队列 +5、private Thread leader = null;//获取数据 等待线程标识 +6、private final Condition available = lock.newCondition();//条件控制,表示是否可以从队列中取数据 + +重要方法介绍: +1、offer(E e):将指定的元素插入到此队列中,在成功时返回 true,在add方法中,内部调用了offer 方法,直接调用offer 方法来完成入队操作。 +其实这方法内部通过PriorityQueue 来进行入队操作,入队超时方法并没有其超时功能。 + + +总结 +DelayQueue 内部通过组合PriorityQueue 来实现存储和维护元素顺序的。 +DelayQueue 存储元素必须实现Delayed 接口,通过实现Delayed 接口,可以获取到元素延迟时间,以及可以比较元素大小(Delayed 继承Comparable) +DelayQueue 通过一个可重入锁来控制元素的入队出队行为 +DelayQueue 中leader 标识 用于减少线程的竞争,表示当前有其它线程正在获取队头元素。 +PriorityQueue 只是负责存储数据以及维护元素的顺序,对于延迟时间取数据则是在DelayQueue 中进行判断控制的。 +DelayQueue 没有实现序列化接口 + + -- Gitee