java.util.concurrent包
我们知道java.util.concurrent包是一个解决并发问题的包,通常用在并发编程中。在前面的博客链接: 多线程 6 —— Lock体系、Lock锁原理(AQS).中已经接触了这个包,这篇博客主要说一下这个包下的其他接口以及实现类。
1、java.util.concurrent.locks包
这个包构成了Lock体系。关于Lock锁以及它的实现原理请看博客多线程 6 —— Lock体系、Lock锁原理(AQS)
此处主要说一下java.util.concurrent.locks包下的接口以及实现类。
1)Condition
作用:线程间的通信
怎么使用:
- 通过Lock对象.newCondition() 获取Condition对象
- 调用Condition对象.await() 让当前线程阻塞等待,并释放锁(=synchronized锁对象.wait())
- 调用Condition对象.signal()/signalAll() 通知之前await阻塞的线程(=synchronized锁对象.notify()/notifyAll())
2)读写锁ReentrantReadWriteLock
使用场景:
多线程执行某个操作时,允许读-读并发/并行执行,不允许读-写、写-写并发/并行执行。 如多线程读写文件读读并发、读写、写写互斥.
读锁和写锁之间,只能降级,不能升级(写锁—>读锁)
优势: 针对读读并发执行,提高运行效率.。
总结:
- 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
- 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
- 锁降级:只允许降级不允许升级====》只能从写锁降为读锁
2、java.util.concurrent.atomic包
java.util.concurrent.atomic包:包含各种原子类。
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。
原子类有以下几个:
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
我们拿 AtomicInteger 举例,常见方法有
方法 | 作用 |
---|---|
addAndGet(int delta) | i += delta; |
decrementAndGet(); | - - i |
getAndDecrement(); | i - -; |
incrementAndGet(); | ++i; |
getAndIncrement(); | i++; |
3、Callable/Future/FutureTask
Callable接口、Future接口和Futuretask类。这三个都是在java.util.concurrent包下。它是创建线程的第三种方式,具体见博客:链接: https://blog.csdn.net/yuiop123455/article/details/108236176.
4、CountDownLatch
使用场景:
在某个线程A某个地方,阻塞等待,直到一组线程执行完毕之后,再执行A后续的代码.
如果有一个子线程没有执行完毕就让一直等待。
主要方法:
- countDown(): 计数器的值-1
- await():当前线程阻塞等待(主线程等待整个一组线程执行完毕)
例如:多线程下载一个文件
20个线程(一组线程)执行- 1操作,1个主线程等待20个线程执行任务结束才继续向下执行。
注意事项: 只提供计数器减的操作
5、信号量Semaphore
什么是信号量呢?
用一个例子来理解:如果锁是只有一个玩具出租的玩具店,那么,信号量是有很多玩具出租的玩具店。
使用场景:
- (1)和CountDownLatch的使用一样,即:等待一组线程执行完毕。
- (2)多线程有限资源的访问
方法:
- new Semaphore(int): 给定数量的初始值
- release(int): 计数器值增加给定的数量
- acquire(int): 当前线程获取Semaphore对象中的给定数量的资源。获取到------->资源数量减少,当前线程往下执行;获取不到------>当前线程阻塞等待。
使用场景一:与CountDownLatch类似的功能
等待20个子线程执行完毕(一组线程):
使用场景二:多线程访问有效资源
在一个时间点,客户端任务数达到1000(上限),再有客户端请求(超过有效资源),将阻塞等待。
信号量Semaphore和CountDownLatch都是通过AQS实现的。
6、ConcurrentHashMap
说到ConcurrentHashMap,我们不得不提及到Hash的几个实现类:
1)HashMap
对于搜索类型的数据结构主要有Map、Set、Hash(哈希表) 、Tree(搜索树),将他们组合起来就会有四种类型:
HashMap:非线程安全的,JDK1.7基于数组+链表,JDK1.8基于数组+链表+红黑树
关于Map、Set的有关内容见后续博客,本篇博客主要说一下三者的区别。
2)HashTable
Hashtable的实现:线程安全的,JDK1.7和JDK1.8都是数组+链表,全部方法都是基于synchronized加锁,效率非常低
加锁方式:
通过synchronized将整个Hash表进行了加锁(也就是整个数组),而线程对hashmap进行添加删除元素的时候仅仅是对数组中的某一个链进行添加删除元素的,这就导致了它的效率不是很好。
为什么效率低呢?
===========》HashTable相当于使用了synchronized这把大锁进行了整体加锁,如下图所示。当A线程在第一条链进行put元素,而B线程在另一条链上put元素,这种情况下线程A和线程B其实不需要互斥(互不影响)。
HashTable相当于使用了synchronized这把大锁,对扩容的效率也产生了影响。
扩容:
如上图所示,加入线程A刚获取到synchronized锁就需要扩容(其他线程阻塞),这个时候就需要线程A一个人将旧数组整体复制到新数组中。效率低。
补充:扩容方式:
- 创建新数组
- 把所有的key-value,重新计算index,然后将所有元素搬到新数组中
3)ConcurrentHashMap
ConcurrentHashMap:线程安全的,基于CAS实现,并且支持很多场景下并发操作,提高了效率。
加锁方式:
把一把大锁,变成了很多小锁(每个链表一个锁)。
只有操作同一个链表,才有必要同步互斥,否则可以并发进行。提升了并发度。
优化了扩容的方式===>提升了并发度:
- 发现需要扩容的线程,只需要创建一个新数组,同时,只搬几个元素过去
- 扩容这段期间,新老数组会同时存在
- 每个来操作ConcurrentHashMap的线程,都有责任,同时参与搬家的过程
- 搬完最后一个元素的线程,负责把老数组删掉,让新数组代替
- 这个期间,插入只往新数组插入
- 这个期间,查找需要查新老数组