文章目录
- 前言
- 一些不常用的工具类
- 不可变集合
- 多值Map
- Table表
- Lists、Maps、Sets
- 字符串操作
- Bag
- LazyList
- 双向Map
- 并发集合小总结
- CopyOnWriteArrayList
- ConcurrentHashMap
前言
最近挖掘了一些在项目中不常用的工具类,有些实用性还是很高的,特此总结一下。
另外又顺便看了一下常用并发集合的相关知识,也在此自我总结一下。
一些不常用的工具类
不可变集合
不可变集合包括 ImmutableList, ImmutableMap,ImmutableSet,ImmutableSortedSet等,当其创建之后就不会发生变化,可以在一些只读的场景来使用他们,减少空间的浪费。
public static void main(String[] args) {
//初始化一个不可变集合
ImmutableList<String> immutableList = ImmutableList.of("12");
List<String> list = new ArrayList<String>() {{
add("321");
}};
//copy 一个list
ImmutableList<String> immutableList1 = ImmutableList.copyOf(list);
}
因其是不可变的,所以无法进行add等操作。
多值Map
Map是一个key对应一个value,当key重复时,再进行put则会进行覆盖。但有些场景需要一个key对应多个value,比如一个人可以有多个职位,key是人的id,value是人的职位id,这种类似的场景可以使用多值Map。
public static void main(String[] args) {
ArrayListMultimap<String, String> multiMap = ArrayListMultimap.create();
multiMap.put("adam","teacher");
multiMap.put("adam","player");
multiMap.put("adam","player");
System.out.println(multiMap.get("adam"));
}
以下是输出的结果,可以看到,返回的是一个List,并且value值重复也并不会被过滤掉。
[teacher, player, player]
Table表
Map是通过一个key来决定value,但某些场景我们想通过两个key来决定value,比如通过经度、纬度才能确定当前的位置。
public static void main(String[] args) {
HashBasedTable<String, String, String> table = HashBasedTable.create();
//通过经纬度来确定当前位置
table.put("12", "25", "position");
table.put("12", "26", "position");
table.put("21", "25", "position1");
//根据第一个key来获取value
Collection<String> values0 = table.row("12").values();
System.out.println(values0);
//根据两个key来获取value
String s = table.get("12", "25");
System.out.println(s);
//获取table中的所有值
Collection<String> values1 = table.values();
System.out.println(values1);
}
打印结果如下:
[position0, position1]
position0
[position0, position1, position2]
Lists、Maps、Sets
Lists的简单使用
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {{
add("123");
add("1234");
add("12345");
}};
//分页操作
Lists.partition(list,1).forEach(System.out::println);
}
打印结果:
[123]
[1234]
[12345]
Sets简单使用
public static void main(String[] args) {
Set<String> set1 = new HashSet<String>() {{
add("1");
add("2");
}};
Set<String> set2 = new HashSet<String>() {{
add("1");
}};
//找出不同
Sets.SetView<String> difference = Sets.difference(set1, set2);
System.out.println(difference);
//找出相同
Sets.SetView<String> intersection = Sets.intersection(set1, set2);
System.out.println(intersection);
}
打印结果:
[2]
[1]
Maps的简单使用
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>(){{
put("123", "20");
put("1234", "201");
put("12345", "202");
}};
//过滤key
Map<String, String> map1 = Maps.filterKeys(map, "123"::equals);
System.out.println(map1);
}
结果:
{123=20}
其余的方法大家可以自行研究~
字符串操作
Joiner的简单使用
public static void main(String[] args) {
/*
* 比如拼接redisKey的场景
*/
Joiner joiner = Joiner.on("_");
String key = joiner.skipNulls().join(Arrays.asList("tenantId", null, "appId", "funcId"));
System.out.println(key);
/*
* 打印map中key和value的场景
*/
Map<String, String> map = new HashMap<String, String>() {{
put("张三", "帅哥");
put("李四", "美女");
}};
String join = Joiner.on("\n").withKeyValueSeparator("是").join(map);
System.out.println(join);
}
以下是打印结果
tenantId_appId_funcId
李四是美女
张三是帅哥
Splitter与Joiner相对,它可实现字符串的分割
public static void main(String[] args) {
/*
* 比如拼接redisKey的场景
*/
Joiner joiner = Joiner.on("_");
String key = joiner.skipNulls().join(Arrays.asList("tenantId", null, "appId", "funcId"));
//使用Splitter进行分割
Iterable<String> split = Splitter.on("_").split(key);
split.forEach(System.out::println);
String key1 = "宁教我负天下人休教天下人负我";
//按照指定长度进行分割
Iterable<String> split1 = Splitter.fixedLength(7).split(key1);
split1.forEach(System.out::println);
}
两次打印结果如下
tenantId
appId
funcId
宁教我负天下人
休教天下人负我
Bag
Bag和List很相似,但它可以统计出重复元素的数量。
public static void main(String[] args) {
Bag box = new HashBag(Arrays.asList("1", "1", "2", "3"));
box.add("1");
//查看“1”有多少个
int count = box.getCount("1");
System.out.println(count);
}
打印结果如下:
3
LazyList
LazyList可以延迟某元素的生成,在集合被访问的时候再生成,是一种懒加载的方式,一定程度上提高了性能。
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {{
add("1");
add("2");
add("3");
}};
List<String> lazy = LazyList.lazyList(list, () -> "4");
System.out.println(lazy);
//只有用到的时候才存入;类似于orElseGet
lazy.get(3);
System.out.println(lazy);
}
打印结果如下:
[1, 2, 3]
[1, 2, 3, 4]
双向Map
jdk中的Map要求键唯一,双向Map则要求键、值都必须唯一,这样它既可以根据key来获取value,也可以通过value来获取key,所以叫双向Map。
public static void main(String[] args) {
BidiMap bidiMap = new TreeBidiMap<String, String>();
bidiMap.put("key", "value");
//key value存在相同则进行覆盖
bidiMap.put("key1", "value");
bidiMap.put("key2", "value2");
System.out.println(bidiMap);
//根据 key获取value
System.out.println(bidiMap.get("key2"));
//根据value获取key
System.out.println(bidiMap.getKey("value2"));
}
{key1=value, key2=value2}
value2
key2
并发集合小总结
CopyOnWriteArrayList
ArrayList一般都是在方法内部使用,所以相对来说是安全的,但是多线程环境下是非安全的,先来看一下ArrayList的源码。
在jdk8下,如果new一个ArrayList,不指定其大小,默认为空:
public ArrayList(int initialCapacity) {
//指定大小
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//不指定大小默认为空
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
当执行add操作时:
public boolean add(E e) {
//根据加上当前元素之后该集合元素的数量来判断是否扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//入参为当前数组以及加上当前元素后当前数组的大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果是第一次add操作,且初始化时没有指定ArrayList大小
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//则默认大小为DEFAULT_CAPACITY 10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
//如果非第一次add或者new的时候指定了容量大小,则返回集合当前的大小+1
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果集合大小小于add该元素之后集大小则进行扩容操作。
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
//扩容后的容量大小为扩容前的(1+0.5)倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩容后的大小小于当前大小的话,则新集合大小为当前大小。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝一个新的数组。
elementData = Arrays.copyOf(elementData, newCapacity);
}
以上有两个比较重要的名词:
1、minCapacity:预期集合大小,即要想把当前元素加到该集合中,该集合需要的大小。
2、elementData:集合中存放元素的数组
用文字来描述一下add流程:
1、判断是否需要扩容
1.1、如果集合预期的大小超过了当前集合大小,则进行扩容
1.2、将集合扩容1.5倍
1.3、如果扩容之后的的大小小于预期大小,则集合大小为预期大小,但如果扩容之后大小大于集合最大值,则进行huge扩容。
2、将集合中的元素加1(i++操作,非原子性)。
在以上过程中,不安全性体现在两个地方:
1、数组越界
当集合元素数量为9,线程A和线程B同时进行add操作,A执行时发现预期集合大小为10,等于当前容量,不需要扩容(也就是上述1.1步骤),线程B此时和线程A运行到同一行代码处,发现也不需要扩容,则当A把元素添加到集合之后,B再添加就会出现数组越界的情况。
2、覆盖操作
因为向数组中存入的操作方式为size++,也就是分了三步
1、从主存中拿到当前size的值放到本地高速缓存中
2、把size值进行加1
3、再把size值放到主存中
当线程A和线程B同时进行size++,A进行到第1步,B也进行到第1步,当A和B都执行到第3步时,则会发生覆盖的情况。
以上就是ArrayList多线程下不安全的原因,解决方法是使用Vector或CopyOnWriteArrayList,前者主要是用了内部锁,这次主要讨论一下后者。
看一下CopyOnWriteArrayList的源码:
private transient volatile Object[] array;
public boolean add(E e) {
final ReentrantLock lock = this.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();
}
}
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
CopyOnWriteArrayList的add操作虽然没有使用内部锁,但使用了可重入锁,每次只能有一个线程执行add操作,这点和Vector的add操作其实是一样的,但CopyOnWriteArrayList在add的时候新建了一个新的数组,在新数组中添加好元素后再将引用给之前的数组,这样CopyOnWriteArrayList在进行读操作时读的是之前的数组,保证了线程的安全性,因此其适用于读多写少的场景。
ConcurrentHashMap
我们先来看一下Map的源码,分析一下Map的不安全性体现在哪里。
transient Node<K,V>[] table;
//hashMap有3个构造,无参构造在初始化的时,负载因子的值为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果在初始化时未设置容量大小,则默认为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//15与当前元素的hash值进行与运算,如果得到的位置为空,则创建一个新的节点。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//当前元素的hash值与之前元素的hash值相同且key相同,还equels,就覆盖之前的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果之前节点的数据结构是红黑树,则将节点放到树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//元素不重复,也不是红黑树,则是单纯的发生了hash冲突
for (int binCount = 0; ; ++binCount) {
//如果发生冲突节点的下一个节点为null,则就将节点放进去
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//走到这里说明比较了8次,则将链表转红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//发生冲突节点的下一个几点和当前节点元素重复,则直接覆盖。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//再进行链表的下一个节点重新判断。
p = e;
}
}
//e不为null说明发生了覆盖,则返回当前元素。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果当前元素数量大于了阈值,则进行扩容。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
用文字来描述jdk1.8的扩容机制如下:
1、初始化容量
2、获取要put元素的数组下标
2.1判断该下标是否发生了碰撞
2.1.1如果没有碰撞则直接put
2.1.2如果发生了碰撞就判断是否和要put的元素一样
2.1.2.1如果一样就直接覆盖
2.1.2.2如果不一样就判断是不是树
如果是树就放到树节点
如果不是树就将创建新的节点并和上一个节点连接上
然后判断连接之后的链表长度是不是大于8,
如果大于8就转树
小于8就put
3、判断容量是否达到阈值
达到了就扩容
没达到就不扩容
jdk1.8的话会出现覆盖现象:
即线程A和线程B同时扩容,但它俩同时put元素时key的hash值是一样的,当A和B都运行到了2.1.1步骤时,A判断没有hash冲突时直接put,B线程也是同样操作,则就会出现了覆盖的现象。
jdk1.7扩容的话会有循环链的情况,在此不做过多描述了,扩容图如下:
为了解决以上问题,我们可以使用ConcurrentHashMap,我们看一下它的jdk1.8源码:
transient volatile Node<K,V>[] table;
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//获取hash值,方式是将hashCode转成二进制右移16位后与hashCode进行异或运算再与int最大值进行与运算。
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//如果当前数组为空,则进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//hash算法来计算当前位置是否为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果当前位置为空则进行cas机制进行创建节点。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果是MOVED,也就是-1,则进行多线程扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//走到这个else,则说明了hash冲突
V oldVal = null;
//当出现hash冲突,就锁住当前元素,保证只有一个线程操作
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
//遍历当前索引下的节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果存在元素相同的情况下则覆盖
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
//没有相同元素则尾插到链表最后
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果节点转化成了树,则添加节点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//链表长度大于8则转树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
当没有出现hash冲突时,通过cas机制保证线程安全,当出现hash冲突后,则加了一把内部锁,锁住当前要操作的元素。以上只是jdk1.8的情况。