Java并发协作工具【锁】
1、可重入锁 ReentrantLock
public ReentrantLock()
public ReentrantLock(boolean fair)
========================
public void lock()
public void lockInterruptibly() throws InterruptedException
public boolean tryLock()
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
public void unlock()
2、读写锁 ReentrantReadWriteLock
通过一个ReadWriteLock产生两个锁,一个读锁,一个写锁。读操作使用读锁,写操作使用写锁。
只有"读-读"操作是可以并行的,“读-写” 和 “写-写” 都不可以。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
3、信号量 Semaphore
3.1、Semaphore的方法
构造方法
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
========================
阻塞获取许可
public void acquire() throws InterruptedException
阻塞获取许可,不响应中断
public void acquireUninterruptibly()
批量获取多个许可
public void acquire(int permits) throws InterruptedException
public void acquireUninterruptibly(int permits)
尝试获取
public boolean tryAcquire()
限定等待时间获取
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException
释放许可
public void release()
3.2、举个栗子
public class AccessControlService {
//一般锁只能由持有锁的线程释放,而Semaphore表示的只是一个许可数,任意线程都可以调用其release方法。
private final static Semaphore permits = new Semaphore(100, true);
public boolean login(String name, String password) {
if (!permits.tryAcquire()) {
// 同时登录用户数超过限制
throw new RuntimeException("超出Semaphore限制");
}
// ..其他验证
return true;
}
public void logout(String name) {
permits.release();
}
public static void main(String[] args) {
AccessControlService acs = new AccessControlService();
for (int i = 0; i < 100; i++) {
acs.login("michael", "michael");
}
}
}
4、倒计时门栓 CountDownLatch
4.1、CountDownLatch 概念
CountDownLatch 是一个同步工具类,可以使一个或多个线程一起等待某些线程各自执行完毕后再执行相关操作。
CountDownLatch 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后调用await方法的等待线程就可以恢复工作了。
CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。
4.2、官方使用 demo
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CountDownLatch.html
- 官方demo1
class Driver { // ...
void main() throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
doSomethingElse(); // don't let run yet
startSignal.countDown(); // let all threads proceed
doSomethingElse();
doneSignal.await(); // wait for all to finish
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}
- 官网demo2
class Driver2 { // ...
void main() throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = ...
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
}
void doWork() { ... }
}
4.2、CountDownLatch 常用方法说明
构造方法,创建一个值为count 的计数器。
public CountDownLatch(int count) //
========================
阻塞当前线程,将当前线程加入阻塞队列。
public void await() throws InterruptedException//
在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
public boolean await(long timeout, TimeUnit unit) throws InterruptedException//
对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
public void countDown()//
4.3、举个栗子
4.3.1、主从协作:主线程等待子线程执行完
主线程await(),子线程countDown()。
public class MasterWorkerDemo {
static class Worker extends Thread {
CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println("run ...");
} catch (Exception e) {
// log
} finally {
this.latch.countDown();
}
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Worker(latch).start();
}
latch.await();//此处会等待CountDownLatch减为0,才会向下执行
System.out.println("collect worker results");
}
}
========输出结果========
run ...
run ...
run ...
run ...
run ...
collect worker results
4.3.2、同时开始:主线程控制子线程同时开始执行
主线程countDown(),子线程await()。
public class RacerWithCountDownLatch {
static class Racer extends Thread {
CountDownLatch latch;
public Racer(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
this.latch.await();
System.out.println(getName() + " start run " + System.currentTimeMillis());
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < 7; i++) {
new Racer(latch).start();
}
Thread.sleep(1000);
System.out.println("开始跑步......");
latch.countDown();
}
}
========输出结果========
开始跑步......
Thread-1 start run 1693985211982
Thread-3 start run 1693985211982
Thread-2 start run 1693985211982
Thread-0 start run 1693985211982
Thread-6 start run 1693985211982
Thread-5 start run 1693985211982
Thread-4 start run 1693985211982
4.3.3、用CountDownLatch 来优化我们的报表统计
运营系统有统计报表、业务为统计每日的用户新增数量、订单数量、商品的总销量、总销售额…等多项指标统一展示出来,因为数据量比较大,统计指标涉及到的业务范围也比较多,单个指标只要几秒钟,而且指标是串行的方式去进行统计的,所以整体下来页面渲染需要将近一分钟。
修改指标从串行化的执行方式改成并行的执行方式,那么整个页面的时间的渲染时间就会大大的缩短。这里使用CountDownLatch 来完成此功能。
模拟业务逻辑
1、分别统计4个指标用户新增数量、订单数量、商品的总销量、总销售额。
2、假设每个指标执行时间为3秒。如果是串行化的统计方式那么总执行时间会为12秒。
3、我们这里使用多线程并行,开启4个子线程分别进行统计。
4、主线程等待4个子线程都执行完毕之后,返回结果给前端。
public class CollectionFlag {
//用于聚合所有的统计指标
// private volatile static Map<String, Integer> map = new HashMap<>(); 这个map有问题哦
private volatile static Map<String, Integer> map = new ConcurrentHashMap<>();
//创建计数器,这里需要统计4个指标
private final static CountDownLatch countDownLatch = new CountDownLatch(4);
public static void main(String[] args) {
//记录开始时间
long startTime = System.currentTimeMillis();
Thread countUserThread = new Thread(() -> {
try {
System.out.println("正在统计新增用户数量");
Thread.sleep(3000);//任务执行需要3秒
map.put("userNumber", 1);//保存结果值
System.out.println("统计新增用户数量完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();//标记已经完成一个任务
}
});
Thread countOrderThread = new Thread(() -> {
try {
System.out.println("正在统计订单数量");
Thread.sleep(3000);//任务执行需要3秒
map.put("countOrder", 2);//保存结果值
System.out.println("统计订单数量完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();//标记已经完成一个任务
}
});
Thread countGoodsThread = new Thread(() -> {
try {
System.out.println("正在商品销量");
Thread.sleep(3000);//任务执行需要3秒
map.put("countGoods", 3);//保存结果值
System.out.println("统计商品销量完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();//标记已经完成一个任务
}
});
Thread countmoneyThread = new Thread(() -> {
try {
System.out.println("正在总销售额");
Thread.sleep(3000);//任务执行需要3秒
map.put("countmoney", 4);//保存结果值
System.out.println("统计销售额完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();//标记已经完成一个任务
}
});
//启动子线程执行任务
countUserThread.start();
countGoodsThread.start();
countOrderThread.start();
countmoneyThread.start();
try {
//主线程等待所有统计指标执行完毕
countDownLatch.await();
long endTime = System.currentTimeMillis();//记录结束时间
System.out.println("------统计指标全部完成--------");
System.out.println("统计结果为:" + map.toString());
System.out.println("任务总执行时间为" + (endTime - startTime) / 1000 + "秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
========输出结果========
正在统计新增用户数量
正在总销售额
正在统计订单数量
正在商品销量
统计新增用户数量完毕
统计销售额完毕
统计订单数量完毕
统计商品销量完毕
------统计指标全部完成--------
统计结果为:{countmoney=4, countOrder=2, userNumber=1, countGoods=3}
任务总执行时间为3秒
4.4、CountDownLatch实现原理
1、创建计数器
当我们调用CountDownLatch countDownLatch=new CountDownLatch(4) 时候,此时会创建一个AQS的同步队列,并把创建CountDownLatch 传进来的计数器赋值给AQS队列的 state,所以state的值也代表CountDownLatch所剩余的计数次数;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);//创建同步队列,并设置初始计数器值
}
2、阻塞线程
当我们调用countDownLatch.wait()的时候,会创建一个节点,加入到AQS阻塞队列,并同时把当前线程挂起。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
判断计数器是技术完毕,未完毕则把当前线程加入阻塞队列
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//锁重入次数大于0 则新建节点加入阻塞队列,挂起当前线程
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
构建阻塞队列的双向链表,挂起当前线程
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//新建节点加入阻塞队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获得当前节点pre节点
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);//返回锁的state
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//重组双向链表,清空无效节点,挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
3、计数器递减
当我们调用countDownLatch.down()方法的时候,会对计数器进行减1操作,AQS内部是通过释放锁的方式,对state进行减1操作,当state=0的时候证明计数器已经递减完毕,此时会将AQS阻塞队列里的节点线程全部唤醒。
public void countDown() {
//递减锁重入次数,当state=0时唤醒所有阻塞线程
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//递减锁的重入次数
if (tryReleaseShared(arg)) {
doReleaseShared();//唤醒队列所有阻塞的节点
return true;
}
return false;
}
private void doReleaseShared() {
//唤醒所有阻塞队列里面的线程
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//节点是否在等待唤醒状态
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//修改状态为初始
continue;
unparkSuccessor(h);//成功则唤醒线程
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
5、循环栅栏 CyclicBarrier
CyclicBarrier是一个栅栏,所有线程在到达该栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是循环的,可以用作重复的同步。
CyclicBarrier特别适用于并行迭代计算,每个线程负责一部分计算,然后在栅栏处等待其他线程完成,所有线程到齐后,交换数据和计算结果,再进行下一次迭代。
5.1、CyclicBarrier的方法
有一个数字的构造方法,表示的是参与的线程个数。
public CyclicBarrier(int parties)
还有一个构造方法,接受一个Runnable参数,
表示栅栏动作,当所有线程到达栅栏后,在所有线程执行下一步动作前,运行参数中的动作,这个动作由最后一个到达栅栏的线程执行。
public CyclicBarrier(int parties, Runnable barrierAction)
await在等待其他线程到达栅栏,调用await后,表示自己已经到达,如果自己是最后一个到达的,就执行可选的命令,
执行后,唤醒所有等待的线程,然后重置内部的同步计数,以循环使用。
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
await可以被中断,可以限定最长等待时间,中断或超时后会抛出异常。
需要说明的是异常BrokenBarrierException,它表示栅栏被破坏了,什么意思呢?
在CyclicBarrier中,参与的线程是互相影响的,只要其中一个线程在调用await时被中断了,或者超时了,栅栏就会被破坏。
此外如果栅栏动作抛出了异常,栅栏也会被破坏,被破坏后,所有在调用await的线程就会退出,抛出BrokenBarrierException。
5.2、举个栗子
public class CyclicBarrierDemo {
static class Tourist extends Thread {
CyclicBarrier barrier;
public Tourist(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
// 模拟先各自独立运行
Thread.sleep((int) (Math.random() * 1000));
// 集合点A
barrier.await();
System.out.println(this.getName() + " arrived A " + System.currentTimeMillis());
// 集合后模拟再各自独立运行
Thread.sleep((int) (Math.random() * 1000));
// 集合点B
barrier.await();
System.out.println(this.getName() + " arrived B " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
int num = 3;
CyclicBarrier barrier = new CyclicBarrier(num, new Runnable() {
@Override
public void run() {
System.out.println("all arrived " + System.currentTimeMillis() + " executed by " + Thread.currentThread().getName());
}
});
for (int i = 0; i < num; i++) {
new Tourist(barrier).start();
}
}
}
CyclicBarrier
与CountDownLatch
强调下其区别:
1、CountDownLatch
的参与线程是有不同角色的,有的负责倒计时,有的在等待倒计时变为0,负责倒计时和等待倒计时的线程都可以有多个,它用于不同角色线程间的同步。
2、CyclicBarrier
的参与线程角色是一样的,用于同一角色线程间的协调一致。
3、CountDownLatch
是一次性的,而CyclicBarrier
是可以重复利用的。
6、小结
- 在读多写少的场景中使用
ReentrantReadWriteLock
替代ReentrantLock
,以提高性能。 - 使用
Semaphore
限制对资源的并发访问数。 - 使用
CountDownLatch
实现不同角色线程间的同步。 - 使用
CyclicBarrier
实现同一角色线程间的协调一致。