持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
知识点:重入锁、Condition 条件、信号量、CountDownLatch、CyclicBarrier、LockSupport
重入锁
管理同步资源的实现类,可用于替换 synchronized关键字。
使用 ReentrantLock 类实现。
使用方式
for(int j=0;j<10000;j++) {
lock.lock(); // 加锁保护
try {
i++;
}finally {
lock.unlock();
}
}
- 创建:ReentrantLock lock = new ReentrantLock();
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* fair=true ,表示公平锁;锁的获取按照时间的先后顺序
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
-
加锁:lock.lock();
-
解锁:lock.unLock();
-
锁中断:lock.lockInterruptibly() ,使用该方法加锁表示这个锁是可以被中断的
ReentrantLock 被称为重入锁,是因为这个锁是可以被同一个线程反复进入的。
lock.lock();
lock.lock();
try {
}finally {
lock.unLock(); // 解锁的个数需与加锁个数一致,否则该线程将一直持有该锁资源
lock.unLock();
}
锁申请等待限时:lock1.tryLock(5, TimeUnit.MINUTES),表示尝试5分钟去获取锁,如果成功获取到锁,则返回true,否则返回false
ReentrantLock 与synchronized的区别
1、ReentrantLock 是Java实现类,依赖于API;synchronized 是关键字,由虚拟机实现;
2、ReentrantLock 对锁的添加与释放可控,synchronized 不可控;
3、ReentrantLock 存在公平锁;
4、ReentrantLock 等待锁可中断,同时提供了限时获取锁的方法;
Condition 条件
配合ReentrantLock 重入锁的接口类,用于控制线程的暂停与执行,类比Object.wait() 与 Object.notify() 方法
使用方式
public class ConditionDemo implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
System.out.println("目标线程获取到锁");
condition.await(); // 使线程等待,同时释放锁,能被中断
// condition.awaitUninterruptibly(); // 与上面的方式基本一致,只是不能被中断
System.out.println("目标执行方法结束");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionDemo demo = new ConditionDemo();
Thread thread = new Thread(demo);
thread.start();
Thread.sleep(1000); // 保证目标线程先拿到锁
lock.lock();
condition.signal(); // 唤醒线程,系统会从当前Condition对象的等待队列中唤醒一个线程
System.out.println("主线程获取该锁");
lock.unlock();
}
}
常用方法
使用condition.await();
和 condition.signal();
时,都需要先获取相关的锁。
在JDK内部常被用于堵塞队列,可参考 ArrayBlockingQueue
信号量 Semaphore
信号量可以说是锁的扩展。信号量可以指定多个线程同时访问某一个资源。
使用场景一般是需要使用多线程做处理时,但场景有限制同时工作的线程数,使用信号量可以做流量控制。例如文件读取。
在很多情况下,可能有多个线程需要访问数目很少的资源。假想在服务器上运行着若干个回答客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。这时候使用信号量就可以大大提高效率和性能了。
Demo
public class SemaphoreDemo implements Runnable {
final Semaphore semaphore = new Semaphore(5,true); // 5个许可,即同时允许5个线程访问同一资源
@Override
public void run() {
try {
semaphore.acquire(); // 尝试获取许可
Thread.sleep(2000); // 模拟耗时
System.out.println(Thread.currentThread().getId() + "-done");
semaphore.release(); // 释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SemaphoreDemo semaphoreDemo = new SemaphoreDemo();
ExecutorService service = Executors.newFixedThreadPool(20);
for(int i=0;i<20;i++) { // 提交20个任务,每次最多会有5个线程进入
service.submit(semaphoreDemo);
}
}
}
倒计时器:CountDownLatch
多线程控制工具类。通常用来控制线程等待,直到等待倒计时结束,再开始执行。
使用方式
设定一个count 倒计时,构建CountDownLatch 对象,每完成一个任务时调用 CountDownLatch对象的countDown() 方法,完成count 个任务后,才会执行 await() 后面的逻辑。
Demo
public class CountDownLatchDemo implements Runnable{
private static final int count = 10;
private static CountDownLatch countDownLatch = new CountDownLatch(count); // 10个线程执行完即可
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "准备完毕");
countDownLatch.countDown();
}
public static void main(String[] args) {
CountDownLatchDemo demo = new CountDownLatchDemo();
ExecutorService exec = Executors.newFixedThreadPool(count);
for (int i = 0; i <= count; i++) {
exec.submit(demo);
}
try {
countDownLatch.await(); // 等待检查
System.out.println("准备工作完毕,开始执行");
exec.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
ps: 如果提交的线程数不足10个,那主线程将一直等待,直到倒计时结束。
与 join 方法的区别
CounDownLatch 与join类似,都可实现线程等待。
- 使用粒度不同。join 使用起来更方便,而
CountDownLatch
的粒度会更细。为什么要说CountDownLatch
粒度更细呢。因为CountDownLatch
可以让当前线程只等待执行一部分的情况下就继续执行了,而join
必须等待线程完全执行完毕才可以。 (countDownLatch.countDown()
的方法可在线程中任一位置调用) - 原理不同。
join
底层是使用Object.wait(0)
方法进行等待的,当线程执行完毕后会调用notifyAll
方法唤醒线程。而CountDownLatch
依靠初始化时的count变量来计算,每当执行一次countDown
方法表示 count变量减1,await()
方法就是去判断这个变量值是否为0,如果是则表示所有操作已完成,否则继续等待。
循环栅栏:CyclicBarrier
CyclicBarrier是另一种多线程并发控制工具。与CounDownLatch 相似,也可以实现线程间的计数等待。但CyclicBarrier可以实现更复杂的功能。CyclicBarrier在倒计时结束后会立即执行所注册的任务线程。
使用场景通常是将一个问题拆分成一系列相互独立的子问题。
使用方法
- 构建 CyclicBarrier 对象,分配count 倒计时数 以及 障碍任务;
- 障碍任务是每一次调用 CyclicBarrier 对象的await() 方法后执行线程任务;
- 调用 CyclicBarrier 对象的await() 方法堵塞,直到满足count 个线程才会继续执行。
- 在满足count后,会执行构建 CyclicBarrier 对象时设定的 障碍任务;
- 下一次使用 wait() 方法会继续从0开始堵塞等待,直到满足count个线程,重复前面的动作;
- 如果await 的调用超时,或者await 堵塞的线程被打断,那么栅栏就被认为打破了,所有堵塞的await 调用都将终止并抛出 BrokenBarrierException。
构造方法
/**
* @param parties,倒计时数,当线程数满足这个数量时,会执行 barrierAction参数的线程
* @param barrierAction,满足await条件时执行的任务线程
**/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
await(); // 倒计时等待的方法,倒计时统计的是执行的线程数。
Demo
public class CyclicBarrierDemo {
public static class Solider implements Runnable {
private String name;
private final CyclicBarrier cyclicBarrier;
public Solider(String name, CyclicBarrier cyclicBarrier) {
this.name = name;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(name + "报道!");
cyclicBarrier.await(); // 线程堵塞,需满足barrier对象的线程数量后,才会继续执行。满足数量条件后会执行 BarrierTask任务
// doWork
Thread.sleep(1000);
System.out.println(name + "任务完成!");
cyclicBarrier.await(); // 线程重新堵塞;满足数量条件后会执行 BarrierTask任务
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static class BarrierTask implements Runnable {
boolean flag;
int n;
public BarrierTask(boolean flag, int n) {
this.flag = flag;
this.n = n;
}
@Override
public void run() {
if(flag) {
System.out.println("司令:[士兵" + n + "个,任务完毕!]");
}else {
flag = true;
System.out.println("司令:[士兵" + n + "个,集合完毕!]");
}
}
}
public static void main(String[] args) {
Thread[] allSolider = new Thread[10];
CyclicBarrier barrier = new CyclicBarrier(10,new BarrierTask(false,10));
System.out.println("集合队伍!");
for (int i = 0; i < 10; i++) {
allSolider[i] = new Thread(new Solider("士兵" + i,barrier));
allSolider[i].start();
}
}
}
运行结果
集合队伍!
士兵0报道
.....省略
士兵9报道
司令:[士兵10个,集合完毕!]
士兵2任务完成!
.....省略
士兵4任务完成!
司令:[士兵10个,任务完毕!]
线程堵塞工具类:LockSupport
线程堵塞工具,可以在线程内任意位置让线程堵塞。与Thread.suspend()类似可以让线程暂停挂起,不同的是使用LockSupport 不需要先获取锁,也不用担心 Thread.resume() 唤醒方法发生在暂停方法之前从而导致线程一直堵塞的问题。
常用方法
方法为静态方法
LockSupport.park(); // 在当前线程堵塞
LockSupport.unpark(t1); // 唤醒t1线程
Demo
public class LockSupportDemo {
public static Object object = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
@Override
public void run() {
synchronized (object) {
System.out.println("in:" + getName());
LockSupport.park(); // 挂起线程
System.out.println(getName() + ":end;");
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
LockSupport.unpark(t1); // 唤醒t1线程
LockSupport.unpark(t2);
t1.join();
t2.join();
System.out.println("end");
}
}