一、简要关系结构图
二、ArrayList
1、jdk 1.7 原理
调用空构造器时初始化 Object[] elementData 数组,长度为10;
初始化 size 属性,值为0;
当elementData 数组中的10个位置都有元素后,添加第11个元素时数组扩容;
扩容长度为原数组的1.5倍;
扩容逻辑是创建一个长度为 15 的数组,将旧数组的数据拷贝到新数组中。
2、jdk 1.8 原理
调用空构造器时初始化 Object[] elementData 数组,长度为 0;
初始化 size 属性,值为0;
添加第一个元素时,进行数组扩容;创建一个长度为 10 的数组,将该元素添加到新数组中;
剩余扩容逻辑与 jdk 1.7 一致;
3、特点
查询快,增删慢;
三、LinkedList
1、原理
是一个双向链表;维护了头节点和尾节点。
维护了一个size,用于计数。
每一个节点对应的类是 linkedList的内部类Node<E>
每一个节点包含三个主要参数:上一个节点地址、下一个节点地址、当前节点存储的数据;
调用 get方法传入索引值,底层会判断当前索引是否大于size的一半,用来判断从头遍历还是从尾部遍历(for循环遍历);
使用for循环和for-each循环实际上是使用 iterator 迭代器进行遍历;
2、特点
查询慢,增删快。
四、Vector
一个被淘汰的集合
底层数组扩容为原数组的 2 倍;
add方法被 synchronized 修饰,表示线程安全;
五、迭代器
1、面试题:iterator()、Iterator、Iterable的关系?
-- Iterable<E>是一个接口,Collection 接口继承 Iterable<E>接口;
-- iterator() 是 Iterable<E>接口 中的一个抽象方法,在具体实现类中被实现;
-- iterator() 方法的返回值类型 是 Iterator<E> 接口,这个接口有两个经典抽象方法:
1、hasNext();
2、next();
-- hasNext()和next()这两个方法在 ArrayList 的内部类 Itr中被实现;
-- hasNext() 方法具体实现是 判断计数器是否与当前size相同;
-- next() 方法具体实现是 根据下标返回数组中的元素;
2、ListIterator
是一个增强型的 iterator ,提供了更多对 List的操作;
TreeMap实现类
唯一,有序(按升序或者降序)
key对应的类型一定要实现比较器(内部比较器、外部比较器)
底层:二叉树,每个节点都是一个 Entry<K,V>对象
每个节点对应的类型:Entry<K,V>,这个类包含6个属性,key、value,parent等信息
六、HashMap
1、原理
分类 | 解释 |
构造器 | -- key唯一,且可以为 null;value没特别要求; -- 底层数据结构是 数组 + 链表; -- 提供空参构造器和带参构造器,如果使用带参构造器指定数组长度,会默认将这个长度改成 2 ^ n; -- 底层数据结构有个 Entry<K,V>类型的主数组,默认长度为16; -- 且这个数组的长度只能是 2 的 n 次方; -- 目的是使用"与运算"代替"取余运算"提高效率; -- 同时减少"哈希冲突" ; -- 在 put 方法中 ,对key的hashCode 和 数组长度做取余运算,来确定存放位置下标; -- 有个加载因子的属性,默认是0.75; -- 有个size属性,每put一个元素,size+1; -- 有个threshold属性,用来表示当前map的扩容边界值; |
put方法 | -- 在put方法中,先对key求hashCode,再用这个hashCode 和 主数组 取余,来确定主数组的下标; -- 如果在主数组中当前下标处没东西,就直接把元素放在这个位置; -- JDK 1.7 头插法; |
扩容 | -- size >= 扩容临界值,且当前节点的数组下标位置已有数据,会触发扩容; -- 扩容为原数组的2倍,将旧数组的数据存到新数组中; -- 重新计算当前节点的下标; |
面试题 | 1、为什么加载因子默认为0.75? -- 如果太大,会增加hash碰撞概率,导致链表太长,查询效率低; -- 如果太小,会导致主数组容易扩容,链表太短,浪费空间; 2、为什么主数组长度一定是 2 ^ n? -- 在put方法中通过key的hashCode计算数组下标时,使用与运算代替取余运算提高效率; -- 降低下标位置冲突概率; |
2、JDK 1.8 HashMap 原理
分类 | 解释 |
底层数据结构 | -- 数组 + 链表(单向链表) + 红黑树。 -- 数组的类型是 Node<K,V> 是HashMap的一个内部类。 |
树化 | -- 链表的个数达到8个且主数组长度达到64时才会树化,否则只进行扩容操作。 |
初始化 | -- 调用构造器创建HashMap对象只会声明一个主数组的类型。 -- 在put第一个元素的时候才会对这个主数组做初始化操作,长度为16。 |
七、TreeMap
1、原理
-- 底层是二叉树结构。
-- 如果没指定比较器就使用key的 hashCode 做排序,来确定新的节点放左边还是右边。
-- 初始化:声明一个comparator(比较器)。
-- 底层维护了一个root节点,第一次put键值对就保存进root节点内。
-- 之后put键值对就使用比较器确定新节点放到左边还是右边。
八、HashTable
1、原理
-- 初始化:默认容量 = 11,加载因子 = 0.75f,创建一个 Entry<?,?>[] table;
-- 底层维护了一个table(数组),数组中每个元素都有next属性,结构和HashMap一样,保存键值对。
-- put方法:
有返回值,如果key重复,返回上一次保存的value,新value覆盖旧value。
如果key不重复,计算hashCode且取余后确定key的位置。
被 synchronized 修饰,表示线程安全。
九、LinkedHashMap
1、原理
-- 是HashMap的子类。
-- 初始化:创建一个HashMap,声明输出顺序以插入顺序为准。
-- put方法:就是HashMap的put方法。
-- get方法:是自己的,取到节点后做后置处理。
2、比较
HashMap | HashTable | LinkedHashMap |
JDK 1.2 诞生。 线程不安全,效率高。 允许key = null。 | JDK 1.0 诞生。 线程安全,效率低。 不允许key = null。 | -- 可以实现按输入顺序来输出; |
十、TreeSet实现类
唯一,有序(按升序或者降序);
-- 借助 TreeMap 实现,每个节点的value都是一个Object对象 --> 只在key的位置保存数据;
-- 所以 TreeSer存储数据的特点就是 TreeMap的key的特点;
十一、HashSet实现类
-- 借助 HashMap实现,每个节点的value都是一个Object对象 --> 只在key的位置保存数据;
-- 所以 HashSet存储数据的特点就是 HashMap的key的特点;
十二、ConcurrentHashMap
1、原理
-- 初始化:只声明一个对象,不申请存储空间。
-- put方法:初次put,创建存储空间,长度 = 16,这个过程是线程安全的。
-- 在写数据过程中都是数据安全的,只锁住当前占用数组位置的数据,提高了执行效率。
2、比较
ConcurrentHashMap | HashTable | HashMap |
--线程安全 --细粒度锁,高效 --锁在片区上,16个片区 | --线程安全 --粗粒度锁,低效 --锁在主数组上 | --线程不安全 --无锁,高效 |
十三、同步集合
public class Test01 {
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
test01(list1);
System.out.println("=============================");
List<String> list2 = new ArrayList<>();
test02(list2);
}
}
1、线程不安全示例
public static void test01(List<String> list) {
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
//并发向集合中添加10000个数据
for (int i = 0; i < 10000; i++) {
//每个线程处理任务:run方法中的内容就是线程单元,每个线程执行的部分
executorService.execute(new Runnable() {
@Override
public void run() {
list.add("aaa");
}
});
}
//关闭线程池资源
executorService.shutdown();
//监控线程是否执行完毕
while (true){
//线程都执行完返回true
if (executorService.isTerminated()){
System.out.println("执行完成");
//执行完毕以后看一下集合中元素的个数
System.out.println(list.size());
break;
}
}
}
2、线程安全示例
public static void test02(List<String> oldlist) {
List<String> list = Collections.synchronizedList(oldlist);
//创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
//并发向集合中添加10000个数据
for (int i = 0; i < 10000; i++) {
//每个线程处理任务:run方法中的内容就是线程单元,每个线程执行的部分
executorService.execute(new Runnable() {
@Override
public void run() {
list.add("aaa");
}
});
}
//关闭线程池资源
executorService.shutdown();
//监控线程是否执行完毕
while (true){
//线程都执行完返回true
if (executorService.isTerminated()){
System.out.println("执行完成");
//执行完毕以后看一下集合中元素的个数
System.out.println(list.size());
break;
}
}
}
十四、COW并发容器
1、Copy On Write 写时复制容器
写数据时先复制一个容器,新容器容量 +1,在加出来的位置写入新数据,修改老容器的指向。
执行过程:
1、此时有一个数组 N
2、需要向数组 N 添加数据时,先复制数组 N,得到 M
3、给 M 数组的长度 + 1
4、将新元素放到 M 中 加出来的长度的位置
5、把 M 的地址指向给 N
2、CopyOnWriteArrayList
add()
addIfAbsent() --> 使用这个方法不会添加重复数据,每次都要对数组进行遍历。
3、CopyOnWriteArraySet
底层容器是 CopyOnWriteArrayList
add() 方法就是上面的 addIfAbsent()
十五、Queue 队列
1、关系结构图
2、BlockingQueue 阻塞队列
Queue 继承自 Collection ,具有Collection的功能。
BlockingQueue继承自Queue,具有队列的特点。
具有阻塞的特点。
入队:
非阻塞队列:队列满时,放入第11个数据,数据丢失。
阻塞队列:队列满时,放入第11个数据,进入阻塞,等待队列中有空位。
出队:
非阻塞队列:队列空时,取数据得到 null。
阻塞队列:队列空时,取数据进入阻塞,等待队列中有数据。
3、ArrayBlockingQueue
基于数组实现的阻塞队列,具有阻塞队列的特点;
需要指定数组长度;
不能添加null,会报空指针异常;
因为是基于数组实现,所以不支持读写同时操作,效率略低;
底层插入元素和取出元素用的是同一把锁;
底层实现基于生产者消费者模型和线程通信;
① 基本方法对比
添加数据 | 取出数据 |
add() 非阻塞; 队列满时添加数据报错 | peek() 非阻塞; 取头部数据; 不移除这个数据; 队列空时获取到 null; |
offer() 不完全阻塞; 可以指定阻塞时间,阻塞时间到达时队列满返回 false | poll() 不完全阻塞; 取头部数据; 移除这个数据; 可以指定阻塞时间,时间到时队列空获取到 null; |
put()完全阻塞; 队列满时阻塞,一直满一直阻塞 | take() 完全阻塞; 取头部数据; 移除这个数据; 队列空时阻塞,一直空一直阻塞 |
② 源码解析
public class ArrayBlockingQueue<E> {
// 底层就是一个数组
final Object[] items;
// 取元素索引,初始值为0
int takeIndex;
// 放元素索引,初始值为0
int putIndex;
// 计数器,数组中元素个数
int count;
// 一把锁,这个锁在很多地方用到,所以定义为属性;
final ReentrantLock lock;
// 锁伴随的等待池 notEmpty
private final Condition notEmpty;
// 锁伴随的等待池 notFull
private final Condition notFull;
// 构造器
// 必须传入队列指定的容量
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
//传入指定的容量
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
//1、创建数组,指定长度
this.items = new Object[capacity];
//2、初始化锁和等待队列
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
// 两个基本方法,一个入队,一个出队
// 入队方法
private void enqueue(E x) {
// 底层数组赋值给 items
final Object[] items = this.items;
// 在对应下标位置放入元素
items[putIndex] = x;
// putIndex索引 做+1操作
if (++putIndex == items.length)
// 底层数组放到最后一格时,putIndex索引归0
// 为了给下一轮放元素做准备
putIndex = 0;
// 每放入一个元素,计数器+1操作
count++;
// 这一步很重要
// 涉及到线程通信
// 队列的底层数组放入一个元素后,会通知取数据的等待队列,将notEmpty队列的线程唤醒
notEmpty.signal();
}
//出队方法
private E dequeue() {
// 底层数组赋值给 items
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// 在对应下标位置取出元素,将元素作为返回值
// 此时takeIndex = 0
E x = (E) items[takeIndex];
// 对应位置元素取出后置为null
items[takeIndex] = null;
// takeIndex索引 做+1操作
if (++takeIndex == items.length)
// 底层数组取到最后一格时,takeIndex索引归0
// 为了给下一轮取元素做准备
takeIndex = 0;
// 每取出一个元素,计数器-1操作
count--;
if (itrs != null)
itrs.elementDequeued();
// 这一步很重要
// 涉及到线程通信
// 队列的底层数组取出一个元素后,这时数组有空位了;
// 会通知放数据的等待队列,将notFull队列的线程唤醒;
notFull.signal();
return x;
}
// put方法
public void put(E e) throws InterruptedException {
// 数据校验
checkNotNull(e);
// 获取锁,这个锁是唯一的,和take方法获取的锁是同一个
final ReentrantLock lock = this.lock;
// 上锁
lock.lockInterruptibly();
try {
// 判断当前队列的元素个数是否达到底层数组的最大额度,
// 如果队列已满就运行,放元素的线程进入等待队列
while (count == items.length)
// 进入等待队列 notFull
notFull.await();
// 入队操作
enqueue(e);
} finally {
// 解锁
lock.unlock();
}
}
// take方法
public E take() throws InterruptedException {
// 获取锁,这个锁是唯一的,和put方法获取的锁是同一个
final ReentrantLock lock = this.lock;
// 上锁
lock.lockInterruptibly();
try {
// 判断当前队列的元素个数是否为0
// 如果当前队列没有元素了,取元素的线程进入等待队列;
while (count == 0)
// 进入阻塞队列 notEmpty
notEmpty.await();
// 出队操作
return dequeue();
} finally {
// 解锁
lock.unlock();
}
}
}
4、面试题
问:put()和take()中的 while 为什么不用 if 替换?
答:
因为在放数据的线程被唤醒的瞬间,可能有其他线程先放入数据了,那么这时候底层数组是满的。
又因为线程在被唤醒后,会沿着await()后面的代码继续执行。
所以如果换成 if 会导致数据丢失。
5、LinkedBlockingQueue
基于链表实现,具有阻塞队列的特点。
可以不指定链表长度,不指定长度就是无限长。
支持读写同时操作,效率高。
底层插入元素和取出元素用的不是同一把锁。
以上ArrayBlockingQueue的方法,在LinkedBlockingQueue中均支持。
底层由其内部类Node<E> 实现。
Node 类有两个参数: E item,Node<E> next。
是一个单向链表。
① 源码解析
public class LinkedBlockingQueue<E>{
// 底层是一个单向链表
// 链表的节点就是这个内部类 Node
// Node类包含两个属性,一个存放当前元素,另一个存放下一个节点的地址
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
// 指定的链表长度,也是队列容量
private final int capacity;
// 计数器
private final AtomicInteger count = new AtomicInteger();
// 链表的头节点
transient Node<E> head;
// 链表的尾节点
private transient Node<E> last;
// 取元素的锁
private final ReentrantLock takeLock = new ReentrantLock();
// 取元素的等待池
private final Condition notEmpty = takeLock.newCondition();
// 放元素的锁
private final ReentrantLock putLock = new ReentrantLock();
// 放元素的等待池
private final Condition notFull = putLock.newCondition();
// 空参构造器,创建无界链表
// 但实际上创建的长度是Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
// 有参构造器
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity; // 设置最大容量
last = head = new Node<E>(null); // 创建一个节点,这个节点既是头节点也是尾节点
}
// 入队方法
private void enqueue(Node<E> node) {
// 这里有两步操作
// 1、尾节点的next指向新的node,这时链表节点多了一个
// 2、last的指向后移一格,这时last还是链表的最后一个节点
last = last.next = node;
}
// 出队方法
private E dequeue() {
// h指向 头节点
Node<E> h = head;
// h节点的下一个节点才是真正的第一个元素
Node<E> first = h.next;
// h的next指向自己
// 这一步是断开 头节点 与后面链表的关联
// 帮助垃圾回收器回收
h.next = h; // help GC
// head的指向后移一格,这时head还是链表的第一个节点
head = first;
// 获取‘头’元素
E x = first.item;
// 删除‘头’元素
first.item = null;
// 返回‘头’元素
return x;
}
// put方法
// 放入元素
public void put(E e) throws InterruptedException {
// 健壮性考虑
if (e == null) throw new NullPointerException();
// 创建一个 c = -1
int c = -1;
// 将放入的元素封装为一个Node对象
Node<E> node = new Node<E>(e);
// 拿到放入元素的锁
final ReentrantLock putLock = this.putLock;
// 拿到计数器
final AtomicInteger count = this.count;
// 上锁
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
// 放数据操作
enqueue(node);
// 这一行有两个操作
// 1、c = count
// 2、count + 1 计数器+1
c = count.getAndIncrement();
// 这一步相当于判断 count < capacity
// 如果count < capacity = true,那就意味着当前队列没满
// 唤醒放元素的等待池中的线程
if (c + 1 < capacity)
// 在队列没满的时候,就可以唤醒放元素的等待池中的线程
notFull.signal();
} finally {
// 解锁
putLock.unlock();
}
// 这句判断相当于 count == 1
// 如果队列中算上刚才放完的元素只有一个
// 那么就通知取元素的线程
if (c == 0)
signalNotEmpty();
}
}
6、SynchronousQueue
特点:
这个队列必须先从队列中取出元素,然后才能向队列中添加元素。
这个队列连1个容量都没有。
优点:
方便、高效地进行线程间数据的传送。
效率极高,且不会产生队列中数据争抢问题。
① SynchronousQueue 工作图例
② 测试代码
一直蹲着等着取,有一个取一个。
7、PriorityBlockingQueue
是一个有顺序的阻塞队列,放元素的时候没体现顺序性,取元素的时候才会体现顺序性。
底层支持自动扩容,所以认为是无限大的队列。
队列中的元素必须实现比较器。
8、DelayQueue
是一个无界的阻塞队列。
用来存放实现了Delayed接口的对象。
① 案例代码
package allwe.thread08;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class User implements Delayed {
private int id;
private String name;
private long endTime;
public User() {
}
public User(int id, String name, long endTime) {
this.id = id;
this.name = name;
this.endTime = endTime;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getEndTime() {
return endTime;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
// 看剩余时间
@Override
public long getDelay(TimeUnit unit) {
// 剩余时间 《 0
return this.getEndTime() - System.currentTimeMillis();
}
//看谁先被移除
@Override
public int compareTo(Delayed o) {
// 到期时间比较
User ohter = (User)o;
return ((Long) this.getEndTime()).compareTo((Long) ohter.getEndTime());
}
}
package allwe.thread08;
import java.util.concurrent.DelayQueue;
public class Test {
DelayQueue<User> a = new DelayQueue<>();
public static void main(String[] args) {
Test queue = new Test();
//添加用户
User user1 = new User(1,"张三",System.currentTimeMillis() + 5000);
User user2 = new User(1,"李四",System.currentTimeMillis() + 10000);
User user3 = new User(1,"拉拉",System.currentTimeMillis() + 2000);
queue.login(user1);
queue.login(user2);
queue.login(user3);
//一直监控
while (true){
//到期的话,就自动下线
queue.logout();
// 如果都下线了,就停止监控
if (queue.onlineSize() == 0)
break;
}
}
public void login(User user){
a.add(user);
System.out.println("用户:[" + user.getId() + "],[" + user.getName() + "]已经登录,预计下机时间为:" + user.getEndTime());
}
public void logout(){
System.out.println(a);
try {
User user = a.take();
System.out.println("用户:[" + user.getId() + "],[" + user.getName() + "]已经下机");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//在线人数
public int onlineSize(){
return a.size();
}
}