目录
♫ReentrantLock
♪什么是ReentrantLock
♪ReentrantLock的用法
♪ReentrantLock和synchronized的区别
♫Semaphore
♪什么是Semaphore
♪semaphore的用法
♫CountDownLatch
♪什么是CountDownLatch
♪CountDownLatch的使用
♫多线程环境使用ArrayList
♫多线程环境使用队列
♫多线程环境使用哈希表
JUC除了提供前面介绍过的线程池、定时器,还提供了多线程编程中其它一些常用的工具类和接口(ReentrantLock、Semaphore、CountDownLatch等)
♫ReentrantLock
♪什么是ReentrantLock
ReentrantLock 是 Java 标准库提供的一种可重入互斥锁,和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全的。
♪ReentrantLock的用法
使用 ReentrantLock 需要使用 lock()( 加锁, 如果获取不到锁就死等)或 trylock(超时时间)( 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁)和 unlock()( 解锁)对代码块进行加锁解锁:.ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); //加锁内容 reentrantLock.unlock();
为了避免加锁内容提前退出(有return语句或异常),导致没有执行到 unlock() 语句,一般搭配 try-finally 语句执行加锁加锁操作:
ReentrantLock reentrantLock = new ReentrantLock(); try {reentrantLock.lock();//加锁内容 } finally {reentrantLock.unlock(); }
♪ReentrantLock和synchronized的区别
♩ . synchronized 是一个关键字, 是 JVM 内部实现的 ( 大概率是基于 C++ 实现 );ReentrantLock 是标准库的一个类, 在 JVM 外实现的 ( 基于 Java 实现 )。♩ . synchronized 使用时不需要手动释放锁; ReentrantLock 使用时需要手动释放。♩ . synchronized 在申请锁失败时, 会死等; ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃。♩ . synchronized 是非公平锁; ReentrantLock 默认是非公平锁,也 可以通过构造方法传入一个 true 开启公平锁模式。♩ . synchronized 是通过 Object 的 wait / notify 实现等待 - 唤醒, 每次唤醒的是一 个随机等待的线程; ReentrantLock 搭配 Condition 类实现等待 - 唤醒, 可以更精确控制唤醒某个指 定的线程。
♫Semaphore
♪什么是Semaphore
Semaphore 是信号量的意思,用来表示 "可用资源的个数",其本质上就是一个计数器。
例如:图书馆有500个空位,学生扫码入座,可用座位-1(相当于信号量的P操作),学生扫码离座,可用座位+1(相当于信号量的V操作),如果座位为0,就无法再扫码入座了(阻塞等待)。
注:Semaphore 的 PV 操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用
♪semaphore的用法
semaphore 通过传递的参数确定总资源个数,通过 acquire() 方法申请资源(P操作),通过release() 方法释放资源(V操作):
import java.util.concurrent.Semaphore;public class Test {public static void main(String[] args) {//初始值为3的信号量Semaphore semaphore = new Semaphore(3);Runnable runnable = new Runnable() {@Overridepublic void run() {try {//申请资源semaphore.acquire();Thread.sleep(100);//释放资源semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}};//10个线程都尝试获取资源for (int i = 0; i < 10; i++) {Thread thread = new Thread(runnable);thread.start();}} }
♫CountDownLatch
♪什么是CountDownLatch
CountDownLatch 用于同时等待 N 个任务执行结束,就 好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。♪CountDownLatch的使用
CountDownLatch 通过传递参数确认需要完成的 任务个数,每个任务执行完毕, 都调用 countDown() 方法,在 CountDownLatch 内部的计数器同时自减,主线程中使用 await(),只有当计数器减为0时调用 countDown() 方法才会唤醒await():import java.util.concurrent.CountDownLatch;public class Test {public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(10);Runnable runnable = new Runnable() {@Overridepublic void run() {try {Thread.sleep(100);countDownLatch.countDown();} catch (InterruptedException e) {throw new RuntimeException(e);}}};for (int i = 0; i < 10; i++) {Thread thread = new Thread(runnable);thread.start();}countDownLatch.wait();} }
♫多线程环境使用ArrayList
在多线程环境下,直接使用ArrayList往往会出现线程安全问题,我们可以通过一下几点方案解决该问题:
♩.自己使用同步机制(synchronized 或者 ReentrantLock)进行加锁
♩.使用Collections.synchronizedList(new ArrayList) : synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List,synchronizedList 的关键操作上都带有 synchronized。♩ .使用 CopyOnWriteArrayList:CopyOnWrite容器即写时拷贝的容器,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行拷贝, 复制出一个新的容器,然后在新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。 这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
♫多线程环境使用队列
♩ .ArrayBlockingQueue :基于数组实现的阻塞队列♩ .LinkedBlockingQueue :基于链表实现的阻塞队列♩ .PriorityBlockingQueue :基于堆实现的带优先级的阻塞队列♩ .TransferQueue :最多只包含一个元素的阻塞队列
♫多线程环境使用哈希表
HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用Hashtable和ConcurrentHashMap。
♩.Hashtable:只是简单的把关键方法加上了 synchronized 关键字,一个Hashtable只有一把锁,两个线程访问Hashtable中的任何数据都会出现锁竞争。
♩.ConcurrentHashMap:
ConcurrentHashMap 相比于 Hashtable 做出了一系列的改进和优化,大大缩小了锁冲突的概率(把一大锁转换成好几把小锁)。①. ConcurrentHashMap的做法是对每个链表分别进行加锁,这样子操作不同链表里的元素就不会发生锁冲突, 大大降 低了锁冲突的概率。②. ConcurrentHashMap对读操作不加锁,而是 使用了 volatile 保证从内存读取结果,避免了读和写之间读到写了一半的数据。③. ConcurrentHashMap充分利用 CAS 特性( 比如 size 属性通过 CAS 来更新), 避免出现重量级锁的情况。④. ConcurrentHashMap优化了扩容方式,即化整为零:发现需要扩容的线程,只需要创建一个新的数组, 同时只搬几个元素过去,扩容期间, 新老数组同时存在,后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程, 每个操作负责搬运一小部分元素,搬完最后一个元素再把老数组删掉。这个期间, 插入只往新数组加, 查找需要同时查新数组和老数组。