之前我们已经知道加锁有两种方式synchronized自动加锁和Lock手动加锁,手动加锁可以通过Lock的实现类ReentrantLock去实现,那么使用它需要注意什么问题呢?我们再来简单回顾一下哎:
a.lock操作要写在try之前;如果lock写在try里面,可能产生引发的锁状态异常将原本的业务异常覆盖掉,增加了调试困难。
b.使用了lock之后,一定要在finally中释放锁unlock,否则锁被一直占用,会出现死锁问题。
Semaphore应用:如秒杀系统,允许前多少用户,后面的用户阻塞
Semaphore代码演示:
public class TestDemo1 {
public static void main(String[] args) {
/*
这里以停车场作为例子
*/
//只传入int创建非公平同步锁 传入两个参数int boolean创建公平同步锁
Semaphore semaphore = new Semaphore(2); //可以根据实际情况限流,2代表停车位数量
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100));
for(int i = 0; i < 4; i++){
//创建任务
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"到达停车场");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//试图进入停车场
try {
//acquire会引发一个异常
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//当代码执行到此处,说明已经获取到锁了
System.out.println(Thread.currentThread().getName()+"进入停车场");
int num = 1 + new Random().nextInt(5); //1-5
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"已经离开停车场了...");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//必须释放锁,因此放在finally中
semaphore.release();
}
}
});
}
}
}
执行结果展示:
对于Semaphore而言比较重要的三步是:
//1.创建锁对象
Semaphore semaphore = new Semaphore();
//2.尝试获取锁
semaphore.acquire();
//3.释放锁
semaphore.release();
作用:计数器是用来保证一组线程都完成某个操作之后,才会去执行后面的任务。
先根据源码看结构:
1)await():执行等待操作;当线程数量不满足CountDownLatch时,执行await()会阻塞等待,直到线程数量满足条件后,才去执行await()之后的代码。
2)countDown():计数
面试题:CountDownLatch是如何实现的?
在CountDownLatch里面存在一个计数器,每次调用countDown()时,计数器数量减1,直到计数器数量位0时,执行后面的代码。
计数器代码演示:
public class TestDemo2 {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5); //计数器初始值为5
for(int i = 1; i < 6; i++){
int id = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+
"开始起跑...");
try {
Thread.sleep(id*1000); //跑步时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+
"到达终点...");
countDownLatch.countDown(); //每到一个人,计数器减1,计数器为0时才会执行await后面的代码
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有人到达重点,宣布排名");
}
}
执行结果展示:
对于CountDownLatch而言重要的三步是:
//1.创建计数器对象,声明计数器初始值
CountDownLatch countDownLatch = new CountDownLatch();
//2.计数器执行减1;减为0才执行await后面代码
countDownLatch.countDown();
//3.阻塞等待
countDown.await();
CountDownLatch缺点:
CountDownLatch的计数器是一次性的,使用完一次之后就不能再使用了。
将上面演示的代码任务数设置为10,而计数器初始值设定为5,我们看一下会发生什么?
CyclicBarrier的出现就是为了解决CountDownLatch计数器不能使用多次的问题
CyclicBarrier代码演示:
public class TestDemo3 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("执行了CyclicBarrier中的Runnable任务");
}
});
for(int i = 1; i < 7; i++){
int id = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+
"开始跑起来...");
try {
Thread.sleep(id*200);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(Thread.currentThread().getName()+"等待其他人。。。");
cyclicBarrier.await(); //计数器减1,判断计数器是否为0
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
//执行到这里,表明一组线程结束满足了条件
System.out.println(Thread.currentThread().getName()+
"执行结束");
}
}).start();
}
}
}
执行结果如下:
cyclicBarrier.await()作用:
1.让计数器减1
2.判断计数器是否为0,如果为0执行后面的代码,不为0则阻塞等待
PS:当计数器为0时,首先会执行await()之后的代码,然后将计数器重置为初始值
。
面试题:CyclicBarrier和CountDownLatch的区别是什么?
答:CyclicBarrier的计数器可以重复使用;而CountDownLatch的计数器只能使用一次。