写在前面
本文一起看下在日常工作中我们经常用到的线程安全的数据类型,以及一些经验总结。
1:常用线程安全数据类型
1.1:jdk集合数据类型
jdk的集合数据类型分为两类,一种是线性数据结构
,另外一种是字典结构
,分别看下。
1.1.1:线性数据结构
线性数据结构主要包括List,Set,Queue,其中List是一种允许数据重复的顺序数据结构,Set是一种不允许数据重复的数据结构,Queue是一种队列的数据结构,接口如下:
public interface List<E> extends Collection<E> {}
public interface Set<E> extends Collection<E> {}
public interface Queue<E> extends Collection<E> {}
二者都继承了相同的集合类结构Collection,如下:
public interface Collection<E> extends Iterable<E> {}
Collection接口🈶继承了jdk rt.jar包中的java.lang.Iterable
接口,所以又是支持迭代的。
- List
List的主要子类包括ArrayList,LinkedList,Vector,Stack(Vector子类) - Set
Set主要子类有HashSet,LinkedSet,TreeSet - Queue
接口定义如下:
public interface Queue<E> extends Collection<E> {
// 向队列添加元素,如果是有空间则插入成功,返回true,否则抛出IllegalStateException
boolean add(E e);
// 向队列添加元素,如果是有空间则插入成功,返回true,否则返回false(这里不同于add方法抛出异常)
boolean offer(E e);
// 获取并且删除队列头元素,如果是队列为空则抛出NoSuchElementException异常
E remove();
// 获取并且删除队列头元素,如果是队列为空则返回null(不同于remote抛出NoSuchElementException异常)
E poll();
// 获取但是不删除队列头元素,如果是队列为空则抛出NoSuchElementException异常
E element();
// 获取但是不删除队列头元素,如果是队列为空则返回null(不同于element抛出NoSuchElementException异常)
E peek();
}
双向队列子接口Deque,一种双端都可以入队和出队的数据结构,源码如下:
public interface Deque<E> extends Queue<E> {
// 在deque头添加元素,如果有空间则返回true,否则抛出IllegalStateException异常
void addFirst(E e);
// 从deque尾入队,如果成功则返回true,如果空间不足插入失败则抛出IllegalStateException异常
void addLast(E e);
// 在deque头添加元素,如果有空间则返回true,否则返回false(优于addFirst抛出IllegalStateException异常)
boolean offerFirst(E e);
// 从deque尾入队,如果成功则返回true,如果空间不足插入失败则返回false(优于addLast抛出IllegalStateException异常)
boolean offerLast(E e);
// 从deque中获取并且删除头元素,如果dequeu为空则抛出NoSuchElementException异常
E removeFirst();
// 获取并且删除deque尾元素,如果是deque没有元素则抛出NoSuchElementException异常
E removeLast();
// 从deque中获取并且删除头元素,如果dequeu为空则返回null(不同于removeFirst抛出NoSuchElementException异常)
E pollFirst();
// 获取并且删除deque尾元素,如果是deque没有元素则返回null(不同于removeLast抛出NoSuchElementException异常)
E pollLast();
// 获取deque头元素,如果deque为空,则抛出NoSuchElementException异常
E getFirst();
// 获取deque尾元素,如果deque为空,则抛出NoSuchElementException异常
E getLast();
// 获取deque头元素,如果deque为空,则返回null(不同于getFirst抛出NoSuchElementException异常)
E peekFirst();
// 获取deque尾元素,如果deque为空,则返回null(不同于getLast抛出NoSuchElementException异常)
E peekLast();
// 从deque中删除首次出现的指定元素,如果存在则删除并返回true
boolean removeFirstOccurrence(Object o);
// 从deque中删除最后出现的指定元素,如果存在则删除并返回true
boolean removeLastOccurrence(Object o);
// *** 队列方法,在父类Queue中已经定义了,这里为什么要重复定义??? ***
// 在deque尾添加元素,成功返回true,没有可用空间则抛出IllegalStateException异常
boolean add(E e);
// 在deque尾添加元素,成功返回true,没有可用空间则返回false(不同于add抛出IllegalStateException异常)
boolean offer(E e);
// 从deque获取并且删除首个元素,如果deque为空则抛出NoSuchElementException
E remove();
// 从deque获取并且删除首个元素,如果deque为空则返回null(不同于remove抛出NoSuchElementException)
E poll();
// 从deque头获取元素,但是不删除,如果是deque为空则抛出NoSuchElementException异常
E element();
// 从deque头获取元素,但是不删除,如果是deque为空则返回null(不同于element抛出NoSuchElementException异常)
E peek();
// *** 栈相关方法定义 ***
// 入栈,这里就是将元素添加到deque的头,如果没有可用空间则抛出IllegalArgumentException,该方法同addFirst,但是push方法更加能够对应栈的入栈操作
void push(E e);
// 出栈,即获取deque头的元素,如果deque为空则抛出NoSuchElementException异常,该方法同removeFirst()
E pop();
// *** 集合方法,看来Deque是一个线性数据结构大杂烩,定义了常见线性数据结构的各种操作 ***
// 删除首次出现的指定元素,同#removeFirstOccurrence(Object)方法
boolean remove(Object o);
// 判断deque中指定元素是否存在
boolean contains(Object o);
// 获取deque元素的个数
public int size();
// 获取deque对应的迭代器,按照head(first)->tail(last)的顺序获取元素
Iterator<E> iterator();
// 获取deque对应的倒叙迭代器,按照tail(last)->head(first)的顺序获取元素
Iterator<E> descendingIterator();
}
其实LinkedList实现了Deque接口,所以我们不仅可以将LinkedList当做Collection来使用,也可以把其当做Stack,Queue,Deque来使用,如下当做队列Queue使用:
// linkedlist当做queue使用
private static void useAsQueue() {
Queue<String> queueList = new LinkedList<>();
// 入队
queueList.offer("xxxx");
queueList.offer("yyyy");
// 出队
System.out.println(queueList.poll());
System.out.println(queueList.poll()); // 空了
System.out.println(queueList.poll());
}
运行结果:
xxxx
yyyy
null
当做Stack使用:
private static void userAsStack() {
// Deque中定义了stack相关的方法,所以引用使用java.util.Deque
Deque<String> stack = new LinkedList<>();
stack.push("aaaa");
stack.push("bbbb");
System.out.println(stack.pop());
System.out.println(stack.pop());
}
运行结果:
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ gogogo ---
bbbb
aaaa
1.1.2:字典结构
字典结构相关有两个顶层类是Map(是个接口),Dictionary(是个抽象类),如下:
public interface Map<K,V> {}
public abstract class Dictionary<K,V> {}
分别看下。
- Map
主要子类HashMap,LinkedHashMap,TreeMap。 - Dictionary
主要子类HashTable,Properties,其中Properties是HashTable的子类,另外Properties有个坑
需要小心,即如果是value是int时,可以插入数据,但是当get时会返回null,如下测试:
public static void main(String[] args) {
// useAsQueue();
// userAsStack();
Properties p = new Properties();
p.put("name", "jack");
p.put("age", 90);
System.out.println(p.getProperty("name"));
System.out.println(p.getProperty("age"));
}
运行结果:
jack
null
如果不小心掉到了properties的这个坑里还真是不好发现😭😭😭。
1.2:List分析
不管是ArrayList还是LinkedList,都不是线程安全的数据结构,存在线程安全问题:
1:写写冲突
当多个线程并发写时可能会出现数据覆盖的问题,比如线程A和线程B都修改位置2的元素,最终到底是谁设置成功就不一定了,特别是对于+1场景会导致少1
2:读写冲突
当读时写可能会产生不可预期的后果,当出现读的过程中发现元素个数发生变化的情况,将会抛出ConcurrentModificationException异常。
那么,如何实现List的线程安全呢?分别来看下。
1.2.1:使用Vector
Vector通过synchronized关键字对方法上对象锁,实现了线程安全,即串行执行,效率堪忧。该方法也是List接口的子类,方法签名如下:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}
1.2.2:使用Collections.synchronizedXXX方法
使用Collections.synchronizedXXX方法转换为线程安全的集合对象,其实就是对其进行简单包装,方法加上synchronized关键字,所以其本质上通Vector,以synchronizedList为例:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
返回的是SynchronizedList包装类,该类方法如下:
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
可以看到都是使用synchronized关键字将具体集合方法的调用放在了同步代码块中。
1.2.3:使用Arrays.asList
该方法返回的是Arrays内部类ArrayList,而非java.util.ArrayList,方法仅仅实现了读取操作和set操作,因此如果是调用add,remove等方法将会调用到AbstractList,会抛出UnsupporedOperationException,如下:
// java.util.Arrays.ArrayList
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{}
测试代码:
List<String> list = Arrays.asList("aa", "bb");
list.add("ccc");
运行结果:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at dongshi.daddy.Huohuo.main(Huohuo.java:17)
从调用栈也可以看出该异常是AbstractList抛出的,其实就是如下方法:
// java.util.AbstractList#add(int, E)
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
1.2.4:使用Collections.unmodifiableXxxx
Collections.unmodifiableXxxx方法会返回一个包装类,该类只允许查看数据,不允许更新数据,也不允许排序,因为排序本质上也是一种修改数据的操作,以unmodifiableList为例如下:
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
返回的包装类是UnmodifiableList
,该类针对修改相关的操作统一抛出UnsupportedOperationException,如下:
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
...
public boolean addAll(int index, Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
throw new UnsupportedOperationException();
}
@Override
public void sort(Comparator<? super E> c) {
throw new UnsupportedOperationException();
}
1.2.5:使用CopyOnWriteArrayList
使用COW,即写时复制方式实现线程安全的集合类,在读时不锁,写时写锁,本事上是一种读写分离
思想的运用,读取数据是最终一致的,看下修改方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 重入锁上锁
lock.lock();
try {
// 获取当前的数组
Object[] elements = getArray();
int len = elements.length;
// 拷贝原数组并将长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 添加新元素
newElements[len] = e;
// 这里设置到private transient volatile Object[] array;注意其是volatile的,根据“对volatile变量的写操作 先行发生于 对volatile的读操作“,后续线程将直接能够读到这里设置的最新值
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
迭代器的实现也是使用快照,如下:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
// 这里虽然是引用传递,但是因为如果是原集合被修改,通过COW会创建新数组,所以不会改变这里的值
// ,即不会破坏这里的快照
cursor = initialCursor;
snapshot = elements;
}
}
适合场景:读锁写少
1.3:Map分析
1.3.1:HashMap
初始容量16,扩容*2,负载因子0.75,jdk8引入红黑树解决hash冲突,当冲突链长度到8,数组长度到64后,升级冲突链表为红黑树。非线程安全,可能如下线程安全问题:
1:读写冲突
2:扩容数据读取导致死循环(严重)
3:keys无序问题
其中2
是存在扩容节点重分布导致出现两个节点互为next的情况,进而导致CPU,耗尽CPU资源。3
keys方法获取键是通过如下方式循环获取的:
因此在扩容,节点重分布的过程中导致错误。
1.3.2:LinkedHashMap
继承自HashMap,如下:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{}
在HashMap基础上增加了链表结构维护数据插入的先后顺序,但也有线程安全问题。
1.3.3:ConcurrentHashMap
在juc 中提供的HashMap的线程安全版本,在jdk7中使用分段的方式来实现线程安全,如下:
jdk7中虽然通过Segment使加锁的概率降低,但是还是有锁,在jdk8中使用基于cas的乐观锁技术进行改版,如下:
2:其它知识点
2.1:ThreadLocal
在一个线程内传递变量的机制,每个线程独立,通过set方法设置变量,通过get方法获取变量。
2.2:并行stream
通过添加.parallel
以并行的方式来执行集合操作,底层使用的是JUC提供的多线程实现相关功能,如下:
2.3:加锁需要考虑的问题
1:锁的粒度如何控制
2:使用公平锁还是非公平锁
3:是否需要考虑自旋的情况
4:是否需要考虑重入
5:当前场景适合是用什么锁,适合如何加锁(脱离场景讨论问题都是耍流氓)
6:加锁后是否会严重影响程序性能
2.4:线程间通信
1:Thrad.join
2:Object,wait/notify/notifyAll
3:JUC工具类
Semaphore,CountDownLatch,CyclicBarrier