java的集合框架
java 集合
在java Collection 接口上面 我们可以看到和它相关的一系列子接口以及实现类(map 在这里的原因, 可能因为它也是一个容器趴) Collection 接口只是为了一系列的容器操作提供了抽象的定义,这些定义在不同的子类中具体着不同的实现, 除此之外还定义了一些抽象的子类 AbstractCollection 在这个抽象类中 提供了一部分常用方法的通用实现, 可交由子类来重写来适应自己。
java 集合主要的继承关系
java 集合主要有两个接口, 分别是 Collection、 Map
- Collection
- Set
- List
- Queue
- Map
* @see Set
* @see List
* @see Map
* @see SortedSet
* @see SortedMap
* @see HashSet
* @see TreeSet
* @see ArrayList
* @see LinkedList
* @see Vector
* @see Collections
* @see Arrays
* @see
* @since 1.2
*/
public interface Collection<E> extends Iterable<E> {}
Set
Set集合中的元素是无序, 能够对元素进行去重! 主要实现类如下
- HashSet
- 底层使用HashMap
- TreeSet
- 底层使用二叉树
- LinkedHashSet
- 底层使用LinkedHashMap
- CopyOnWriteArraySet
- CopyOnWriteArrayList
- 添加元素的时候采用写入时复制的原则
- CopyOnWriteArrayList
常用操作
1 添加元素、遍历集合
public static void main(String[] args) {
HashSet<Integer> set = new HashSet<>();
for (int i = 0; i < 10; i++) {
set.add(i);
}
for (Integer integer : set) {
System.out.println(integer);
}
// 遍历的时候 就可以看到set中 只有一份 0 - 9 去重!
for (Iterator<Integer> iterator = set.iterator(); iterator.hasNext(); ) {
System.out.println(iterator.next());
}
}
2 底层使用的是HashMap
public HashSet() {
map = new HashMap<>();
}
List
List 中的元素是有序的,并且可重复。
- ArrayList
- 数组做为自己的底层实现
- LinkedList
- 链表做为自己的底层实现
- 提供了操作头部、尾部元素的方法
- Vector
- 数组做为自己的底层实现
- 并且所有的方法, 都是用synocrized修饰,访问的效率没有 ArrayList 高
- CopyOnWriteArrayList
- 数组实现, 使用写入时复制的原则
- 线程安全
ArrayList 的常见 API
private static void test(List<Integer> list) {
for (int i = 0; i <= 10; i++) {
list.add(i);
}
for (int i = 0; i <= 10; i++) {
list.add(i);
}
list.remove(5);
System.out.println(list.indexOf(3));
System.out.println(list.lastIndexOf(4));
Object[] array = list.toArray();
list.set(1, 2);
System.out.println(list.remove(2));
System.out.println(list.get(0));
System.out.println(list.size());
System.out.println(list.contains(1));
for (Integer integer : list) {
System.out.print(integer + " ");
}
}
LinkedList 具有一些特殊的操作
LinkedList 底层是链表来实现的, 所以他在查找的时候是比较慢的, 但是在删除元素和添加元素的时候会快一点。 由于 LinkedList 提供了一些对于头部尾部元素操作的API, 所以完全可以当作栈和队列来用。
- 链表查找元素 只能一个一个遍历
- 链表删除、添加元素 改变节点的指针指向即可
private static void testLinkList(LinkedList<Integer> linkedList) {
linkedList.push(2);
/**
* 返回头元素 、尾部元素, 添加 头、尾部元素
*/
System.out.println(linkedList.getFirst());
System.out.println(linkedList.getLast());
linkedList.addFirst(2);
linkedList.addLast(3);
linkedList.removeLast();
/**
* 返回头元素 、尾部元素, 但是不删除
*/
Integer peek = linkedList.peek();
System.out.println(peek);
Integer integer = linkedList.peekFirst();
System.out.println(integer);
Integer peekLast = linkedList.peekLast();
System.out.println(peekLast);
/**
* 返回头元素 、尾部元素, 并且删除元素
*/
Integer poll = linkedList.poll();
System.out.println(poll);
Integer pollFirst = linkedList.pollFirst();
System.out.println(pollFirst);
Integer pollLast = linkedList.pollLast();
System.out.println(pollLast);
}
Vector
Vector 也是 由数组来完成底层实现的集合, 相应着它也是一个有序的集合, 并且由于 自身的方法都被synchronized 修饰过了,所以它是一个线程安全的!
- 数组底层实现
- synchronized 线程安全
public synchronized int capacity() {
return elementData.length;
}
Map
我们常用的 Map中继承关系
- Map key 不可重复, value可以重复
- HashMap
- 线程不安全
- 底层数据 + 链表 + 红黑树
- 允许key一次为空
- HashTable
- 线程安全
- 底层哈希表
- key value都不能是空
- TreeMap
- 线程不安全
- 采用二叉树
- HashMap
HashTable
HashTable 线程安全的原因, 方法都用了 synchronized 修饰!
public synchronized int size() {
return count;
}
TreeMap
TreeMap 是SortedMap的子类, 所以它是一个有序的, 就是在存放映射关系的时候, 会安装key进行一个排序。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
}
public interface NavigableMap<K,V> extends SortedMap<K,V> {}
并发问题
什么是并发 和并行
并发的含义
- 在只有一个处理器的机器上运行的多个线程
- 多个线程按照时间片轮转的方式交替执行
- 线程A 在执行任务
- 线程B 也在执行任务
- 线程A 操作一半的时候, 时间片轮转了
- 线程B 开执行自己的任务
- 并发中 线程之间是在抢占资源的
并行的含义
- 在多个处理器额机器上运行多个线程
- 一个时间点
- 处理器 一 在执行任务A
- 处理器 二 在执行任务 B
- 并行 中线程不需要抢占资源
小结
单核的CPU 由于每个时间点只能执行一个进程, 但是采用进程调度算法之后使得线程之间可以交替执行, 从而造成一种多个进程可以同时执行的假象, 这就是并发!
多核的CPU在执行进程的时候, 可以一个核心来执行一个进程, 这样就是进程之间的并行! 但是当进程的数量大于核数的时候就会变成并发了!
- 两核、A B C 三个进程
- 核 一 执行 A
- 核 二 执行 B
- 此时 进程A B 之间在并行!
- 核一 停止的 A 的执行 、 开始执行C
- 此时 A C 就是处于一个并发!
为什么并发下不安全
下面的测试程序, 对于sum 的累加操作由10个线程来执行,但是最终的执行结果每次都不相同, 原因就是并发。
- 首先sum++ 不是一个原子的操作, 会转化为若干条的机器指令, 在执行++的时候也许会被打断, 原子性。
- 线程之间的数据共享问题, 线程A 修改了数据, 线程B 不能立马知道最新的数据, 可见性。
- CPU在执行的时候并没有按照原有程序的顺序执行, 指令重排。
解决的办法就是围绕着上面的原因来处理! 变成原子操作, 保证可见性, 禁止指令重排, 或者加锁, 让线程逐个执行!
public class Test {
static Lock lock = new ReentrantLock();
static Integer value =0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
add();
}, "" + i).start();
}
// 保证上面的线程都已经执行完毕了
TimeUnit.SECONDS.sleep(5);
System.out.println(Test2.value);
}
/**
*
*/
public static void add() {
for (int i = 0; i < 10000; i++) {
Test2.value++;
}
}
}
使用锁
- synchronized
- Lock
public synchronized static void add2() {
for (int i = 0; i < 10000; i++) {
Test2.value++;
}
}
public static void add3() {
for (int i = 0; i < 10000; i++) {
synchronized (Test2.value) {
Test2.value++;
}
}
}
public static void add4() {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
Test2.value++;
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
不安全集合
List
解决办法
- vector
- synchronizedList
- CopyOnWriteArrayList
// 测试 list 的线程不安全
public class TestList {
// Exception in thread "6" java.util.ConcurrentModificationException
public static void main(String[] args) {
List<Integer> list;
list = new ArrayList<Integer>();
// list = new Vector<>();
/**
* 解决方案
* 1、调用集合工具包 Collections.synchronizedList();
* 2、使用 vector
* 3、new CopyOnWriteArrayList<Integer>();
*/
List<Integer> integerList = Collections.synchronizedList(list);
// list = new CopyOnWriteArrayList<Integer>();
//
for (Integer i = 0; i < 30; i++) {
Integer temp = i;
new Thread(() -> {
list.add(temp);
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
Set
- synchronizedSet
- CopyOnWriteArraySet
// Exception in thread "43" java.util.ConcurrentModificationException
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<String>();
/**
* 1、Collections.synchronizedSet(set);
* 2、new CopyOnWriteArraySet<>();
*/
Set<String> set1 = Collections.synchronizedSet(set);
CopyOnWriteArraySet<Object> set2 = new CopyOnWriteArraySet<>();
for (int i = 0; i <
50; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set.toString());
}, String.valueOf(i)).start();
}
}
}
Map
- synchronizedMap
- ConcurrentHashMap
public class MapTest {
public static void main(String[] args) {
Map<Object, Object> map;
// map = new HashMap<>();
// Exception in thread "11" java.util.ConcurrentModificationException
// map = Collections.synchronizedMap(new HashMap<>());
map = new ConcurrentHashMap();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
map.put(UUID.randomUUID().toString().substring(0, 2),
UUID.randomUUID().toString().substring(0, 2));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
小结
解决集合线程安全的方法还是按照线程安全的原因来处理的! !
- 加锁来保证同一个时刻只有一个线程可以操作这个资源。
- synchronized
- Lock
- 使用其他的手段来保证线程之间数据的可见性问题。
- CopyOnWrite
- ConcurrentHashMap
- 内存分段
synchronized
synchronized
synchronized 本身是一个关键字, 底层使用的是阻塞队列。可以在并发环境下来保证数据的一致性。主要的特点。
- 不需要显示的解锁和加锁
- 基于阻塞对列, 可能会造成线程的阻塞
- 可以作用与同步方法以及需要同步的资源
- 锁住的是资源的使用者
- 同步方法来说, 锁住的就是方法的调用者
- 对于需要同步的资源来说, 锁住的就是该资源的使用者
CopyOnWrite
CopyOnWrite
写入时复制思想, 当多个线程同时访问系统资源的时候, 他们都会得到该资源的指针。当出现一个线程要修改该资源的时候, 系统会给该线程一个资源的备份, 此时系统中该资源并没有做出修改, 当线程修改完该资源的时候会用新的数据来替换系统中旧的数据, 在此期间其他线程依旧可以读取该资源,但是读取到的都是旧的数据。
- A 修改 资源 a , B 读取
- A 得到资源a的副本 进行修改, B 继续读取资源 a
- A修改结束后,用新的资源a 替换 之前的 资源a
- 这样的操作步骤, 可以在最终的时候保证数据的一致性,在替换旧资源的时候也会出现数据的不一致。
合适场景
-
读多 写少的情况, 读取的时候都能得到最终一致的数据, 并且是一种没有使用锁的机制
-
和读写锁存在着区别
- 读写锁是一种细粒度的锁, 在发生读写互斥的情况下会加锁
-
https://www.cnblogs.com/jmcui/p/12377081.html#_label0