Java中的集合主要继承Collection和Map两个根接口,Collection又派生了List,Queue,Set三个子接口,因此Java集合主要分为四类:List,Queue,Set,Map
List | Queue | Set | |||
---|---|---|---|---|---|
线程安全 | 非线程安全 | 线程安全 | 非线程安全 | 线程安全 | 非线程安全 |
CopyOnWriteArrayList | ArrayList | ArrayBlockingQueue | ArrayDeque | CopyOnWriteArraySet | HashSet |
CollesynchronizedList | LinkedList | PriorityBlockingQueue | PriorityQueue | TreeSet | |
Vector | DelayQueue | ||||
Stack | LinkedBlockingDeque | Map | |||
LinkedBlockingQueue | 线程安全 | 非线程安全 | |||
LinkedTransferQueue | Hashtable | Hashmap | |||
SynchronousQueue | Collections.synchronizedMap | TreeMap | |||
DelayedWorkQueue | ConcurrentHashMap |
Hashmap和ConcurrentHashMap
HashMap描述一一映射的关系,是非线程安全的数据结构,如果想在多线程下安全的操作 map,主要有以下解决方法:
(1)使用HashTable线程安全类,HashTable实现线程安全,是将所有访问和操作数据的方法被synchronized修饰(加同步锁),对整张哈希表加锁(注意:HashTable单个方法是线程安全的,但是当多个线程同时进行put和remove时,可能出现不线程安全);
(2)使用Collections.synchronizedMap方法,对方法进行加同步锁(本质和HashTable类似);
(3)ConcurrentHashMap把整个整张哈希表分为默认N个Segment,每个Segment提供相同的线程安全,效率提升N倍(默认N=16)
【线程安全就是同一个对象对多线程访问时,采用加锁机制,对该对象的某个关键数据进行保护,其他线程不能进行访问直到该线程访问操作完,其他线程才可进入访问数据,不会出现不同的线程在同一时刻读取该对象的数据不一致或者数据污染的问题;
非线程安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据;】
为什么connurrentHashMap的效率高于hashtable
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,只要有一个线程在修改操作hashtable的对象,其他线程只能阻塞等待锁被释放,并发编程中性能差,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术(每个Segment由自己的ReentrantLock保护);
ConcurrentHashMap什么时候不线程安全
案例
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("key", 0);
ExecutorService executorService = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
/** 这块代码造成concurrentHashMap线程不安全:
*
* int value = concurrentHashMap.get("key");
* 首先,concurrentHashMap的get方法是线程安全的,一个线程在获取的时候,其他线程阻塞,
* 而且concurrentHashMap通过HashEntry存储的value是volatile的,保证其原子性
*
* concurrentHashMap.put("key", ++value);
* concurrentHashMap的get方法是线程安全的,一个线程在修改数据的时候,其他线程阻塞,
*
* 那问题出在哪呢
*/
int value = concurrentHashMap.get("key");
concurrentHashMap.put("key", ++value);
}
});
}
executorService.shutdown();
Thread.sleep(3000);
System.out.println(concurrentHashMap.get("key"));
//运行三次打印结果:
"C:\Program Files\Java\jdk-11.0.3\bin\java.exe" "-javaagent:E:\IntelliJIDEA\IntelliJ IDEA Community Edition 2019.3.1\lib\idea_rt.jar=64471:E:\IntelliJIDEA\IntelliJ IDEA Community Edition 2019.3.1\bin" -Dfile.encoding=UTF-8 -classpath E:\Projects\project_java\LeetCodeAns\out\production\LeetCodeAns javaKown.JAVA数据结构
998
995
999
Process finished with exit code 0
ConcurrentHashMap保证单个读或写操作的线程安全, 但是不能保证读写复合操作的原子性,++value包括三步:
1. 获取value值;
2. value+1;
3. 将新值赋给value;
可能存在线程已经读到value值然后等待写锁,等它拿到写锁去更新时,value已经被前一个获取到写锁的线程修改,因此出现value的线程不安全。
解决方法
1. value使用原子类AtomicInteger(见【Java 基础 12】Atomic )来定义,保证读写复合操作原子性;
2. 外部程序中单个线程的关键代码块加同步锁,但是这样做的话,跟多线程访问修改HashTable差不多,单个线程锁着整个哈希表,效率降低,在实际开发过程中要仔细思考灵活应用
ExecutorService executorService = Executors.newFixedThreadPool(1000);
Object lock = new Object();
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
// 关键代码区加同步锁
synchronized (lock) {
int value = concurrentHashMap.get("key");
concurrentHashMap.put("key", ++value);
}
}
});
}
executorService.shutdown();
//运行三次打印结果
"C:\Program Files\Java\jdk-11.0.3\bin\java.exe" "-javaagent:E:\IntelliJIDEA\IntelliJ IDEA Community Edition 2019.3.1\lib\idea_rt.jar=65355:E:\IntelliJIDEA\IntelliJ IDEA Community Edition 2019.3.1\bin" -Dfile.encoding=UTF-8 -classpath E:\Projects\project_java\LeetCodeAns\out\production\LeetCodeAns javaKown.JAVA数据结构
1000
1000
1000
Process finished with exit code 0
HashMap,HashTable,ConcurrentHashMap如何扩容
1. HashMap底层由数组+链表实现,可以存储null键和null值, 参数initialCapacity(默认16),loadFactor(默认75%),扩容策略:newsize = oldsize*2;
2. HashTable底层由数组+链表实现,不可以存储null键和null值,参数initialCapacity(默认11),loadFactor(默认75%),扩容策略:newsize = oldsize*2+1;
3. ConcurrentHashMap底层由分段的数组+链表实现,参数initialCapacity(默认16),loadFactor(默认75%),concurrencyLevel(维护Segment数组大小,默认16,或者自定义参数最接近2的n次方的数),扩容策略:newsize = oldsize*2;
注意,JDK8之后,HashMap底层由数组+链表+红黑树实现
阻塞队列和非阻塞队列
阻塞队列,实现了阻塞接口BlockingQueue(BlockingQueue接口规定队列:当生产线程向队列添加元素但队列已满时,生产线程被阻塞直到队列有剩余空间;当消费线程从队列移除元素但队列为空时,消费线程被阻塞直到队列有元素),
位于 java.util.concurrent 并发包中,包括:
ArrayBlockingQueue<>(capacity) :基于数组实现的有界队列;
DelayQueue :基于优先级堆实现,支持时延的无界调度队列;
DelayedWorkQueue in ScheduledThreadPoolExecutor : 基于优先级堆实现,支持时延实现周期性的无界调度队列;
LinkedBlockingDueue<>(省略/capacity) :基于链表实现的可选无界双端队列;
LinkedBlockingQueue<>(省略/capacity) :基于链表实现的可选无界队列;
LinkedTransferQueue:基于链表实现可选无界队列(采用预占模式,当队列为空时,提交一个null进队列,消费线程发现null进行等待,省略了链表的判空步骤);
PriorityBlockingQueue :基于优先级堆实现的无界队列;
SynchronousQueue<>() :没有缓冲的阻塞队列(1进1出);
非阻塞队列,没有实现BlockingQueue接口的
ArrayDeque: 基于数组实现的双端队列(位于 java.util包中);
PriorityQueue:基于优先级堆实现的,维护一个有序列表的无界队列(位于 java.util包中);
ConcurrentLinkedDeque:基于链表实现的,线程安全的双端队列(位于 java.util.concurrent包中);
ConcurrentLinkedQueue:基于链表实现的,线程安全的队列(位于 java.util.concurrent包中);
Java队列中线程安全的有
ArrayBlockingQueue<>(capacity,boolean) :并发控制使用经典的两条件法(使用1个ReentrantLock结合2个Condition)
DelayQueue:并发控制通过一个final ReebtrantLock非公平锁;
DelayedWorkQueue in ScheduledThreadPoolExecutor :
LinkedBlockingDeque:并发控制使用经典的两条件法(使用2个ReentrantLock结合2个Condition)
LinkedBlockingQueue:并发控制使用经典的两条件法(使用1个ReentrantLock结合2个Condition)
LinkedTransferQueue:是ConcurrentLinkedQueue,SynchronousQueue ,LinkedBlockingQueue的超类;
PriorityBlockingQueue :并发控制使用经典的条件法(使用1个ReentrantLock结合1个Condition)
SynchronousQueue:内部没有实现锁,但是使用LockSupport控制线程,实现"wait-free"算法(即CAS算法, CompareAndSwap,比较的交换,保证原子操作)控制基础链表的head和tail;
(阻塞队列都是线程安全的,使用一个锁或者两个锁控制入队和出队, 它们都位于java.util.concurrent 并发包)
ConcurrentLinkedQueue: 采用非阻塞的方式实现线程安全队列,它采用CAS算法来实现;
ConcurrentLinkedDeque: 采用非阻塞的方式实现线程安全队列,它采用CAS算法来实现;
(非阻塞队列实现线程安全,基于循环CAS实现, 它们也都位于java.util.concurrent 并发包)
Java队列中非线程安全的有
ArrayDeque: 非线程安全队列,无同步策略,不支持多线程安全访问(位于 java.util包中);
PriorityQueue:非线程安全队列,无同步策略,不支持多线程安全访问(位于 java.util包中);
并发编程中如果要用ArrayDeque和PriorityQueue可以替换成线程安全的ArrayBlockingQueue和PriorityBlockingQueue