1,常用map
AbstractMap 的成员变量:transient, volatile修饰
transient volatile Set<K> keySet;//keySet, 保存 map 中所有键的 Set
transient volatile Collection<V> values;//values, 保存 map 中所有值的集合
1)HashMap
HashMap 常用方法:
方法 | 描述 | 备注 |
---|---|---|
clear() | 删除 hashMap 中的所有键/值对 | |
Object remove(Object key) | 从映像中删除与key相关的映射 | |
clone() | 复制一份 hashMap | |
Object put(Object key, Object value) | 添加。若k存在则覆盖v,并返回关键字的旧值,否则返回null | |
void putAll(Map t) | 将来自特定映像的所有元素添加给该映像 | |
putAll() | 将所有键/值对添加到 hashMap 中。参数为一个Map | |
putIfAbsent() | 如果 hashMap 中不存在指定的键,则将指定的键/值对插入到 hashMap 中。 | |
containsKey() | 检查 hashMap 中是否存在指定的 key 对应的映射关系。 | |
containsValue() | 检查 hashMap 中是否存在指定的 value 对应的映射关系。 | |
replace() | 替换 hashMap 中是指定的 key 对应的 value。 | |
replaceAll() | 将 hashMap 中的所有映射关系替换成给定的函数所执行的结果。 | |
getOrDefault() | 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值 | |
forEach() | 对 hashMap 中的每个映射执行指定的操作。 | |
entrySet() | 返回 hashMap 中所有映射项的集合集合视图。 | |
keySet() | 返回 hashMap 中所有 key 组成的集合视图 | list = new ArrayList(map.value()); map转list |
values() | 返回 hashMap 中存在的所有 value 值。 | |
merge() | 添加键值对到 hashMap 中 | |
compute() | 对 hashMap 中指定 key 的值进行重新计算 | prices.compute(“Shoes”, (key, value) -> value - value * 10/100)如果 key 对应的 value 不存在,则返回该 null,如果存在,则返回重新计算后的值。 |
computeIfAbsent() | 对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hasMap 中 | |
computeIfPresent() | 对 hashMap 中指定 key 的值进行重新计算,前提是该 key 存在于 hashMap 中。 | |
int size() | 返回当前映像中映射的数量 | |
boolean isEmpty() | 判断map是否有元素。建议使用 |
1> 特点
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
- 访问速度快
- 无序
- k、v都可以为null,k不可重复
对于key为null的值,在talbe[0]链表中存储。 - 非线程安全;
线程安全时用concurrentHashMap。
2>原理—哈希表(数组)+ 单向链表 + 红黑树
- 哈希函数:hash(key)&(len-1)
实现了均匀的散列,但比直接%len效率高,因为len为2的整数次幂,所以len-1为奇数(最后一位为1),保证了hash值通过&运算后,得到和原hash的低位相同,减少碰撞。 - 哈希冲突:链表定址法。
通过equals判断当前key是否存在,存在则覆盖对应key;
key不冲突,jdk1.7时通过头插法插入链表,新结点作为头结点存入哈希表;jdk1.8时使用尾插法插入链表。
get步骤:1)计算hash值得到哈希数组下标;2)通过equals方法确定key值;3)从头结点开始遍历链表看key是否相等,相等返回。
查找和插入复杂度:O(1) - 当hash表长度>64,链表的长度超过8时,链表会自动转化为红黑树
jdk8的时候加入了红黑树查找时间复杂度由O(N)变成O(lgN)。同时链表采用尾插法。之前的头插法虽然效率高一点,但是并发情况下容易死循环,而头插尾插对红黑树没什么影响。
两个条件有一个不满足则退化为链表。 - 扩容
hash表初始长度是16,每次扩容长度必须是 2的幂。默认加载因子是0.75(超过75%就会开始扩容,容量变成原来的2倍+1)。
每一次put新数据时,都会检查当前容量,如果需要扩容,所有元素都需要按照新的hash算法被算一遍。ReHash代价交够。
因为HashMap的key是int类型,所以最大值是231次方,但是查看源码,当到达 230次方,即MAXIMUM_CAPACITY
之后,便不再进行扩容。 - 线程不安全,支持快速失败。
3>高并发情况下,为什么HashMap出现死锁?
ReHash过程:每个元素重新算hash值,将链表翻转(遍历每个bucket上的链表,然后用头插法插入对应的bucket上的链表中)。对于同时准备扩容的两个线程1和2,如下图:
源码如下:
while(null != e) {
Entry<K,V> next = e.next; //线程1还没有执行这句 中断了
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);//计算hash值
}
int i = indexFor(e.hash, newCapacity);//获取扩容后这个结点对应索引位
e.next = newTable[i];//这三行代码表示头插法插入
newTable[i] = e;
e = next;
}
(1)线程1,中断,线程2 reHash
(2)线程2将原表 bucket 1 处的链表分发到 新表 bucket 1 和 bucket 3 上(hash值的后2位,第一位不同,则不是01就是11),分散到 bucket 3上的值有两个, key(3), key(7),遍历原表Bucket 1 上的 链表,采用头插法,结果就是 链表反转且还属于新表此bucket的元素放到 此bucket上。此时 key(7) -> key(3) -> null
(3)此时线程 2 被中断,线程 1调度。
此时线程 1 中 e 是 key(3)-> null,根据源码继续将e插入对应的table[3]链表,头插法,结果为:key(3)->key(7)->key(3),这是一个循环链表。
e = next, e是null,才能跳出循环。上面e永远不会空,死循环了。
4>常量集合
public static final Map<String, String> myMap = Collections.unmodifiableMap(new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("1", "11");
put("2", "22");
}
});
5>空集合
//空集合: 这个空集合没有add方法。 emptyList,emptySet同理
return Collections.emptyMap();
6>iterator遍历
//可以使用keySet遍历 也可以私用values遍历,推荐使用Entry遍历
Set<Map.Entry<String,String>> entrys = map.entrySet();
Iterator<Map.Entry<String,String>> it = entrys.iterator();
while(it.hasNext()){
Map.Entry<String,String> entry = it.next();
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
2)Hashtable
JDK1.0。
- k v不可为空;
- 线程安全但效率低下;
是线程安全的hashMap,在hash表上加了全局synchronized(并发下只有一个线程能写hash表)。 推荐使用concurentHashMap。 - 哈希函数:直接使用对象的hashCode;
- 扩容:hash数组默认大小是11,增加的方式是old*2 + 1。
3)ConcurrentHashMap(线程安全且高效)
jdk1.5 java.util.concurrent(简称JUC)包下。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
i>(jdk7):ReentrantLock+segment+HashEntry
在hashmap的基础上,对hash表的每个数组分段加锁(ReentrantLock):
- 1个Segment包含了一个HashEntry数组,每个HashEntry又是一个链表结构。
- segment继承自ReentrantLock,ReentrantLock用来保证segment的线程安全,分段锁提高了效率
- 对数组分段加锁,使用segment分片锁,这样一个线程只会锁住数组的一片,其他线程仍可以访问数组的其他片进行写操作和扩容
ii>(jdk8):synchronized+CAS+红黑树
1.8hashmap的基础上做了优化:
- 加锁
用synchronized对链表头结点加锁;(不影响其它元素读写) - 扩容(阻塞所有的读写、扩容操作)
多线程并发扩容,对原始数组进行分片,每个线程对分片进行扩容。 - size计算
并发量小的时候,通过CAS来实现size递增;
如果并发量大,则通过数组维持元素个数做负载。要增加元素个数,随机取数组中一个数来做CAS。 - 多线程写数据
通过CAS修改,失败后(哈希冲突)通过分段synchronized来写。 - 多线程读数据
无锁。
ii>原理
ConcurrentHashMap 的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable 和同步的HashMap 一样了。
- 修改数据CAS
ConcurrentHashMap并发控制的关键在于一个变量,如下所示:private transient volatile int sizeCtl;
sizeCtl被volatile关键字修饰是一个多线程共享的变量,当它的值为负数的时候说明某个线程正在操作这个Map,想要去操作这个Map的线程就要一直去竞争这个sizeCtl,没有得到这个变量的值就要一直自旋等待这个变量,当占用这个变量的线程操作完成后,要将这个变量的值设置回来,以便让其他线程走出自旋,竞争到该变量。
这种同步进制事实上是一种CAS的做法。 - get()方法的弱一致性
正是因为get 操作几乎所有时候都是一个无锁操作( get 中有一个readValueUnderLock 调用,不过这句执行到的几率极小),使得同一个Segment 实例上的put 和get 可以同时进行,这就是get 操作是弱一致的根本原因。 - clear()方法的弱一致性
因为没有全局的锁,在清除完一个segment 之后,正在清理下一个segment 的时候,已经清理的segment 可能又被加入了数据,因此clear返回的时候,ConcurrentHashMap 中是可能存在数据的。因此,clear 方法是弱一致的。 - ConcurrentHashMap 的弱一致性
在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException 异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap 迭代器弱一致的表现。
在这种迭代方式中,当iterator 被创建后,集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据,这样iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
4)TreeMap
- key有序(Unicode升序),key非null;
基于红黑树实现,可以按照自然顺序或者自定义顺序自动排序,不允许插入null值,查找效率比较高,适合需要排序的场景。 - 非线程安全
5)LinkedHashMap
- key有序(插入顺序)
LinkedHashMap的构造函数里有个布尔参数accessOrder,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。
6)WeakHashMap
- 基于HashMap
- key不常使用时,允许GC
WeakReference。
7)ImmutableMap
不可变Map,也就是说初始化之后,不能再往里面put元素了,不然会报异常java.lang.UnsupportedOperationException。
初始化:
public class MapTest {
Map<String, String> myMapA = ImmutableMap.of("张三", "北京", "李四", "上海");
// 或者
Map<String, String> myMapB = ImmutableMap.<String, String>builder()
.put("张三", "北京")
.put("李四", "上海")
.build();
}
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
8) ConcurrentSkipListMap
juc下面的类。
- 线程安全的有序的哈希表
相当于线程安全的TreeMap - 原理:跳表
9)LinkedMultiValueMap
一个key对应多个value。
功能 | 方法 | 备注 |
---|---|---|
添加一个k-v | void add(K, V); | |
添加一个K对应多个v | void add(K, List); | |
设置一个k对应一个v | void set(K, V); | |
设置一个k对应多个v | void set(K, List); | |
移除一个k及其v | List remove(K); | |
清除所有v | void clear(); | |
K集合 | Set keySet(); | |
v集合 | List values(); | |
查询k对应的某个v | V getValue(K, index); | |
查询k对应的所有v | List getValues(K); | |
map大小 | int size(); | |
判空 | boolean isEmpty(); | |
判k的存在 | boolean containKey(K); |
2,常用List
1)常见用法
1>排序
Collections.sort 排序
//简单的list排序
List<String> list = ...
Collections.sort(list);
自定义排序:建立一个第三方类并实现java.util.Comparator接口。
package com.set;
import java.util.Set;
import java.util.TreeSet;
class MySort implements java.util.Comparator<Student2>{
//倒序 o2-o1 正序:o1-o2
public int compare(Student2 o1, Student2 o2) {
return o2.id-o1.id;
}
}
使用:
Set<Student2> set = new TreeSet<Student2>(new MySort());
set.add(student1);
set.add(student2);
2>初始化
//预防空指针
List<String> list = null;
List<String> newList = Optional.ofNullable(list).orElse(Lists.newArrayList());
3>常用方法
//是一个private全局变量,在添加和删除时进行修改。作为成员变量是保存在堆中的。
int size();
boolean isEmpty();
boolean contains(Object o);
//以正确的顺序返回list中元素的迭代器
Iterator<E> iterator();
boolean add(E e);
boolean addAll(Collection<? extends E> c);
//在指定位置插入指定集合
boolean addAll(int index, Collection<? extends E> c);
//如果指定元素存在list中,移除list中第一次出现的指定元素(实现类可以选择具体的实现)
boolean remove(Object o);
//判断list中是否包含某个集合
boolean containsAll(Collection<?> c);
//删除list中包含的Collection中的所有元素
boolean removeAll(Collection<?> c);
//保留list中包含的Collection中的所有元素
boolean retainAll(Collection<?> c);
//将该列表的每个元素替换为将该运算符应用于该元素的结果。
default void replaceAll(UnaryOperator<E> operator);
//对list中的元素排列
default void sort(Comparator<? super E> c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
//在指定位置上增加指定元素
void add(int index, E element);
//删除指定索引上的元素
E remove(int index);
//获取对象的第一个索引
int indexOf(Object o);
//获取对象的最后一个索引
int lastIndexOf(Object o);
//返回list的list 迭代器
ListIterator<E> listIterator();
//从指定位置返回list的迭代器
ListIterator<E> listIterator(int index);
//返回list的子list
List<E> subList(int fromIndex, int toIndex);
list转为数组:
Object[] toArray();
//在list的末尾插入元素(实现类可以选择插入的位置)
String[] array=list.toArray(new String[list.size()]);//可以指定类型
4>com.google.common.collect.Lists
import com.google.common.collect.Lists;
//将list等分成多个list
List<List<User>> parts = Lists.partition(users, 50);
//new LinkedList();
Lists.newLinkedList()
//new ArrayList()并且addAll
Lists.newArrayList();Lists.newArrayList("a","b");
2)ArrayList(推荐)
JDK1.2。
- 底层的数据结构:动态数组
- 查询快、尾插法修改快
地址连续,下标随机访问。O(1)
尾插法快(指定初始容量,性能甚至会超过LinkedList);
头插法慢(所有元素要后移一位) - 非线程安全;
- 扩容
当ArrayList中的元素超过它的初始大小时,ArrayList只增加50%的大小。(节约内存)
初始容量默认为10,扩容:((旧容量 * 3) / 2) + 1 - 有序(插入顺序);
- 可重复;
- 比较:equals重写了,比较的是存储的元素相等,而不是直接比较引用。
1>Double Brace Initialization初始化,增强可读性
ArrayList<String> lists2 = new ArrayList<String>(){ //这个括号相当于派生自ArrayList<String>的匿名类。如果我们将该匿名类实例通过函数调用等方式传到该类型之外,那么对该匿名类的保持实际上会导致外层的类型无法被释放,进而造成内存泄露。
{ //这个括号:由于匿名类中不能添加构造函数,因此这里的instance initializer实际上等于构造函数,用来执行对当前匿名类实例的初始化
add("test1");
add("test2");
}
};
实际上,任何类型都可以通过它来执行预初始化:
NutritionFacts cocaCola = new NutritionFacts() {{
setCalories(100);
setSodium(35);
setCarbohydrate(27);
5 }};
2>直接采用[]、{}的形式存入对象
JDK1.7对Java集合增强支持。
直接采用[]、{}的形式存入对象,采用[]的形式按照索引、键值来获取集合中的对象,如下 :
List<String> list=["item"]; //向List集合中添加元素
String item=list[0]; //从List集合中获取元素
Set<String> set={"item"}; //向Set集合对象中添加元素
Map<String,Integer> map={"key":1}; //向Map集合中添加对象
int value=map["key"]; //从Map集合中获取对象
3>常量集合
public static final List<String> myList = Collections.unmodifiableList(new ArrayList<String>() {
private static final long serialVersionUID = 1L;
{
add("a");
add("b");
}
});
public static final List<String> myList1 = new ArrayList<String>(Arrays.asList("Tom", "Jerry", "Mike"));
//常量数组 不允许修改 (没有add方法)
//list = Arrays.asList(str1,str2);str1和str2可以是public static final String
public static final List<String> OBJECTS_LIST = Collections.unmodifiableList(list);
//不可变的集合且长度只有1,可以减少内存空间
List<String> list=Collections.singletonList("111");
//返回的不可变列表组成的n个拷贝的指定对象。<T> List<T> nCopies(int n, T o)
list = Collections.nCopies(3, 0); //输出list [0, 0, 0]
4>空集合
//空集合: 这个空集合没有add方法。 emptyMap,emptySet同理
return Collections.emptyList();
5>判断两个List集合是否相等
list.containsAll(list1)
3)LinkedList
- 底层的数据结构:基于双向链表结构;
常用堆栈与队列的实现,地址不连续,查询慢,增删快(性能主要耗费在创建node节点)。 - 非线程安全
- 必须使用迭代器进行访问
使用IndexOf会遍历所有node,性能很差
4)Vector
JDK1.0。
- 底层是数据结构:动态数组
在内存中占用连续的空间; - 线程安全
是线程安全的ArrayList,通过Synchronized修饰。 - 可以设置增长因子;
当Vector中的元素超过它的初始大小时,Vector会将它的容量翻倍。
6)Stack(栈)
- 先进后出(FILO, First In Last Out)。
- 线程安全
继承于Vector。
7)CopyOnWriteArrayList
jdk1.5 java.util.concurrent(简称JUC)包。
i>概念
- 线程安全
相当于线程安全的ArrayList,它实现了List接口。
add方法是加锁的(ReentrantLock),适用于读多写少的场景。 - 写时复制(适用于数据量小的并发)
当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
慎用!如果数据量过大,每次add/set都要重新复制数组,代价高昂容易引起故障。 - 读的时候并发写读的是旧数据
public boolean add(E e) {
final ReentrantLock lock = this.lock;//加的是lock 锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);//将原容器的引用指向新的容器;
return true;
} finally {
lock.unlock();
}
}
8)Collections 类中提供的静态工厂方法创建的类
Collections.synchronizedXXX(),可以将指定的集合包装成线程同步的集合。比如,
List list = Collections.synchronizedList(new ArrayList());
Set set = Collections.synchronizedSet(new HashSet());
3,Set
Java.util.Set。
1)特点
- 不可重复
作用:询问某个对象是否在某个Set; - 允许null值;
- 无序。
2)方法
- 新建实例
Set<String> set = new LinkedHashSet<String>();
- 遍历
Iterator遍历; - 删除
移除的是值为"a"的项。不能按索引移除。
set.remove("a");
3)HashSet
- 基于hash表实现
地址不连续。
采用hashCode计算hash值。
自定义对象存入hashSet需要重新hashCode和equals方法。 - 非线程安全
- 无序
4)TreeSet
- 有序(Unicode升序)
存放对象不能排序则报错、可指定排序规则。
自定义的对象需要实现Comparable接口的 compareTo(object o)方法 - 原理:红黑树
- 非线程安全
- 不允许插入null值
5)LinkedHashSet
- 有序(插入顺序)
根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。
6)CopyOnWriteArraySet
juc包下。
- 线程安全
- 写时复制,读写分离
相当于线程安全的HashSet。
内部包含一个CopyOnWriteArrayList对象。
7)ConcurrentSkipListSet
juc包下。
- 线程安全
- 写时复制,读写分离
是线程安全的有序的集合(相当于线程安全的TreeSet);
通过ConcurrentSkipListMap实现。
4,Arrays数组
1)Array与Arrays的区别
①Array(数组类)
- 效率高,但容量固定且无法动态改变。
- 它无法判断其中实际存有多少元素,length只是告诉我们array的容量。
- 因此除了结构的初始化和销毁之外,数组只有存取元素和修改元素值的操作。
- Array可以存放对象类型、基本数据类型的数据,其中的元素的类型必须相同。
②Arrays(静态类)
此静态类专门用来操作array ,提供搜索、排序、复制等静态方法。
2)数组定义
int [] a;//一开始不定义长度
int [] a = new int[10];
float f[][] = new float[6][6];
float []f[] = new float[6][6];
float [][]f = new float[6][6];
float [][]f = new float[6][];
3)常用方法
1>Arrays.asList()
将数组转换成java.util.ArrayList类型;
注意:返回的是Arrays的内部类java.util.Arrays A r r a y L i s t ,而不是 j a v a . u t i l . A r r a y L i s t 。 j a v a . u t i l . A r r a y s ArrayList, 而不是java.util.ArrayList。java.util.Arrays ArrayList,而不是java.util.ArrayList。java.util.ArraysArrayList和java.util.ArrayList都是继承AbstractList,remove、add等方法AbstractList中是默认throw UnsupportedOperationException而且不作任何操作。java.util.ArrayList重了写这些方法而Arrays的内部类ArrayList没有重写,所以使用时会抛出异常。
2>Arrays.sort()
数组的排序;
char[] array = str.toCharArray();
Arrays.sort(array);
输出array,此时已排序
3>Arrays.binarySearch()
在排好序的array中二分查找元素。
4>Arrays.equals()
比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等。
5>Arrays.fill()
给数组赋初值。
类似C语言中的memset()应用
char *p = new char[90];
memset((void *)p, -2, 90);
//把90个char都赋成-2,因为C++里的char是一个byte(8bit);
java写法:
java.util.Arrays.fill( float[], float)
6>复制数组
当数据量很大时,复制的效率:System.arraycopy > clone > Arrays.copyOf > for循环
。
a>System.arraycopy
System类源码中给出了arraycopy的方法,是native方法,也就是本地方法,肯定是最快的。
//数据量小的时候,for可能快。
public static void arraycopy(Object src, //源数组
int srcPos, //源数组中的起始位置
Object dest, //目标数组
int destPos, //目标数据中的起始位置
int length) //要复制的数组元素的数量
b>clone
java.lang.Object类的clone()方法为protected类型,不可直接调用,需要先对要克隆的类进行下列操作:
首先被克隆的类实现Cloneable接口;
然后在该类中覆盖clone()方法,并且在该clone()方法中调用super.clone();
这样,super.clone()便可以调用java.lang.Object类的clone()方法。
//被克隆的类要实现Cloneable接口
class Cat implements Cloneable {
private String name;
private int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
//重写clone()方法
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Clone {
public static void main(String[] args) throws CloneNotSupportedException {
Cat cat1 = new Cat("xiaohua", 3);
System.out.println(cat1);
//调用clone方法
Cat cat2 = (Cat) cat1.clone();
System.out.println(cat2);
}
}
c>Arrays.copyOf
Arrays.copyOf有十种重载方法,复制指定的数组,返回原数组的副本。具体可以查看jdk api。
d>Arrays.copyOfRange
将数组拷贝至另外一个数组
arr1 = Arrays.copyOfRange(arr, 0, index);
参数:
original:第一个参数为要拷贝的数组对象
from:第二个参数为拷贝的开始位置(包含)
to:第三个参数为拷贝的结束位置(不包含)
7)字符串和char[] 转换
//str->char[]
char[] array = str.toCharArray();
//char[]->str 切忌不可array.toString(),转换的是对象名
String key = new String(array);
8)数组计算
int[] a = {1,2,3};
++a[0]; //2,2,3
4)数组名
数组名不等价于指针,只有数组名作为函数参数时,才退化为指针,此时数组名的sizeof()就是指针大小,除了这种情况外,均是整个指整个数组的大小。
char *string_a=(char *)malloc(100*sizeof(char));//对于64位机:sizeof(string_a)为8
char string_b[100];//sizeof(string_b)为100.
6)Array和List区别
- 数组在内存中顺序存储,需要提前知道大小。
- List存储数据都转换为对象,存在不安全类型且需要拆箱装箱。
5,Iterator接口
1)概念
- Iterator是所有集合的总接口,其他所有接口都继承于它。
- 用于集合的安全遍历:移除和添加。
java.util.Iterator其接口定义如下:
public interface Iterator {
boolean hasNext(); //判断容器内是否还有可供访问的元素
Object next(); //返回迭代器刚越过的元素的引用,返回值是Object,需要强制转换成自己需要的类型
void remove(); //删除迭代器刚越过的元素
}
2)使用
①遍历List、Set、Map
Iterator iterator = list.iterator();
while(iterator.hasNext()){
String s = iterator.next();
if (s.contains("1")) {
iterator.remove();//安全删除
}
}
②安全删除
Iterator支持从源集合中安全地删除对象(在Iterator上调用remove方法)。
Iterator的remove()方法不仅会删除元素,还会维护一个标志,用来记录目前是不是可删除状态,例如,你不能连续两次调用它的remove()方法,调用之前至少有一次next()方法的调用。
如果在循环的过程中调用集合的remove()方法,就会导致循环出错,引发ConcurrentModificationException异常。
原因:Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast原则 Iterator 会马上抛出java.util.ConcurrentModificationException 异常。所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。正确的做法是不用list.remove(),使用Iterator.remove();
4) ListIterator
功能:
- 双向遍历:next() 、 previous()
- 在遍历时修改 List 的元素;
- 遍历时获取迭代器当前游标所在位置。
6,Queue 队列
1)java.util.List(非阻塞队列)
jdk5.0以前使用。
2)PriorityQueue(优先队列)
JDK1.5。
1>特性
- 自动排序
对于基本数据类型的包装类(如:Integer,Long等),默认asc(小顶堆)排序,取最小值出队。
对于自定义类,需要自定义比较器。
PriorityQueue<MaxNum> pq = new PriorityQueue(new Comparator<MaxNum>(){
//大于0表示n2>n1
public int compare(MaxNum n1, MaxNum n2){
//降序排列
return n2.num - n1.num;
}
});
- 非阻塞队列;
- 线程不安全;
- 不允许null值。
2>使用
方法 | 说明 | 备注 |
---|---|---|
peek() | 返回队首元素 | |
poll() | 返回队首元素,队首元素出队列。 | 默认asc排序,出min |
add() | 添加元素 | |
size() | 返回队列元素个数 | |
isEmpty() | /判断队列是否为空,为空返回true,不空返回false |
3>原理
Object[]
存储数据;
- 跟节点:数组实现的二叉堆,根节点在index=0位置上。
- 左孩子:数组n位置上的元素,其左孩子在[2n+1]位置上。
- 右孩子:数组n位置上的元素,其右孩子在 2(n+1) 位置上。
- 父节点:数组n位置上的元素,其父节点在 (n-1)/2 位置上。
- 最大不能超过Integer.MAX_VALUE - 8
4>topN计算
package sort;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Random;
/**
* 固定容量的优先队列,模拟大顶堆,用于解决求topN小的问题
* @author 张恩备
* @date 2016-11-25 下午02:29:31
*/
public class FixSizedPriorityQueue<E extends Comparable> {
private PriorityQueue<E> queue;
private int maxSize; // 堆的最大容量
public FixSizedPriorityQueue(int maxSize) {
if (maxSize <= 0)
throw new IllegalArgumentException();
this.maxSize = maxSize;
this.queue = new PriorityQueue(maxSize, new Comparator<E>() {
public int compare(E o1, E o2) {
// 生成最大堆使用o2-o1,生成最小堆使用o1-o2, 并修改 e.compareTo(peek) 比较规则
return (o2.compareTo(o1));
}
});
}
public void add(E e) {
if (queue.size() < maxSize) { // 未达到最大容量,直接添加
queue.add(e);
} else { // 队列已满
E peek = queue.peek();
if (e.compareTo(peek) < 0) { // 将新元素与当前堆顶元素比较,保留较小的元素
queue.poll();
queue.add(e);
}
}
}
public List<E> sortedList() {
List<E> list = new ArrayList<E>(queue);
Collections.sort(list); // PriorityQueue本身的遍历是无序的,最终需要对队列中的元素进行排序
return list;
}
public void printResult(){
// 或者直接用内置的 poll() 方法,每次取队首元素(堆顶的最大值)
while (!queue.isEmpty()) {
System.out.print(queue.poll() + ", ");
}
}
}
final FixSizedPriorityQueue pq = new FixSizedPriorityQueue<Integer>(3);
for (int i=0; i<a.length; i++) {
pq.add(a[i]);
}
pq.printResult();
3)阻塞队列(JUC包)
常见阻塞队列对比:
队列 | 是否有界 | 是否加锁 | 是否线程安全 |
---|---|---|---|
ArrayBlockingQueue | 有界 | 加锁 | 数组 |
LinkedBlockingQueue | 可选有界 | 加锁 | 链表 |
PriorityBlockingQueue | 无界 | 加锁 | 数组 |
DelayQueue | 无界 | 加锁 | 数组 |
LinkedTransferQueue | 无界 | 无锁 | 链表 |
LinkedBlockingDeque | 可选有界 | 有锁 | 链表 |
1>ArrayBlockingQueue
- 数据结构:数组
内部还保存着3个变量,由于cpu缓存一致性协议,可能导致伪共享。
# 出队下标
/** items index for next take, poll, peek or remove */
int takeIndex;
# 入队下标
/** items index for next put, offer, or add */
int putIndex;
# 队列中元素数量
/** Number of elements in the queue */
int count;
- 线程安全
- 有界
初始化时必须指定队列的大小。 - 阻塞队列
生产者和消费者共用一把锁:读写操作上都需要锁住整个容器。==》吞吐量一般
2> LinkedBlockingQueue
- 数据结构:单向链表;
按 FIFO(先进先出)排序元素。 - 可选有界
指定队列的大小时,就是有界的;
不指定队列的大小时,默认是Integer.MAX_VALUE,相当于无界。==》但当生产速度大于消费速度时候,有可能会内存溢出。 - 阻塞队列
生产者和消费者分别采用了独立的锁来控制数据同步==》高并发的情况下put()有putLock锁,take()有putLock;吞吐量较高。
常用方法 | 说明 | 是否阻塞 |
---|---|---|
put() | 把元素加入到阻塞队列中。 如果阻塞队列没有空间,则调用此方法的线程被阻塞,直到有空间的时候再继续 | 阻塞 |
take()方法 | 取出排在阻塞队列首位的对象。若阻塞队列为空,则调用此方法的线程被阻塞,直到有新的对象被加入的时候再继续。 | 阻塞 |
offer() | 把元素加入到阻塞队列中,如果可以容纳,则返回true。如果不可以容纳,则返回false。 | 不阻塞 |
poll() | 取出排在阻塞队列首位的对象,若阻塞队列为空,则返回null,如果不为空,则返回取出来的那个元素。 | 不阻塞 |
3>LinkedBlockingDeque
- 数据结构:双向链表
- 双向并发阻塞队列:同时支持FIFO和FILO。
- 可选有界
- 有锁
4>ConcurrentLinkedQueue
- 数据结构:单向链表:
FIFO - 无界队列
4>ConcurrentLinkedDeque
- 数据结构:双向链表
同时支持FIFO和FILO。 - 无界队列
5>PriorityBlockingQueue
- 数据结构:数组;
按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。
PriorityBlockingQueue 里面存储的对象必须是实现Comparable 接口,队列通过这个接口的compare 方法确定对象的priority。
队列的元素并不是全部按优先级排序的,但是队头的优先级肯定是最高的。每取一个头元素时候,都会对剩余的元素做一次调整,这样就能保证每次队头的元素都是优先级最高的元素。 - 无界
- 加锁:不会阻塞生产者,但会阻塞消费者。
6>DelayQueue
- 数据结构:数组;
DelayQueue = BlockingQueue +PriorityQueue + Delayed。 - 无界
- 加锁:不会阻塞生产者,但会阻塞消费者。
放置实现了Delayed 接口的对象,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。这个队列里面所存储的对象都带有一个时间参数,采用take 获取数据的时候,如果时间没有到,取不出来任何数据。而加入数据的时候,是不会阻塞的(不会阻塞生产者,但会阻塞消费者)。
优势:
如果不使用DelayQueue,那么常规的解决办法就是:使用一个后台线程,遍历所有对象,挨个检查。对象数量过多时,可能存在性能问题;而且做不到按超时的时间顺序处理。 - 应用场景:缓存系统的设计。
缓存中的对象,超过了有效时间,需要从缓存中移出。使用一个线程循环查询DelayQueue,一旦能从DelayQueue 中获取元素时,表示缓存有效期到了。
7>Disruptor(推荐)
- 数据结构:环形数组;
当数据填满队列时(2^n - 1)时,再次添加数据会覆盖之前的数据。
位运算:数组大小必须为2的n次方,通过位运算提高效率。 - 有界
- CAS无锁
只有一个游标器Sequencer,它可以保证生产的消息不会覆盖还未消费的消息。一个变量避免伪共享的产生。
线程间通信性能远远高于ArrayBlockingQueue。
7,快速失败和安全失败
1)快速失败(fail—fast)
①概念
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
fail-fast是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
②原理
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
③场景
java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
2)安全失败(fail—safe)
①概念
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
②原理
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
③缺点
基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
④场景
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
8,JUC(java.util.concurrent)
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中的 Collection 实现等;
1)copyOn系列的List和Set
2)阻塞队列
3)ConcurrentHashMap
4)Condition
①Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
②作用
对锁进行更精确的控制。
对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如:
假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,“读线程"需要等待。
如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
③方法
// 造成当前线程在接到信号或被中断之前一直处于等待状态。
void await()
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 对应Object的wait();
boolean await(long time, TimeUnit unit)
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout)
// 造成当前线程在接到信号之前一直处于等待状态。
void awaitUninterruptibly()
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
boolean awaitUntil(Date deadline)
// 唤醒一个等待线程。对应Object的notify();
void signal()
// 唤醒所有等待线程。对应Object的notifyAll()
void signalAll()
不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
5)Semaphore类
控制某个资源可被同时访问的个数,通过构造函数设定一定数量的许可,通过acquire() 获取一个许可,如果没有就等待,而release() 释放一个许可。
下面的例子只允许5 个线程同时进入执行acquire()和release()之间的代码:
public class SemaphoreTest {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5 个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20 个客户端访问
for (int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println("Accessing: " + NO);
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放,如果屏蔽下面的语句,则在控制台只能打印5 条记录,之后线程一直阻塞
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
} // 退出线程池
exec.shutdown();
}
}
6)ReentrantLock类
具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大;
7)Future接口
表示异步计算的结果;
8)java.util.concurrent.CountDownLatch类(锁计数器)
①功能
可以用来在一个线程中等待多个线程完成任务的类。
②原理
CountDownLatch 是通过“共享锁”实现的。
i>创建
CountDownLatch latch = new CountDownLatch(3);
创建时传入一个int 类型参数,表示该“共享锁”最多能被count 个线程同时获取, 这个值只能被设置一次, 而且CountDownLatch 没有提供任何机制去重新设置这个计数值。
ii>await()函数
让线程阻塞等待其他线程,直到CountDownLatch 的计数值变为0,才继续执行之后的操作。
主线程必须在启动其他线程后立即调用await()方法:
WorkerThread t1 = new WorkerThread(latch);
WorkerThread t2 = new WorkerThread(latch);
WorkerThread t3 = new WorkerThread(latch);
t1.start();
t2.start();
t3.start();
latch.await();
iii>countDown()函数
这个函数用来将CountDownLatch 的计数值减1,如果计数达到0,则释放所有等待的线程。
由其他线程调用:latch.countDown();
③场景
一个任务,它需要等待其他的一些任务都执行完毕之后它才能继续执行。
比如:开5 个多线程去下载,当5 个线程都执行完了才算下载成功。
9)java.util.concurrent.CyclicBarrier(循环屏障)
①功能
这个类是一个可以重复利用的屏障类。它允许一组线程相互等待,直到全部到达某个公共屏障点,然后所有的这组线程再同步往后执行。
②原理
await()函数:每被调用一次,计数便会减少1(CyclicBarrier 设置了初始值),并阻塞住当前线程。当计数减至0 时,阻塞解除,所有在此CyclicBarrier 上面阻塞的线程开始运行。
③CountDownLatch 和CyclicBarrier 区别
(1) CountDownLatch 的作用是允许1 个线程等待其他线程执行完成之后,它才执行;而CyclicBarrier 则是允许N 个线程相互等待到某个公共屏障点,然后这一组线程再同时执行。
(2) CountDownLatch 的计数器的值无法被重置,这个初始值只能被设置一次,是不能够重用的;CyclicBarrier 是可以重用的。
④使用
private static final CyclicBarrier cb=new CyclicBarrier(4,new Runnable() {
public void run()
{
System.out.println("寝室四兄弟一起出发去球场");
}
});
//cb传入线程中,线程中通过
//线程通过cb.await();使cb计数器减一
10)生产者消费者问题多种实现
①使用阻塞队列 LinkedBlockingQueue实现
class Producer implements Runnable {
private final BlockingQueue sharedQueue;
public Producer(BlockingQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println("Produced: " + i);
sharedQueue.put(i);
} catch (InterruptedException ex) {
System.out.println(ex);
}
}
}
}
class Consumer implements Runnable {
private final BlockingQueue sharedQueue;
public Consumer(BlockingQueue sharedQueue) {
this.sharedQueue = sharedQueue;
}
public void run() {
while (true) {
try {
int i = (Integer) sharedQueue.take();
System.out.println("Consumed: " + i);
} catch (InterruptedException ex) {
System.out.println(ex);
}
}
}
}
public class ProducerConsumerPattern {
public static void main(String args[]) {
BlockingQueue sharedQueue = new LinkedBlockingQueue();
Thread prodThread = new Thread(new Producer(sharedQueue));
Thread consThread = new Thread(new Consumer(sharedQueue));
prodThread.start();
consThread.start();
}
}
②使用Object 的wait()和notify()实现
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10);//充当缓冲区
class Consumer extends Thread {
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == 0) {//队列空的条件下阻塞
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.poll(); // 每次移走队首元素
queue.notify();
}
}
}
}
class Producer extends Thread {
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == 10) {//队列满了的条件下阻塞
try {
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.offer(1); // 每次插入一个元素
queue.notify();
}
}
}
}
③使用Condition 实现
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(10);
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
class Consumer extends Thread {
public void run() {
while (true) {
lock.lock();
try {
while (queue.size() == 0) {
try {
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll(); // 每次移走队首元素
notFull.signal();
} finally {
lock.unlock();
}
}
}
}
class Producer extends Thread {
public void run() {
while (true) {
lock.lock();
try {
while (queue.size() == 10) {
try {
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(1); // 每次插入一个元素
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
}