1、CAS
CAS是一种无锁的非阻塞算法,全称为:Compare-and-swap(比较并交换),大致思路是:先比较目标对象现值是否和旧值一致,如果一致,则更新对象为新值;如果不一致,则表明对象已经被其他线程修改,直接返回。
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。
private boolean flag=false;
public static void main(String[] args) {
CasTest casTest=new CasTest();
boolean boolTest = casTest.getBoolTest();
System.out.println(boolTest);
}
private synchronized boolean getBoolTest(){//在多线程下该部分必须为原子代码块
if(!flag){
flag=true;
return true;
}
return false;
}
2、ConcurrentHashMap:
JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现ConcurrentHashMap;
HashMap,Hashtable与ConcurrentHashMap都是实现的哈希表数据结构,在随机读取的时候效率很高。Hashtable实现同步是利用synchronized关键字进行锁定的,其是针对整张哈希表进行锁定的,即每次锁住整张表让线程独占,在线程安全的背后是巨大的浪费。ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度进行区别以及如何区锁定。
上图中,左边是Hashtable的实现方式,可以看到锁住整个哈希表;而右边则是ConcurrentHashMap的实现方式,单独锁住每一个桶(segment).ConcurrentHashMap将哈希表分为16个桶(默认值),诸如get(),put(),remove()等常用操作只锁当前需要用到的桶,而size()才锁定整张表。原来只能一个线程进入,现在却能同时接受16个写线程并发进入(写线程需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的。
而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器(fast-fail iterator)的另一种迭代方式,称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时实例化出新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
我们在上面阐述了ConcurrentHashMap的使用特点和原理,分别在同样的一个高并发场景下,测试不同的方式产生的延时(ms):
Map<String, String> map = new ConcurrentHashMap<>();//483
Map<String, String> map = new ConcurrentSkipListMap<>(); //高并发并且排序 559
Map<String, String> map = new Hashtable<>(); //499
Map<String, String> map =Collections.synchronizedMap(new HashMap<>()); // 530
Map<String, String> map =Collections.synchronizedMap(new TreeMap()); //905
| 构造方法摘要 | |
|---|---|
ConcurrentHashMap() 创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。 | |
ConcurrentHashMap(int initialCapacity) 创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。 | |
ConcurrentHashMap(int initialCapacity, float loadFactor) 创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (16) 的新的空映射。 | |
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) 创建一个带有指定初始容量、加载因子和并发级别的新的空映射。 | |
ConcurrentHashMap(Map<? extends K,? extends V> m) 构造一个与给定映射具有相同映射关系的新映射。 | |
| 方法摘要 | |
|---|---|
void | clear() 从该映射中移除所有映射关系 |
boolean | contains(Object value) 一种遗留方法,测试此表中是否有一些与指定值存在映射关系的键。 |
boolean | containsKey(Object key) 测试指定对象是否为此表中的键。 |
boolean | containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。 |
Enumeration<V> | elements() 返回此表中值的枚举。 |
Set<Map.Entry<K,V>> | entrySet() 返回此映射所包含的映射关系的 Set 视图。 |
V | get(Object key) 返回指定键所映射到的值,如果此映射不包含该键的映射关系,则返回 null。 |
boolean | isEmpty() 如果此映射不包含键-值映射关系,则返回 true。 |
Enumeration<K> | keys() 返回此表中键的枚举。 |
Set<K> | keySet() 返回此映射中包含的键的 Set 视图。 |
V | put(K key, V value) 将指定键映射到此表中的指定值。 |
void | putAll(Map<? extends K,? extends V> m) 将指定映射中所有映射关系复制到此映射中。 |
V | putIfAbsent(K key, V value) 如果指定键已经不再与某个值相关联,则将它与给定值关联。 |
V | remove(Object key) 从此映射中移除键(及其相应的值)。 |
boolean | remove(Object key, Object value) 只有目前将键的条目映射到给定值时,才移除该键的条目。 |
V | replace(K key, V value) 只有目前将键的条目映射到某一值时,才替换该键的条目。 |
boolean | replace(K key, V oldValue, V newValue) 只有目前将键的条目映射到给定值时,才替换该键的条目。 |
int | size() 返回此映射中的键-值映射关系数。 |
Collection<V> | values() 返回此映射中包含的值的 Collection 视图。 |
hashtable之所以效率低下,是因为采用的是synchronized锁,锁住的是整个对象(hash表),因此,在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap.
java jdk1.5----- jdk1.7
在JDK1.7中ConcurrentHashMap采用了 数组 + Segment + 分段锁 的方式实现
ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,即每个分段则类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。
相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ConcurrentHashMap的初始化
JDK1.7的ConcurrentHashMap的初始化主要分为两个部分:
一是初始化ConcurrentHashMap,即初始化segments数组、segmentShift段偏移量和segmentMask段掩码等;
二是:然后则是初始化每个segment分段。
初始化ConcurrentHashMap
,在初始化时创建了两个中间变量ssize和sshift,它们都是通过concurrencyLevel计算得到的。其中ssize表示了segments数组的长度,为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方,所以在初始化时通过循环计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为数组的长度;而sshift表示了计算ssize时进行移位操作的次数。
初始化Segment分段
ConcurrentHashMap通过initialCapacity和loadFactor来初始化每个Segment. 在初始化Segment时,也定义了一个中间变量cap,其等于initialCapacity除以ssize的倍数c,如果c大于1,则取大于等于c的2的N次方,cap表示Segment中HashEntry数组的长度;loadFactor表示了Segment的加载因子,通过cap*loadFactor获得每个Segment的阈值threshold.
默认情况下,initialCapacity等于16,loadFactor等于0.75,concurrencyLevel等于16.
ConcurrentHashMap的操作
在介绍ConcurrentHashMap的操作之前,首先需要介绍一下Unsafe类,因为在JDK1.7新版本中是通过Unsafe类的方法实现锁操作的。Unsafe类是一个保护类,一般应用程序很少用到,但其在一些框架中经常用到,如JDK、Netty、Spring等框架。Unsafe类提供了一些硬件级别的原子操作,其在JDK1.7和JDK1.8中的ConcurrentHashMap都有用到,但其用法却不同,在此只介绍在JDK1.7中用到的几个方法:
arrayBaseOffset(Class class):获取数组第一个元素的偏移地址。
arrayIndexScale(Class class):获取数组中元素的增量地址。
getObjectVolatile(Object obj, long offset):获取obj对象中offset偏移地址对应的Object型field属性值,支持Volatile读内存语义。
get
JDK1.7的ConcurrentHashMap的get操作是不加锁的,因为在每个Segment中定义的HashEntry数组和在每个HashEntry中定义的value和next HashEntry节点都是volatile类型的,volatile类型的变量可以保证其在多线程之间的可见性,因此可以被多个线程同时读,从而不用加锁。put
ConcurrentHashMap的put方法首先也会通过hash算法定位到对应的Segment,此时,如果获取到的Segment为空,则调用ensureSegment()方法;否则,直接调用查询到的Segment的put方法插入值,注意此处并没有用getObjectVolatile()方法读,而是在ensureSegment()中再用volatile读操作,这样可以在查询segments不为空的时候避免使用volatile读,提高效率。在ensureSegment()方法中,首先使用getObjectVolatile()读取对应Segment,如果还是为空,则以segments[0]为原型创建一个Segment对象,并将这个对象设置为对应的Segment值并返回。
在Segment的put方法中,首先需要调用tryLock()方法获取锁,然后通过hash算法定位到对应的HashEntry,然后遍历整个链表,如果查到key值,则直接插入元素即可;而如果没有查询到对应的key,则需要调用rehash()方法对Segment中保存的table进行扩容,扩容为原来的2倍,并在扩容之后插入对应的元素。插入一个key/value对后,需要将统计Segment中元素个数的count属性加1。最后,插入成功之后,需要使用unLock()释放锁。
size
ConcurrentHashMap的size操作的实现方法也非常巧妙,一开始并不对Segment加锁,而是直接尝试将所有的Segment元素中的count相加,这样执行两次,然后将两次的结果对比,如果两次结果相等则直接返回;而如果两次结果不同,则再将所有Segment加锁,然后再执行统计得到对应的size值。实列(实战):
public static void main(String[] args) {
ConcurrentHashMap<Integer,String> hashMap=new ConcurrentHashMap();
for (int i = 0; i < 100; i++) {
hashMap.put(i,"第"+i+"值");
}
Set<Map.Entry<Integer, String>> entries = hashMap.entrySet();
Iterator<Map.Entry<Integer, String>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<Integer, String> next = iterator.next();
System.out.println(next.getKey()+"-----"+next.getValue());
}
}
JDK1.8:
采用了 数组 + 链表 + 红黑树 的实现方式来设计,内部大量采用CAS操作。
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才可能有机会执行。
JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。
Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。
<strong>class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; //... 省略部分代码 } </strong>
Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,不过保证线程安全性。
在JDK8中ConcurrentHashMap的结构,由于引入了红黑树,使得ConcurrentHashMap的实现非常复杂,我们都知道,红黑树是一种性能非常好的二叉查找树,其查找性能为O(logN),但是其实现过程也非常复杂,而且可读性也非常差,Doug
Lea的思维能力确实不是一般人能比的,早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。
实战:
总结
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
3.ConcurrentLinkedQueue
一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
以ConcurrentLinkedQueue为例,他实现了Queue接口,实例化方式如下:
Queue<String> strs = new ConcurrentLinkedQueue<>();
添加元素的方法:offer()
取出队头的方法:poll()
判断队列长度:size()
对于双端队列,使用ConcurrentLinkedDeque类型来实现.
需求:现在有一个公共资源、可以给指定的人拥有查看权限
ConcurrentLinkedQueue() 创建一个最初为空的 ConcurrentLinkedQueue。 |
ConcurrentLinkedQueue(Collection<? extends E> c) 创建一个最初包含给定 collection 元素的 ConcurrentLinkedQueue,按照此 collection 迭代器的遍历顺序来添加元素。 |
| 方法摘要 | ||
|---|---|---|
boolean | add(E e) 将指定元素插入此队列的尾部。 | |
boolean | contains(Object o) 如果此队列包含指定元素,则返回 true。 | |
boolean | isEmpty() 如果此队列不包含任何元素,则返回 true。 | |
Iterator<E> | iterator() 返回在此队列元素上以恰当顺序进行迭代的迭代器。 | |
boolean | offer(E e) 将指定元素插入此队列的尾部。 | |
E | peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。 | |
E | poll() 获取并移除此队列的头,如果此队列为空,则返回 null。 | |
boolean | remove(Object o) 从队列中移除指定元素的单个实例(如果存在)。 | |
int | size() 返回此队列中的元素数量。 | |
Object[] | toArray() 返回以恰当顺序包含此队列所有元素的数组。 | |
| toArray(T[] a) 返回以恰当顺序包含此队列所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。 | |
@Slf4j
public class ConcurrentLinkedQueueTest {
private static int maxThread=200;
private static String [] str={"张三","李四","王五"};
private static Queue<String> queue=new ConcurrentLinkedQueue<>();
public static void main(String[] args) {
ConcurrentLinkedQueueTest concurrentLinkedQueueTest=new ConcurrentLinkedQueueTest();
concurrentLinkedQueueTest.add(queue);
ThreadTest threadTest=new ThreadTest(queue,str);
//创建线程池
ExecutorService executorService=Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < str.length; i++) {
executorService.execute(threadTest);
}
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
executorService.shutdown();
log.info("关闭线程池");
}
queue.offer("\n"+"电脑");
queue.poll();
queue.peek();
System.out.println(queue);
System.out.println("执行完毕");
}
/**
* 创建资源
* @param queue
* @return
*/
public String add(Queue<String> queue){
String queue2=null;
for(int i=0;i<30;i++){
queue.add("大"+i);
queue2=queue.element();
}
if(null==queue2){
return queue2;
}else{
return null;
}
}
}
/**
* 线程获取资源
*/
class ThreadTest implements Runnable{
int index=0;
private Queue<String> queue=null;
private String[] str=null;
public ThreadTest(Queue<String> queue,String[] str){
this.queue=queue;
this.str=str;
}
@Override
public void run() {
if(null==queue){
queue=new ConcurrentLinkedQueue<>();
}
System.out.println(str[index]+"线程:"+(index++)+""+queue);
}
}
https://www.jianshu.com/p/808da4a75f22【相关博客】
4、CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
写时复制容器,即copy-on-write,在多线程环境下,写时效率低,读时效率高,适合写少读多的环境。对比测试几种情况:
CopyOnWriteArrayList() 创建一个空列表。 |
CopyOnWriteArrayList(Collection<? extends E> c) 创建一个按 collection 的迭代器返回元素的顺序包含指定 collection 元素的列表。 |
CopyOnWriteArrayList(E[] toCopyIn) 创建一个保存给定数组的副本的列表。 |
CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
| 方法摘要 | ||
|---|---|---|
boolean | add(E e) 将指定元素添加到此列表的尾部。 | |
void | add(int index, E element) 在此列表的指定位置上插入指定元素。 | |
boolean | addAll(Collection<? extends E> c) 按照指定 collection 的迭代器返回元素的顺序,将指定 collection 中的所有元素添加此列表的尾部。 | |
boolean | addAll(int index, Collection<? extends E> c) 从指定位置开始,将指定 collection 的所有元素插入此列表。 | |
int | addAllAbsent(Collection<? extends E> c) 按照指定 collection 的迭代器返回元素的顺序,将指定 collection 中尚未包含在此列表中的所有元素添加列表的尾部。 | |
boolean | addIfAbsent(E e) 添加元素(如果不存在)。 | |
void | clear() 从此列表移除所有元素。 | |
Object | clone() 返回此列表的浅表副本。 | |
boolean | contains(Object o) 如果此列表包含指定的元素,则返回 true。 | |
boolean | containsAll(Collection<?> c) 如果此列表包含指定 collection 的所有元素,则返回 true。 | |
boolean | equals(Object o) 比较指定对象与此列表的相等性。 | |
E | get(int index) 返回列表中指定位置的元素。 | |
int | hashCode() 返回此列表的哈希码值。 | |
int | indexOf(E e, int index) 返回第一次出现的指定元素在此列表中的索引,从 index 开始向前搜索,如果没有找到该元素,则返回 -1。 | |
int | indexOf(Object o) 返回此列表中第一次出现的指定元素的索引;如果此列表不包含该元素,则返回 -1。 | |
boolean | isEmpty() 如果此列表不包含任何元素,则返回 true。 | |
Iterator<E> | iterator() 返回以恰当顺序在此列表元素上进行迭代的迭代器。 | |
int | lastIndexOf(E e, int index) 返回最后一次出现的指定元素在此列表中的索引,从 index 开始向后搜索,如果没有找到该元素,则返回 -1。 | |
int | lastIndexOf(Object o) 返回此列表中最后出现的指定元素的索引;如果列表不包含此元素,则返回 -1。 | |
ListIterator<E> | listIterator() 返回此列表元素的列表迭代器(按适当顺序)。 | |
ListIterator<E> | listIterator(int index) 返回列表中元素的列表迭代器(按适当顺序),从列表的指定位置开始。 | |
E | remove(int index) 移除此列表指定位置上的元素。 | |
boolean | remove(Object o) 从此列表移除第一次出现的指定元素(如果存在)。 | |
boolean | removeAll(Collection<?> c) 从此列表移除所有包含在指定 collection 中的元素。 | |
boolean | retainAll(Collection<?> c) 只保留此列表中包含在指定 collection 中的元素。 | |
E | set(int index, E element) 用指定的元素替代此列表指定位置上的元素。 | |
int | size() 返回此列表中的元素数。 | |
List<E> | subList(int fromIndex, int toIndex) 返回此列表中 fromIndex(包括)和 toIndex(不包括)之间部分的视图。 | |
Object[] | toArray() 返回一个按恰当顺序(从第一个元素到最后一个元素)包含此列表中所有元素的数组。 | |
| toArray(T[] a) 返回以恰当顺序(从第一个元素到最后一个元素)包含列表所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。 | |
String | toString() 返回此列表的字符串表示形式。 | |
List<String> lists = new ArrayList<>();
//这个会出并发问题!报错:ArrayIndexOutOfBoundsException
List<String> lists = new Vector();//111 ms
List<String> lists = new CopyOnWriteArrayList<>();//5230 ms
//测试核心代码:
Runnable task = new Runnable() {
@Override
public void run() {
for(int i=0; i<1000; i++) lists.add("a" + r.nextInt(10000));
}
};
//多线程向该容器中不断加入数据。
从JDK 5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后向新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为在当前读的容器中不会添加任何元素。所以CopyOnWrite容器是一种读写分离的思想,读和写对应不同的容器
happen-before 原则:
1.为什么会出现happen-before 原则?
java 存在线程工作内存及主内存(多核cpu及多级缓存导致),线程对变量的操作都是在工作内存中进行的,那么多线程的操作必然会导致共享数据的错乱
操作系统会对指令进行重排序,在多线程环境下,重排序对会线程的执行结果及执行顺序造成严重的影响
通过对happen-before原则的定义,可以满足多线程环境下,程序能正常按照我们预想的路径进行执行,满足我们预想的结果。
2.JVM 依靠什么来实现happen-before 原则呢?
主要是依靠内存屏障。
3.happen-before八大原则:
1、单线程happen-before原则:
在同一个线程中,书写在前面的操作happen-before后面的操作。
2、锁的happen-before原则:
同一个锁的unlock操作happen-before此锁的lock操作。
3、volatile的happen-before原则:
对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
4、happen-before的传递性原则:
如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
5、线程启动的happen-before原则:
同一个线程的start方法happen-before此线程的其它方法。
6、线程中断的happen-before原则:
对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
7、线程终结的happen-before原则:
线程中的所有操作都happen-before线程的终止检测。
8、对象创建的happen-before原则:
一个对象的初始化完成先于他的finalize方法调用。
小列子:
public static void main(String[] args) {
CopyOnwriteArrayListTest2 copyOnwriteArrayListTest2=new CopyOnwriteArrayListTest2();
CopyOnWriteArrayList copyOnWriteArrayList=new CopyOnWriteArrayList(copyOnwriteArrayListTest2.getList());
Iterator iterator = copyOnWriteArrayList.iterator();
while (iterator.hasNext()){
String next = (String) iterator.next();
System.out.println(next);
}
}
/**
* 集合资源
*/
public List getList(){
List<String> list=new ArrayList<>();
for (int i = 0; i < 20; i++) {
list.add("小"+i);
}
return list;
}
CopyOnWriteArraySet:基于CopyOnWriteArrayList进行迭代,不添加重复元素
public static void main(String[] args) {
CopyOnWriteArraySet<String> copyOnWriteArraySet=new CopyOnWriteArraySet();
for (int i = 0; i < 20; i++) {
copyOnWriteArraySet.add("大"+i);
}
copyOnWriteArraySet.add("大2");
Iterator iterator = copyOnWriteArraySet.iterator();
while(iterator.hasNext()){
String str=(String)iterator.next();
System.out.println("result:"+str);
}
}
5、BlockingQueue:阻塞队列
作用:支持两个附加操作的 Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。
BlockingQueue 不接受 null 元素。
BlockingQueue 可以是限定容量的。
BlockingQueue 实现主要用于生产者-使用者队列,但它另外还支持 Collection 接口。
BlockingQueue 实现是线程安全的
BlockingQueue 实质上不 支持使用任何一种“close”或“shutdown”操作来指示不再添加任何项。
内存一致性效果:当存在其他并发 collection 时,将对象放入 BlockingQueue 之前的线程中的操作 happen-before 随后通过另一线程从 BlockingQueue 中访问或移除该元素的操作。
| 抛出异常 | 特殊值 | 阻塞 | 超时 | |
| 插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
| 移除 | remove() | poll() | take() | poll(time, unit) |
| 检查 | element() | peek() | 不可用 | 不可用 |
这种并发容器,会自动实现阻塞式的生产者/消费者模式。使用队列解耦合,在实现异步事物的时候很有用。下面的例子,实现了阻塞队列:
入列:
offer(E e):如果队列没满,立即返回true; 如果队列满了,立即返回false-->不阻塞
put(E e):如果队列满了,一直阻塞,直到队列不满了或者线程被中断-->阻塞
offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,,如果队列已满,则进入等待,直到出现以下三种情况:-->阻塞
被唤醒
等待时间超时
当前线程被中断
出列:
poll():如果没有元素,直接返回null;如果有元素,出队
take():如果队列空了,一直阻塞,直到队列不为空或者线程被中断-->阻塞
poll(long timeout, TimeUnit unit):如果队列不空,出队;如果队列已空且已经超时,返回null;如果队列已空且时间未超时,则进入等待,直到出现以下三种情况:
被唤醒
等待时间超时
当前线程被中断
LinkedBlockingQueue
static BlockingQueue<String> strs = new LinkedBlockingQueue<>(10);
strs.put("a" + i); //加入队列,如果满了,就会等待
strs.take(); //取出队列元素,如果空了,就会等待
在实例化时,可以指定具体的队列容量。
在加入成员的时候,除了使用put方法还可以使用其他方法:
Str.add(“aaa”);
/* add如果在队列满了之后,再加入成员会抛出异常,而这种情况下,put方法会一直等待被消费掉。
*/
Str.offer(“aaa”);
/* offer添加成员的时候,会有boolean类型的返回值,如果添加成功,会返回true,如果添加失败,会返回false.除此之外,offer还可以按时段进行添加,例如:
*/
strs.offer("aaa", 1, TimeUnit.SECONDS);
/*
如果队列满了,等待1秒,再进行成员的添加,如果添加失败了,则返回false.https://www.jianshu.com/p/7b2f1fa616c6 //知识点【博客地址】
6、ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
static BlockingQueue<String> strs = new ArrayBlockingQueue<>(10);
对象的方法和上面的BlockingQueue是一样的,用法也是一样的。
二者的区别主要是:
1. LinkedBlockingQueue是一个单向链表实现的阻塞队列,在链表一头加入元素,如果队列满,就会阻塞,另一头取出元素,如果队列为空,就会阻塞。
2. LinkedBlockingQueue内部使用ReentrantLock实现插入锁(putLock)和取出锁(takeLock)。
相比于数组实现的ArrayBlockingQueue的有界情况,我们称之为有界队列,LinkedBlockingQueue可认为是无界队列。当然,也可以向上面那样指定队列容量,但是这个参数常常是省略的,多用于任务队列。
https://www.xttblog.com/?p=3686【相关博客】
7、DelayQueue:
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。
DelayQueue也是一个BlockingQueue,其特化的参数是Delayed。
Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay()的返回值应为固定值(final).DelayQueue内部是使用PriorityQueue实现的,即:
DelayQueue = BlockingQueue + PriorityQueue + Delayed
可以说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。这是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。但是要注意的是,不能将null元素放置到这种队列中。
Delayed,一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象。此接口的实现类必须重写一个 compareTo() 方法,该方法提供与此接口的 getDelay()方法一致的排序。
DelayQueue存储的对象是实现了Delayed接口的对象,在这个对象中,需要重写compareTo()和getDelay()方法
| 构造方法摘要 | |
|---|---|
DelayQueue() 创建一个最初为空的新 DelayQueue。 | |
DelayQueue(Collection<? extends E> c) 创建一个最初包含 Delayed 实例的给定 collection 元素的 DelayQueue。 | |
| 方法摘要 | ||
|---|---|---|
boolean | add(E e) 将指定元素插入此延迟队列中。 | |
void | clear() 自动移除此延迟队列的所有元素。 | |
int | drainTo(Collection<? super E> c) 移除此队列中所有可用的元素,并将它们添加到给定 collection 中。 | |
int | drainTo(Collection<? super E> c, int maxElements) 最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中。 | |
Iterator<E> | iterator() 返回在此队列所有元素(既包括到期的,也包括未到期的)上进行迭代的迭代器。 | |
boolean | offer(E e) 将指定元素插入此延迟队列。 | |
boolean | offer(E e, long timeout, TimeUnit unit) 将指定元素插入此延迟队列中。 | |
E | peek() 获取但不移除此队列的头部;如果此队列为空,则返回 null。 | |
E | poll() 获取并移除此队列的头,如果此队列不包含具有已到期延迟时间的元素,则返回 null。 | |
E | poll(long timeout, TimeUnit unit) 获取并移除此队列的头部,在可从此队列获得到期延迟的元素,或者到达指定的等待时间之前一直等待(如有必要)。 | |
void | put(E e) 将指定元素插入此延迟队列。 | |
int | remainingCapacity() 因为 DelayQueue 没有容量限制,所以它总是返回 Integer.MAX_VALUE。 | |
boolean | remove(Object o) 从此队列中移除指定元素的单个实例(如果存在),无论它是否到期。 | |
int | size() 返回此 collection 中的元素数。 | |
E | take() 获取并移除此队列的头部,在可从此队列获得到期延迟的元素之前一直等待(如有必要)。 | |
Object[] | toArray() 返回包含此队列所有元素的数组。 | |
| toArray(T[] a) 返回一个包含此队列所有元素的数组;返回数组的运行时类型是指定数组的类型。 | |
8、LinkedTransferQueue:
TransferQueue是一个继承了BlockingQueue的接口,并且增加若干新的方法。LinkedTransferQueue是TransferQueue接口的实现类,其定义为一个无界的队列,具有先进先出(FIFO)的特性。
TransferQueue接口含有下面几个重要方法:
1. transfer(E e)
若当前存在一个正在等待获取的消费者线程,即立刻移交之;否则,会插入当前元素e到队列尾部,并且等待进入阻塞状态,到有消费者线程取走该元素。
2. tryTransfer(E e)
若当前存在一个正在等待获取的消费者线程(使用take()或者poll()函数),使用该方法会即刻转移/传输对象元素e;若不存在,则返回false,并且不进入队列。这是一个不阻塞的操作。
3. tryTransfer(E e,long timeout,TimeUnit unit)
若当前存在一个正在等待获取的消费者线程,会立即传输给它;否则将插入元素e到队列尾部,并且等待被消费者线程获取消费掉;若在指定的时间内元素e无法被消费者线程获取,则返回false,同时该元素被移除。
4. hasWaitingConsumer()
判断是否存在消费者线程。
5. getWaitingConsumerCount()
获取所有等待获取元素的消费线程数量。
6. size()
因为队列的异步特性,检测当前队列的元素个数需要逐一迭代,无法保证原子性,可能会得到一个不太准确的结果,尤其是在遍历时有可能队列发生更改。
使用方法:
LinkedTransferQueue<String> strs = new LinkedTransferQueue<>();//实例化
如果当前没有消费者线程(存在take方法的线程):
strs.transfer("aaa");
该方法会一直阻塞在这里,知道有消费者线程存在。
而如果使用传统的put()方法来加入元素的话,则不会发生阻塞现象。
strs.take()
同样,获取队列中元素的方法take()也是阻塞在这里等待获取新的元素的。https://www.jianshu.com/p/808da4a75f22【相关博客】
9、SynchronousQueue
对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略。默认情况下不保证这种排序。但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。
SynchronousQueue也是一种BlockingQueue,是一种无缓冲的等待队列。所以,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为0的阻塞队列(也可以认为是1),它的isEmpty()方法永远返回是true,remainingCapacity()方法永远返回是0.
remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null.
在使用put()方法时,会一直阻塞在这里,等待被消费:
“`
| 构造方法摘要 | |
|---|---|
SynchronousQueue() 创建一个具有非公平访问策略的 SynchronousQueue。 | |
SynchronousQueue(boolean fair) 创建一个具有指定公平策略的 SynchronousQueue。 | |
| 方法摘要 | ||
|---|---|---|
void | clear() 不执行任何操作。 | |
boolean | contains(Object o) 始终返回 false。 | |
boolean | containsAll(Collection<?> c) 除非给定 collection 为空,否则返回 false。 | |
int | drainTo(Collection<? super E> c) 移除此队列中所有可用的元素,并将它们添加到给定 collection 中。 | |
int | drainTo(Collection<? super E> c, int maxElements) 最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中。 | |
boolean | isEmpty() 始终返回 true。 | |
Iterator<E> | iterator() 返回一个空迭代器,其中 hasNext 始终返回 false。 | |
boolean | offer(E e) 如果另一个线程正在等待以便接收指定元素,则将指定元素插入到此队列。 | |
boolean | offer(E o, long timeout, TimeUnit unit) 将指定元素插入到此队列,如有必要则等待指定的时间,以便另一个线程接收它。 | |
E | peek() 始终返回 null。 | |
E | poll() 如果另一个线程当前正要使用某个元素,则获取并移除此队列的头。 | |
E | poll(long timeout, TimeUnit unit) 获取并移除此队列的头,如有必要则等待指定的时间,以便另一个线程插入它。 | |
void | put(E o) 将指定元素添加到此队列,如有必要则等待另一个线程接收它。 | |
int | remainingCapacity() 始终返回 0。 | |
boolean | remove(Object o) 始终返回 false。 | |
boolean | removeAll(Collection<?> c) 始终返回 false。 | |
boolean | retainAll(Collection<?> c) 始终返回 false。 | |
int | size() 始终返回 0。 | |
E | take() 获取并移除此队列的头,如有必要则等待另一个线程插入它。 | |
Object[] | toArray() 返回一个 0 长度的数组。 | |
| toArray(T[] a) | |
BlockingQueue strs = new SynchronousQueue<>();//实例化
strs.put(“aaa”); //阻塞等待消费者消费
strs.add(“aaa”);//会产生异常,提示队列满了
strs.take();//该方法可以取出元素,同样是阻塞的,需要在线程中去实现他,作为消费者.
https://www.cnblogs.com/shangxiaofei/p/5707552.html【相关博客】
1949

被折叠的 条评论
为什么被折叠?



