Java学习笔记10-并发容器类
推理HashMap的实现
-
数据要存储
涉及到数据结构:数组、链表、栈、树、队列
-
数组的插入和查找
- 顺序查找:插入时按先后顺序插入,查找时轮询扫描进行对比。
- 二分查找:插入时进行排序;查找时将n个元素分成大致相等的两部分,减少复杂度。
- 分块查找:分块查找是二分查找和顺序查找的一种改进。
- 哈希表:对元素的关键信息进行hash计算,求出下标后直接插入或查找。常用的实现是除留余数法。
-
哈希冲突,数组位置已存在值
当hash(key2) = hash(key1) 时哈希冲突。解决方法:链地址法;ReHash1(key2) 再次计算hash;
-
合理控制数组和链表的长度
动态扩容resize()
HashMap源码分析
JDK7中HashMap,以数组+链表形式进行存储,默认空间大小16,默认负载因子0.75;通过hash(key)对数组长度求余算出数组下标index进行存放,若该位置已存在数据,再以链表形式next存储数据;链表越长查询效率越低,当大小超过16*0.75=12则resize扩容,扩容可以使数组变大,链表变小从而提高查询效率;由于内部计数采用 i++形式,所以为非线程安全。
JDK8中对HashMap进行优化,以数组+链表+红黑树形式进行存储,默认变树阀值8,当链表长度为8时,变为红黑树结构来提高查询效率。
JDK7中HashMap:key/value --> hash(key)%size --> index数组下标 --> 重复则覆盖,否则存入链表
JDK8中HashMap:key/value --> hash(key)%size --> index数组下标 --> 重复则覆盖,否则存入链表 --> 链表超过一定长度变为红黑树结构
链表↓ | JDK8红黑树↓ | |||||
---|---|---|---|---|---|---|
hash(key)%size=index | index=0 | index=1 | index=2 | index=3 | … | index=15 |
key/value/next数组→ | key1 | null | key3 | null | … | key16 |
↓ | ↓ | ↓ | ||||
key11 | key33 | null | ||||
↓ | ↓ | |||||
… | 链表长度到阀值 | |||||
↓ | ↓ | |||||
null | 变红黑树 |
ConcurrentHashMap源码分析
JDK7中以Segment形式进行存储,Segment内部还是个HashMap,本质就是HashMap包裹了层Segment,Segment继承了ReentrantLock可加锁,所以线程安全。
JDK8中又进行了优化,采用CAS+Synchronized实现线程安全,阉割了Segment仅保留定义无实际功能,在hash无冲突时使用CAS机制加入数组,有冲突时用同步关键字操作链表,提高性能。
ConcurrentSkipListMap源码分析
我们通过查看源码发现ConcurrentSkipListMap结构是个跳表(名字上也能猜到),跳表实际上是加了索引的链表,我们知道单向链表查询需要一个个去查,那效率…… 跳表在链表上随机生成了多级索引,每级索引相连可以向右和向下查找,在查询时先查询索引,往右查找进行比对,然后再向下,中间跳过了很多节点,比单向快了很多,描述不清了,直接上图吧~
开始↓ | |||||||||
---|---|---|---|---|---|---|---|---|---|
五级索引 | 1(→) | 59(↓) | |||||||
四级索引 | 1 | 59(↓) | |||||||
三级索引 | 1 | 26 | 59(→) | 72(↓) | |||||
二级索引 | 1 | 15 | 26 | 37 | 59 | 72(↓) | |||
一级索引 | 1 | 15 | 26 | 37 | 59 | 72(↓) | |||
单向链表 | 1-13 | 14-22 | 23-38 | 39-48 | 45-55 | 56-68 | 69-72(→) | 73-85 | 86-100 |
查75 |
怎么样,用单向链表去一个个查…累死宝宝了,要是用了跳表 腰不酸了腿不疼了,一口气能上五楼,还不费劲儿
List集合分析
ArrayList源码分析
原理比较简单,内部就是以数组形式进行存储,当大小超过数组长度,新建个数组将原来数组数据拷贝过来再添加新元素,由于内部计数采用 i++形式,所以为非线程安全。
CopyOnWriteArrayList源码分析
CopyOnWriteArrayList是JUC包里的,它内部读取操作无锁,修改操作加了把ReentrantLock锁,每次添加元素时都会新建个数组将原来数组数据拷贝过来添加新元素再替换回去。
它与ArrayList相比,优点是并发安全,缺点有两个:
- 多了内存占用:写数据是copy一份完整的数据,单独进行操作。占用双份内存。
- 数据一致性:数据写完之后,其他线程不一定是马上读取到最新内容。
Set集合分析:Set和List重要区别:不重复
HashSet源码分析
HashSet基于HashMap原理实现,每次添加存储到HashMap的key值,所以是不重复,特点为非线程安全。
CopyOnWriteArraySet源码分析
CopyOnWriteArraySet基于CopyOnWriteArrayList原理实现,调用CopyOnWriteArrayList的addIfAbsent方法,如果不存在就添加到集合,特点为线程安全。
ConcurrentSkipListSet源码分析
ConcurrentSkipListSet基于ConcurrentSkipListMap原理实现,调用ConcurrentSkipListMap的putIfAbsent方法,如果不存在就添加到集合,特点为线程安全,有序,查询快。
Queue队列分析
队列数据结构的实现,分为阻塞队列和非阻塞队列。下方为核心Queue API
方法 | 作用 | 描述 |
---|---|---|
add | 添加一个元素 | 如果队列已满,则抛出一个lllegalStateException异常 |
remove | 移除并返回队列头部的元素 | 如果队列为空,则抛出一个NoSuchElementException异常 |
element | 返回队列头部的元素 | 如果队列为空,则抛出一个NoSuchElementException异常 |
offer | 添加一个元素并返回ture | 如果队列已满,则返回false |
poll | 移除并返回队列头部的元素 | 如果队列为空,则返回null |
peek | 返回队列头部的元素 | 如果队列为空,则返回null |
put | 添加一个元素 | 如果队列已满,则阻塞 |
take | 移除并返回队列头部的元素 | 如果队列为空,则阻塞 |
ArrayBlockingQueue源码分析
ArrayBlockingQueue是基于数组的阻塞循环队列,操作的时候加了ReentrantLock锁,在put与take时都用了park进行阻塞,等待unpark进行操作,前两节我们讲了park/unpark操作,这里就不在重复讲解了,直接上代码~
ArrayBlockingQueue测试代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class ArrayBlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
// 构造时设置队列大小,还可以设置 公平(FIFO先进先出原则) / 非公平
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3, true);
// 消费者取数据
new Thread(() -> {
for (; ; ) {
try {
//System.out.println("取到数据:" + queue.poll()); // poll非阻塞(如果队列为空,则返回null)
System.out.println("取到数据:" + queue.take()); // take阻塞(如果队列为空,则阻塞)
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
}
}
}).start();
// 等待3秒,让消费者先跑起来
TimeUnit.SECONDS.sleep(3L);
// 生成者存数据
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
// offer非阻塞(如果队列已满,则返回false)
//System.out.println(threadName + "存入" + queue.offer(threadName));
// put阻塞(如果队列已满,则阻塞)
System.out.println(threadName + "存入");
queue.put(threadName);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
LinkedBlockingQueue源码分析
LinkedBlockingQueue是基于链表的阻塞队列,操作的时候也加了ReentrantLock锁,在put与take时都用了park进行阻塞,等待unpark进行操作,只不过存储形式变成了链表,这里就不在重复讲解了~
ConcurrentLinkedQueue源码分析
ConcurrentLinkedQueue也是基于链表的队列,只不过它是非阻塞的,没有put与take方法,在操作的时候使用了CAS机制提高了效率,但批量操作不提供原子保证(addAll, removeAll, retainAll, containsAll, equals, and toArray),另外size()方法每次都是遍历整个链表,最好不要频繁调用。
ConcurrentLinkedQueue测试代码
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
public class ConcurrentLinkedQueueTest {
public static void main(String[] args) throws InterruptedException {
// 不需要指定队列大小
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// 消费者取数据
new Thread(() -> {
for (; ; ) {
try {
System.out.println("取到数据:" + queue.poll()); // poll非阻塞(如果队列为空,则返回null)
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
}
}
}).start();
// 等待3秒,让消费者先跑起来
TimeUnit.SECONDS.sleep(3L);
// 生成者存数据
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
String threadName = Thread.currentThread().getName();
// offer非阻塞(如果队列已满,则返回false)
System.out.println(threadName + "存入" + queue.offer(threadName));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
SynchronousQueue源码分析
SynchronousQueue骨骼比较清奇,他实际不存数据,一个读取操作需要等待一个写入操作,生产者和消费者是需要组队完成工作,缺少一个将会阻塞线程,直到等到配对为止。内部有一个Transferer,根据构造时公平策略(默认公平模式)实例化TransferQueue或TransferStack,在SynchronousQueue中TransferQueue队列FIFO提供公平模式,而TransferStack栈LIFO提供的则是非公平模式。内部逻辑相似,都是进去读取数据,没数据park,等待写入数据的unpark;或者进去写入数据park,等待读取数据的unpark。
SynchronousQueue测试代码
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousQueueTest {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
//System.out.println(synchronousQueue.add("a")); // IllegalStateException
//System.out.println(synchronousQueue.offer("a"));
//System.out.println(synchronousQueue.poll()); // 非阻塞
// 阻塞:已写入,等待读取
//new Thread(() -> {
// try {
// System.out.println("VIP快递服务送快递来了!没人取不走啊~");
// synchronousQueue.put("快递包裹");
// System.out.println("终于来人取了……走人~");
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//}).start();
//
//TimeUnit.SECONDS.sleep(3L);
//
//System.out.println("我的快递,那给我吧!");
//System.out.println("取到了" + synchronousQueue.take());
// 阻塞:已读取,等待写入
new Thread(() -> {
try {
System.out.println("大哥行行好吧,吃不上饭了,给点钱吧!");
System.out.println("拿到了" + synchronousQueue.take());
System.out.println("比我还穷了,还是快走吧~");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(3L);
System.out.println("臭要饭的,拿了钱快走!");
synchronousQueue.put("5毛钱");
}
}
PriorityQueue源码分析
PriorityQueue是一个带优先级的队列,而不是先进先出队列,通过Comparator比较器可以设置比对方式;元素按优先级顺序被移除,该队列也没有上限,可以自动扩容,PriorityBlockingQueue与PriorityQueue类似,每个操作加了ReentrantLock锁,多了put / take 方法,这里不在多说。
PriorityQueue测试代码
import java.util.Comparator;
import java.util.PriorityQueue;
public class PriorityQueueTest {
public static void main(String[] args) {
PriorityQueue<PriorityMessage> priorityQueue = new PriorityQueue<>(new Comparator<PriorityMessage>() {
@Override
public int compare(PriorityMessage o1, PriorityMessage o2) {
return o1.order < o2.order ? -1 : 1;
}
});
int num = 5;
PriorityMessage priorityMessage;
for (int i = num; i > 0; i--) {
priorityMessage = new PriorityMessage("title" + i, "content" + i, i);
priorityQueue.offer(priorityMessage);
System.out.println("装填 " + priorityMessage);
}
while (!priorityQueue.isEmpty()) {
System.out.println("释放 " + priorityQueue.poll());
}
}
}
class PriorityMessage {
String title;
String content;
int order;
/**
* 构造函数
*
* @param title 标题
* @param content 内容
* @param order 序号
*/
public PriorityMessage(String title, String content, int order) {
this.title = title;
this.content = content;
this.order = order;
}
@Override
public String toString() {
return title + " " + content + " " + order;
}
}
DelayQueue源码分析
DelayQueue延时队列内部使用PriorityQueue存储,每个操作加了ReentrantLock锁,多了put / take 方法,这里不在多说。
DelayQueue延时队列基于PriorityQueue原理实现是一个存放Delayed 元素的无界阻塞队列,内部操作使用ReentrantLock锁,也是通过Comparator比较器可以设置比对方式,只不过比较的是延时时间,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素,如果延迟都还没有期满,则队列没有头部,并且poll将返回null,此队列不允许使用 null 元素, ScheduledExecutorService 定时任务内部用的就是类似的延时队列。
DelayQueue测试代码
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueTest {
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayedMessage> delayQueue = new DelayQueue<>();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
long now = System.currentTimeMillis();
System.out.println("起始时间:" + dateFormat.format(now));
// 2秒后发送
DelayedMessage msg1 = new DelayedMessage("title1", "content1", new Date(now + 2000L));
delayQueue.add(msg1);
// 5秒后发送
DelayedMessage msg2 = new DelayedMessage("title2", "content2", new Date(now + 5000L));
delayQueue.add(msg2);
while (!delayQueue.isEmpty()) {
System.out.println(delayQueue.take());
TimeUnit.SECONDS.sleep(1L);
}
}
}
class DelayedMessage implements Delayed {
String title;
String content;
Date sendTime;
/**
* 构造函数
*
* @param title 标题
* @param content 内容
* @param sendTime 发送时间
*/
public DelayedMessage(String title, String content, Date sendTime) {
this.title = title;
this.content = content;
this.sendTime = sendTime;
}
@Override
public long getDelay(TimeUnit unit) {
long duration = sendTime.getTime() - System.currentTimeMillis();
return unit.convert(duration, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return o.getDelay(TimeUnit.NANOSECONDS) < this.getDelay(TimeUnit.NANOSECONDS) ? 1 : -1;
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
@Override
public String toString() {
return title + " " + content + " " + dateFormat.format(sendTime);
}
}