1. ReentrantLock
先来个简单案例:
// 1.创建一个锁
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
// 2.加锁
reentrantLock.lock();
for (int i = 0; i < 10;i++){
// 减缓打印速度
TimeUnit.SECONDS.sleep(1);
System.out.println("线程1:运行中");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//3.解锁
reentrantLock.unlock();
}
},"T1");
t1.start();
案例很简单,就是创建一个锁,然后加锁,解锁。
1.1. 常用API
-
支持公平锁和非公平锁,公平锁就是按照顺序进入锁等待队列来获取锁,非公平锁是先尝试获取锁,获取失败后进入等待队列
- 公平锁:
ReentrantLock reentrantLock = new ReentrantLock(true);
- 非公平锁:
ReentrantLock reentrantLock = new ReentrantLock(false);
- 默认是非公平锁,源码很简单,一看便知。
- 公平锁:
-
lock():加锁,锁被占有,则阻塞等待。
-
tryLock():尝试获取锁,不会阻塞线程,返回值是Boolean 类型,代表是否获取到锁。
-
tryLock(timeout,Times):尝试获取锁,在参数指定时间内获取不到锁则线程继续执行。
-
lockInterruptibly() :加锁,这个锁支持被打断,比如线程1 调用lockInterruptibly() 加锁成功,线程2 因等待锁被阻塞,那么线程2 支持通过interrupt() 打断。
-
hasQueuedThreads() 是否有线程在等待获取当前锁
-
isLocked():当前锁是否被占有。
1.2. Condition 的使用
Condition 是可以唤醒指定线程,用于生产者消费者场景的线程调度
先来个场景,有一个仓库,5个线程往里生产,5个线程消费,这10个线程共享一个仓库资源,所以得使用同一把锁,当仓库满了需要生产线程等待,仓库空的时会需要消费线程等待。
案例伪代码:
ReentrantLock reentrantLock = new ReentrantLock();
// 消费者
Condition con1 = reentrantLock.newCondition();
// 生产者
Condition con2 = reentrantLock.newCondition();
Thread t1 = new Thread(() -> {
try {
reentrantLock.lock();
// 假如仓库已满,进行等待,并且唤醒消费者,
if(假如仓库已满){
// 生产者组等待
con2.await();
// 消费者组唤醒
con1.signal();
}else{
// 创建产品,添加到仓库1个产品
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
reentrantLock.unlock();
}
},"T1");
t1.start();
Thread t2 = new Thread(() -> {
try {
reentrantLock.lock();
// 假如仓库已空,进行等待,并且唤醒生产者组线程,
if(假如仓库已空){
// 消费者组等待
con1.await();
// 生产者组唤醒
con2.signal();
}else{
// 消费产品,仓库产品数减少1个
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
reentrantLock.unlock();
}
},"T1");
t2.start();
总结:被指定con1 执行wait 的方法,只能被con1 signal方法唤醒。消费者和生产者共享仓库资源,共同使用同一锁对象,但是可以被Condition 对象分成两个组,实现线程的调度。
2. CountDownLatch用法
2.1. 案例
这个比较简单,来个案例
public static void countDownLatch() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
System.out.println("==========运行开始==============");
new Thread(() -> {
// latch.getCount() 初始值 等于构造参数变量 10
while (latch.getCount() > 0){
System.out.println("latch.getCount()"+latch.getCount());
// 执行方法:latch.getCount() 减 1
latch.countDown();
sleep(1);
}
}).start();
// 这里一直会阻塞直到 latch.getCount() = 0时,解除阻塞
latch.await();
System.out.println("==========运行结束==============");
}
当主线程执行
latch.await()
方法时,会被阻塞,直到另一个线程一直执行latch.countDown();
把 CountDownLatch 内部对象的int 变量减为0,主线程才能继续执行。
案例中是多个线程执行
latch.countDown();
单个线程await
,其实也可以多个线程await
,单线程countDown
,大家要明白意思灵活多变。
注意
await()
方法不要和wait()
方法混淆,await()
是CountDownLatch
自带的,wait()
方法是继承自Object
的
2.2 API
api更简单,只有如下3个方法
latch.await()
线程等待await(long timeout, TimeUnit unit)
线程等待指定时长,时间到了,无论CountDownLatch内部int 变量是否减至0,线程都会继续执行latch.getCount()
获取CountDownLatch内部int 变量值
3. CyclicBarrier
public static void cyclicBarrier(){
Runnable runnable = () -> System.out.println("人满,发车");
// 这里模拟
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,runnable);
for (int i = 0; i < 3; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-上车等待await...");
try {
// 线程执行到这里都会阻塞,同时cyclicBarrier 计数器减1
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+"-await结束,准备出发...");
} catch (Exception e) {
throw new RuntimeException(e);
}
}).start();
}
}
Thread-0-上车等待await...
Thread-2-上车等待await...
Thread-1-上车等待await...
人满,发车
Thread-1-await结束,准备出发...
Thread-2-await结束,准备出发...
Thread-0-await结束,准备出发...
CyclicBarrier 有两个参数 int 类型 parties 、runable 类型,线程执行
cyclicBarrier.await();
是会阻塞,同时在parties 减 1,当parties 减到0 时,执行runable ,然后所有被cyclicBarrier.await();
阻塞的线程继续执行
注意
await()
方法不要和wait()
方法混淆,await()
是CyclicBarrier
自带的,wait()
方法是继承自Object
的
4. Phaser
4.1. 常用API
-
构造方法
- Phaser(): 参与任务数0
- Phaser(int parties) :指定初始参与任务数
- Phaser(Phaser parent):指定parent阶段器, 子对象作为一个整体加入parent对象, 当子对象中没有参与者时,会自动从parent对象解除注册
- Phaser(Phaser parent,int parties) : 集合上面两个方法
-
增减参与任务数方法
- int register() 增加一个任务数,返回当前阶段号。
- int bulkRegister(int parties) 增加指定任务个数,返回当前阶段号。
- int arriveAndDeregister() 减少一个任务数,返回当前阶段号。
-
到达、等待方法
- int arrive() 到达(任务完成),返回当前阶段号。
- int arriveAndAwaitAdvance() 到达后等待其他任务到达,返回到达阶段号。
- int awaitAdvance(int phase) 在指定阶段等待(必须是当前阶段才有效)
- int awaitAdvanceInterruptibly(int phase) 阶段到达触发动作
- int awaitAdvanceInterruptiBly(int phase,long timeout,TimeUnit unit)
- protected boolean onAdvance(int phase,int registeredParties)类似CyclicBarrier的触发命令,通过重写该方法来增加阶段到达动作,phase代表当前阶段,registeredParties代表当前阶段执行任务数,该方法返回true将终结Phaser对象,后面阶段不会再触发。
大白话解释:多个线程干活,活干完调用arrive() 报告,每干完 parties 数量的活,执行一次onAdvance()方法,阶段号就是每干够parties数量任务,阶段号加1
public static void phaser(){
Phaser phaser = new Phaser(3){
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("任务到达第"+phase+"阶段,本阶段注册线程数:"+registeredParties);
//返回true,终止触发执行后续阶段
return false ;
}
};
for (int i = 0; i < 15; i++) {
new Thread(() -> {
sleep(1);
// arrive() 方法表示任务完成,这里几个线程干活自己定,干完调用arrive() 方法就行
System.out.println("phaser.arrive()="+phaser.arrive());
}).start();
}
// for (int i = 0; i < 3; i++) {
// 等待当前阶段完成
// phaser.awaitAdvance(phaser.getPhase());
// System.out.println("执行阶段 = "+phaser.getPhase());
// System.out.println("phaser.register() = "+phaser.register());
// }
System.out.println("======Main 运行结束=====");
}
5. Semaphore
Semaphore semaphore = new Semaphore(2);
创建一个semaphore 有两个信号semaphore.acquire()
获得一个信号,semaphore 有剩余信号,继续执行,没有则阻塞semaphore.release()
释放信号 线程执行完释放- 限流场景,和令牌桶意思一样
public static void semaphore(){
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sleep(2);
System.out.println("拿到信号,执行任务");
semaphore.release();
}).start();
}
}
6. Exchanger
- 作用:交换线程的数据
- exchanger.exchange(s) 方法是阻塞的
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(()->{
String s = "T1";
s = exchanger.exchange(s);
System.out.println("线程一:"+s);
},"t1").start();
new Thread(()->{
String s = "T2";
s = exchanger.exchange(s);
System.out.println("线程二:"+s);
},"t2").start();
}
7. LockSupport
API介绍
- park(): 阻塞当前线程,直到unpark方法被调用或当前线程被中断,park方法才会返回。
- parkNanos(long nanos): 同park方法,nanos表示最长阻塞超时时间,超时后park方法将自动返回。单位是纳秒 ,1秒 = 100010001000 纳秒
- parkUntil(long deadline): 同park()方法,deadline参数表示最长阻塞到某一个时间点,当到达这个时间点,park方法将自动返回。(该时间为从1970年到现在某一个时间点的毫秒数)
- unpark(Thread thread): 唤醒处于阻塞状态的线程thread。
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
System.out.println("主线程开启。。");
Thread thread = new Thread(() -> {
System.out.println("启动子线程");
//阻塞当前线程
LockSupport.park();
System.out.println("子线程结束");
});
thread.start();
// 主线程3S后解除子线程阻塞
TimeUnit.SECONDS.sleep(3);
LockSupport.unpark(thread);
System.out.println("主线程结束");
}
说明
- 本类底层是使用 UNSAFE 类进行线程阻塞的, UNSAFE是Java中提供的可以直接操作系统底层的不安全的类,UNSAFE类要慎用。
- 有很多带blocker 参数的重载方法,该参数主要用于问题排查和系统监控,在线程dump中会显示该参数的信息,有利于问题定位。
- 也可以先调用unpark,再调用park方法,不过park 方法不会被阻塞。
- 在同步代码块中使用park 方法时,线程不会释放锁,所以避免混合使用,以免出现死锁。
- park方法支持中断,也就是说一个线程调用park方法进入阻塞后,如果该线程被中断则能够解除阻塞立即返回。但需要注意的是,它不会抛出中断异常,所以我们不必去捕获InterruptedException。