上一篇文章中,我们详细剖析了并发工具包 J.U.C 的底层实现 AQS 的细节,不过只解释了基于 AQS 的最常用的同步工具锁 ReentrantLock。诚然 ReentrantLock 是很常用、很重要的一个同步工具类。不过基于 AQS 其实还有很多同步工具类,下面我们就来学习一下这些同步工具类的简单实用
本篇文章中只介绍同步工具类的基本使用,其底层原理实现都是基于 AQS,如果想要详细理解 AQS 的实现,请移步:https://blog.csdn.net/qq_42583242/article/details/108746299
CountDown 顾名思义叫做倒数,Latch 即门闩,CountDownLatch 又称闭锁。
可以理解为一班固定座位数的客车,滚动发车,当坐满了,没有空座位了就发车。
在程序中根据具体的业务场景,一旦满足某个条件,就调用 latch.countDown() 让 count 减一,一直减到 count 变成0,才允许当前线程向下运行,执行 latch.await() 之后的逻辑。
代码示例:
public class TestCountDownLatch { public static void main(String[] args) { usingCountDownLatch(); System.out.println("业务逻辑"); } private static void usingCountDownLatch() { Thread[] threads = new Thread[10]; CountDownLatch latch = new CountDownLatch(threads.length); for(int i=0; i<threads.length; i++) { threads[i] = new Thread(()->{ int result = 0; for(int j=0; j<100; j++) result += j; //每一个线程结束的时候,让门闩限制减一 latch.countDown(); }); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } try { //指定一个条件,判断什么时候,代码可以继续向下运行,比如100个线程结束之后的定时任务。 latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } //下面可以添加具体的业务逻辑 System.out.println("业务逻辑"); System.out.println("end latch"); } }使用步骤,主要有以下几个方法配合使用:
CountDownLatch latch = new CountDownLatch(count);创建一个闭锁,指定count。即指定座位数量latch.await();阻塞,让逻辑不再向下运行。即人不满车不走latch.countDown();满足条件就让 count 减一,直到 count 为 0,就继续向下运行。即无空座位就发车使用 CountDownLatch 闭锁,就相当于一个门闩栓在哪,使用程序控制什么时候允许向下走,完成定时任务。
构造函数:
//permits:允许的数量 public Semaphore(int permits) { sync = new NonfairSync(permits); } //其底层实现是 AQS,和 ReentrantLock 一样,有公平和非公平的概念,根据构造函数传参来决定创建公平锁还是 //非公平锁 public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }Semaphore 即信号量,允许多少个线程同时执行
应用场景:限流。比如售票,只有 5 个窗口,那 Semaphore 传参 permits 为 5,同时在买票的人只有 5 个人。
代码示例:
public class TestSemaphore { public static void main(String[] args) { //Semaphore s = new Semaphore(2); //允许两个线程执行,并可以指定是否是公平锁 Semaphore s = new Semaphore(2,false); //允许一个线程同时执行 //Semaphore s = new Semaphore(1); new Thread(()->{ try { //从 Semaphore 获的许可,才可以向下允许,如果 Semaphore 已经满了,就不能获得锁 s.acquire(); System.out.println("T1 running..."); Thread.sleep(200); System.out.println("T1 running..."); } catch (InterruptedException e) { e.printStackTrace(); } finally { s.release(); } }).start(); new Thread(()->{ try { //从 Semaphore 获的许可,才可以向下允许,如果 Semaphore 已经满了,就不能获得锁 s.acquire(); System.out.println("T2 running..."); Thread.sleep(200); System.out.println("T2 running..."); s.release(); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }ReadWriteLock 是与 Reentrantlock 同级别的,同样继承自 J.U.C.lock 接口,其底层实现也是 AQS
读写锁其实就是共享锁和排他锁的概念:
共享锁:即 ReadLock 读锁排他锁:即 WriteLock 写锁比如数据库中的某条数据读的时候特别多,写的时候特别少。当某一时刻有大量并发请求此数据,有读有写,此时的处理可以分为以下情况:
不加锁:可能出现写的线程写一半,另外的读线程就把数据读出来了;还有可能出现两个写线程对同一条记录修改,造成数据不一致加互斥锁:如果对数据加 Synchronized ,肯定能保证数据一致性,但是如果大量并发只是查询数据,并没有修改数据,结果却使用 Synchronized,性能就会很低加读写锁:读线程来的时候,对数据加 ReadLock,这样其他读线程能读数据,但是不能修改数据。写线程来的时候会尝试对数据加 WriteLock,其他线程既不能读,也不能写,保证数据一致性。这样既能保证数据一致性,也保证了比较优秀的效率数据库的锁是数据库的锁,程序的锁是程序的锁,两者不要混为一谈,它们不是一个应用层面的东西
数据库的锁是访问数据库的时候,加在数据库上面的,至于读数据的时候是给行加锁,还是给整张表加锁;是加读锁还是写锁,这些数据数据库层面的东西,操作磁盘。
数据从数据库读出来是放在内存中的,程序的锁是作用于内存中的数据,多线程下操作内存中的数据一致性,操作内存。
ReadWriteLock 读写锁是与 Reentrantlock 的 Condition 相当相似的。
代码示例:
public class TestReadWriteLock { static Lock lock = new ReentrantLock(); private static int value; static ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); static Lock readLock = readWriteLock.readLock(); static Lock writeLock = readWriteLock.writeLock(); public static void read(Lock lock) { try { lock.lock(); //故意延时,模拟读取操作 Thread.sleep(1000); System.out.println("read over!"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println("read unlock"); } } public static void write(Lock lock, int v) { try { lock.lock(); //故意延时,模拟写操作 Thread.sleep(1000); value = v; System.out.println("write over!"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) { //如果使用普通的reentryLock,就是排他锁,所有的读操作也都要排着队,一替一个轮流读,效率比较低 // Runnable readR = ()-> read(lock); //如果使用ReentrantReadWriteLock,中的读锁,是共享锁,允许所有的读操作并发执行,效率比较高。 Runnable readR = ()-> read(readLock); // Runnable writeR = ()->write(lock, new Random().nextInt()); Runnable writeR = ()->write(writeLock, new Random().nextInt()); for(int i=0; i<18; i++) new Thread(readR).start(); for(int i=0; i<2; i++) new Thread(writeR).start(); } }使用步骤:同 ReentrantLock 相似,调用方法的时候,根据业务选择共享锁还是独占锁
CyclicBarrier 的底层实现并非 AQS,但是属于同步工具类,所以在此处一并阐述了
Barrier 即栅栏,CyclicBarrier又称为 Java 中关于线程的计数器
CyclicBarrier的概念:相当于是个栅栏,在阻塞运行,当线程数量达到一定限制,会推翻栅栏,向下运行。线程都向下运行之后,栅栏重新起来,阻挡下一批线程,如此循环往复。
也可以理解成类似发电站水坝,达到一定刻度就会开闸放水一次。水位降低以后,重新关闭闸门,直到再次达到一定刻度,如此循环往复。
应用场景:
定时任务:比如服务器当每满100个客户端连接,会做出什么反应,抽奖活动:参与人数满100人自动开奖代码示例:
public class TestCyclicBarrier { public static void main(String[] args) throws InterruptedException { //只传一个参数,表示只是限制20个线程之后,推翻栅栏向下运行,运行后面的代码逻辑 //CyclicBarrier barrier = new CyclicBarrier(20); //可以传两个参数,第二个参数可以重开一个线程,执行相应的业务逻辑 CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() { @Override public void run() { System.out.println("执行CyclicBarrier的任务——有3个人了,抽一次奖"); } }); for(int i=0; i<99; i++) { new Thread(()->{ try { //这里可以书写具体的业务逻辑 System.out.println(Thread.currentThread().getName() + "到达"); barrier.await(); //这里可以写具体的执行逻辑 Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }).start(); } } }执行结果:
Thread-0到达 Thread-1到达 Thread-2到达 执行CyclicBarrier的任务——有3个人了,抽一次奖 Thread-3到达 Thread-4到达 Thread-5到达 执行CyclicBarrier的任务——有3个人了,抽一次奖 ......使用步骤,主要有以下几个方法配合使用:
CyclicBarrier barrier = new CyclicBarrier(count);只传一个参数,表示只是限制指定线程之后,推翻栅栏向下运行,运行后面的代码逻辑CyclicBarrier barrier = new CyclicBarrier(3, new Runnable());可以传两个参数,第二个参数可以重开一个线程,执行相应的业务逻辑barrier.await();阻塞,直到满足一定数量才能推翻栅栏向下运行应用场景:CyclicBarrier 可以完成只有等其他线程都完事了,某些线程才可以继续向下运行,可以完成多个线程相互间协同工作。
一个复杂的操作,需要从数据库、网络、硬盘都读取到指定数据,才可以执行业务:
串行执行:一个线程先从数据库取数据,得到数据之后,再去网络读数据,再去硬盘读数据。一旦发生数据库阻塞、网络延迟等意外,会造成整个流程效率很低(先洗脸,再洗衣服,再吃饭)并发执行:使用三个线程,不同线程执行不同操作,当三个线程都获得数据,此操作就可以执行(先洗衣服,同时洗脸、吃饭)PS:本篇文章只是简单介绍了有可能会用到的同步工具类(好吧,其实用到的情况很少,更多还是为了应付面试。面试造火箭,入职拧螺丝嘛!都懂),有关同步工具类的选择,需要根据具体的业务场景
关联文章:
https://blog.csdn.net/javazejian/article/details/75043422
多线程—Java内存模型与线程
多线程——Volatile 关键字详解
多线程——线程安全及实现机制
多线程——深入剖析 Synchronized
多线程\并发编程——ReentrantLock 详解
多线程/并发编程——CAS、Unsafe及Atomic