专栏首页JVMGCJava同步组件之CyclicBarrier,ReentrantLock
原创

Java同步组件之CyclicBarrier,ReentrantLock

Java同步组件概况

  • CountDownLatch : 是闭锁,通过一个计数来保证线程是否一直阻塞
  • Semaphore: 控制同一时间,并发线程数量
  • CyclicBarrier:字面意思是回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。
  • ReentrantLock:是一个重入锁,一个线程获得了锁之后仍然可以反复加锁,不会出现自己阻塞自己的情况。
  • Condition:配合ReentrantLock,实现等待/通知模型
  • FutureTask:FutureTask实现了接口Future,同Future一样,代表异步计算的结果。

CyclicBarrier介绍

CycliBarrier是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共的屏障点(common barrier point),也称之为栅栏点。通过它可以多个线程之间的相互等待,只有当每个线程都准备就绪后,才能各自完成后续的操作。它和CountDownLatch有相似的地方,都是通过计数器实现。当某个线程调用await()方法之后,该线程就进去了等待状态,计数器执行的是加一操作,当计数器到达初始值,前面调用await()的线程会被唤醒,继续执行后面的操作。由于CyclicBarrier在等待线程释放后,可以被重用,所以被称为循环屏障。

image-20210116105545758
image-20210116105437873

CountDownLatch比较

相同点

  • 都是同步辅助类
  • 使用计数器实现

不同点

  • CyclicBarrier允许一个或多个线程,等待其它一组线程完成操作,再继续执行。
  • CyclicBarrier允许一组线程之间相互等待,到达一个共同点,再继续执行。
  • CountDownLatch不能被复用。
  • CyclicBarrier适合更复杂的业务场景,如计算发生错误,通过重置计数器,并让线程重新执行。
  • CyclicBarrier还提供其它有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量,isBroken方法用来知道阻塞的线程是否被中断。

CountDownLatchCyclicBarrier的场景比较

CyclicBarrier : 好比一扇门,默认情况下关闭状态,堵住了线程执行的道路,直到所有线程都就位,门才打开,让所有线程一起通过。

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

CountDownLatch : 监考老师发下去试卷,然后坐在讲台旁边玩着手机等待着学生答题,有的学生提前交了试卷,并约起打球了,等到最后一个学生交卷了,老师开始整理试卷,贴封条

代码演示

package com.rumenz.task.CyclicBarrier;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier=new CyclicBarrier(2,new Runnable(){
            @Override
            public void run() {
                System.out.println("汇总计算----");
            }
        });

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(()->{

            try{
                System.out.println("计算昨天的数据");
                Thread.sleep(5000);
                cyclicBarrier.await();
            }catch (Exception e){
                e.printStackTrace();
            }

        });

        executorService.execute(()->{
            try{
                System.out.println("计算今天的数据");
                Thread.sleep(3000);
                cyclicBarrier.await();
            }catch (Exception e){
                e.printStackTrace();
            }

        });
        executorService.shutdown();

    }
}
//计算昨天的数据
//计算今天的数据
//汇总计算----

ReentrantLock可重入锁

JAVA中的锁分两类:synchronized关键字与J.U.C所提供的锁。J.U.C的核心锁是ReentrantLock,本质上都是lockunlock的操作。

ReentrantLock(可重入锁)和synchronized的区别

可重入性:ReentrantLock字面意思即为再进入锁,称为可重入锁,其实synchronize所使用的锁也是可以重入的,两者关于这个区别不大,它们都是同一个线程进入一次,锁的计数器进行自增,要等到锁的计数器下降为零时,才能释放锁

锁的实现:synchronized依赖于JVM实现无法了解底层源码,而ReentrantLock基于JDK实现。通过阅读源码,区别就类似于操作系统控制实现与用户使用代码实现。

性能区别:在synchronized优化以前,性能比ReentrantLock差很多,但自从synchronize引入了偏向锁、轻量级锁(自选锁)后 ,也就是自循锁后,两者性能差不多(JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”)。在两种场景下都可以使用,官方更推荐使用synchronized,因为写法更容易。synchronized的优化其实是借鉴了ReentrantLock中的CAS技术,都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

ReentrantLocksynchronized的功能区别

  • synchronized更加便利,它由编译器保证加锁与释放。ReentrantLock需要手动声明和释放锁,所以为了避免忘记手动释放锁造成死锁,所以最好在finally中声明释放锁。
  • ReentrantLock的锁粒度更细更灵活。

ReentrantLock特有功能

  • ReentrantLock可以指定为公平或者非公平,synchronized是非公平锁。(公平锁的意思就是先等待的锁先获得锁)
  • 提供一个Condition类,它可以分组唤醒需要唤醒的线程。不像synchronized要么随机唤醒一个,要么全部唤醒。
  • 提供能够中断等待锁的线程的机制,通过lock.lockInterruptibly()实现,这种机制ReentrantLock是一种自选锁,通过循环调用CAS操作来实现加锁。性能比较好的原因是避免了进入内核态的阻塞状态。想进办法避免线程进入内核阻塞状态, 是我们分析和理解锁设计的关键

如果满足ReentrantLock三个独有的功能,那么必须使用ReentrantLock。其他情况下可以根据性能、业务场景等等来选择synchronized还是ReentrantLock

synchronized的使用场景

 synchronized能做的,ReentrantLock都能做;而ReentrantLock能做的,而synchronized却不一定做得了。性能方面,ReentrantLock不比synchronized差

  • J.U.C包中的锁定类是用于高级情况和高级用户的工具,除非说你对Lock的高级特性有特别清楚的了解以及有明确的需要,或这有明确的证据表明同步已经成为可伸缩性的瓶颈的时候,否则我们还是继续使用synchronized
  • 相比较这些高级的锁定类,synchronized还是有一些优势的,比如synchronized不可能忘记释放锁。 在退出synchronized块时,JVM会自动释放锁,会很容易忘记要使用finally释放锁,这对程序非常有害。
  • 还有当JVM使用synchronized管理锁定请求和释放时,JVM在生成线程转储时能够包括锁定信息,这些信息对调试非常有价值,它们可以标识死锁以及其他异常行为的来源。 而Lock类知识普通的类,JVM不知道哪个线程具有Lock对象,而且几乎每个开发人员都是比较熟悉synchronized

代码演示

package com.rumenz.task;

import com.google.common.net.InternetDomainName;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class ReentrantLockExample {
    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;
    public static Integer count=0;
    // 声明锁的实例,调用构造方法,默认生成一个不公平的锁 
    private final static Lock lock=new ReentrantLock();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(()->{
                   try{
                       semaphore.acquire();
                       update();
                       semaphore.release();
                   }catch (Exception e){
                       e.printStackTrace();
                   }
            countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("count:"+count);

    }

    private static void update() {
        lock.lock();
        try{
            count++;
        }finally {
            lock.unlock();
        }

    }
}
//count:5000

ReentrantLock常用方法

tryLock():仅在调用时锁定未被另一个线程保持的情况下才获取锁定。
tryLock(long timeout, TimeUnit unit):如果锁定在给定的时间内没有被另一个线程保持且当前线程没有被中断,则获取这个锁定。
lockInterruptbily():如果当前线程没有被中断的话,那么就获取锁定。如果中断了就抛出异常。
isLocked():查询此锁定是否由任意线程保持
isHeldByCurrentThread:查询当前线程是否保持锁定状态。
isFair:判断是不是公平锁

ReentrantReadWriteLock 读写锁

public class ReentrantReadWriteLock
    implements ReadWriteLock, java.io.Serializable {
    /** 内部类提供的读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 内部类提供的读锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;
}

ReentrantReadWriteLock提供了ReadLockWriteLock,在没有任何读写锁时,才可以取得写入锁。如果进行读取时,可能有另外一个写入的请求,为了保持同步,读取锁定。

ReentrantReadWriteLock写锁是互斥的,也就是说,读和读是不互斥的,但是读和写,写和读是互斥的。

在没有任何读写锁的时候才可以取得写入锁(悲观读取,容易写线程饥饿),也就是说如果一直存在读操作,那么写锁一直在等待没有读的情况出现,这样我的写锁就永远也获取不到,就会造成等待获取写锁的线程饥饿。所以,此类不能乱用,在使用时一定要掌握其特性与实现方式。

ReentrantReadWriteLock是Lock的另一种实现方式,我们已经知道了ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。

ReentrantReadWriteLock代码演示

package com.rumenz.task.reentrant;

import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;


public class ReentrantLockExample {

    private static Integer clientTotal=5000;
    private static Integer threadTotal=200;
    private static LockMap map=new LockMap();

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        final Semaphore semaphore=new Semaphore(threadTotal);
        for (int i = 0; i < 2500; i++) {
            final Integer m=i;
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    map.put(m+"",m+"");
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        for (int j = 0; j< 2500; j++) {
            final Integer n=j;
            executorService.execute(()->{
                executorService.execute(()->{
                    try{
                        semaphore.acquire();
                        String s = map.get(n + "");
                        System.out.println("===="+s);
                        semaphore.release();

                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                });
            });

        }

        countDownLatch.await();

        executorService.shutdown();
    }

}
//线程安全的一个Map
class LockMap {
    private final Map<String,String> map=new TreeMap<>();
    //声明读写锁
    private final ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
    //获得读写锁中的读锁
    private final Lock rlock=reentrantReadWriteLock.readLock();
    //获得读写锁中的写锁
    private final Lock wlock=reentrantReadWriteLock.writeLock();

    //读取数据
    public String get(String key){
        rlock.lock();
        try{
            return map.get(key);
        }finally {
            rlock.unlock();
        }
    }
    //写入数据
    public String put(String k,String v){
        wlock.lock();
        try{
            return map.put(k,v);
        }finally {
            wlock.unlock();
        }
    }
    //读取数据
    public Set<String> getAllKeys(){
        rlock.lock();
        try {
            return map.keySet();
        }finally {
            rlock.unlock();
        }
    }
}

StampedLock介绍

在JDK1.8中,新增 StampedLock ,它是ReentrantReadWriteLock的增强版,是为了解决ReentrantReadWriteLock的一些不足。正因为ReentrantReadWriteLock出现了读和写是互斥的情况,需要优化,因此就出现了StampedLock!

它控制锁有三种模式(写、读、乐观读)。一个StempedLock的状态是由版本和模式两个部分组成。锁获取方法返回一个数字作为票据(stamp),他用相应的锁状态表示并控制相关的访问。数字0表示没有写锁被锁写访问,在读锁上分为悲观锁和乐观锁。

乐观读: 如果读的操作很多写的很少,我们可以乐观的认为读的操作与写的操作同时发生的情况很少,因此不悲观的使用完全的读取锁定。程序可以查看读取资料之后是否遭到写入资料的变更,再采取之后的措施。

它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。 在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写。使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的

StampedLock源码中的一个案例

package com.rumenz.task.stampedLock;
import java.util.concurrent.locks.StampedLock;

class Point {
    private double x, y;
    // 锁实例
    private final StampedLock sl = new StampedLock();
    // 排它锁-写锁(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    //乐观读锁(tryOptimisticRead)
    double distanceFromOrigin() {
        // 尝试获取乐观读锁(1)
        long stamp = sl.tryOptimisticRead();
        // 将全部变量拷贝到方法体栈内(2)将两个字段读入本地局部变量
        double currentX = x, currentY = y;
        // 检查在(1)获取到读锁票据后,锁有没被其他写线程排它性抢占(3)
        if (!sl.validate(stamp)) {
            // 如果被抢占则获取一个共享读锁(悲观获取)(4)
            stamp = sl.readLock();
            try {
                currentX = x; // 将两个字段读入本地局部变量(5)
                currentY = y; // 将两个字段读入本地局部变量(5)
            } finally {
                // 释放共享读锁(6)
                sl.unlockRead(stamp);
            }
        }
        // 返回计算结果(7)
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX, double newY) {
        // 这里可以使用乐观读锁替换(1)
        long stamp = sl.readLock();
        try {
            // 如果当前点在原点则移动(2)
            while (x == 0.0 && y == 0.0) {
                // 尝试将获取的读锁升级为写锁(3)
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升级成功,则更新票据,并设置坐标值,然后退出循环(4)
                if (ws != 0L) { //这是确认转为写锁是否成功
                    stamp = ws; //如果成功 替换票据
                    x = newX; //进行状态改变
                    y = newY; //进行状态改变
                    break;
                }
                else {
                    // 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试(5)
                    sl.unlockRead(stamp); //我们显式释放读锁
                    stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
                }
            }
        } finally {
            sl.unlock(stamp); //释放读锁或写锁(6)
        }
    }
}

总结

  • synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;
  • ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;
  • StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;
  • StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;
  • 当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;
  • 当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析

    在分析完AbstractQueuedSynchronizer(以下简称 AQS)和ReentrantLock的原理后,本文将分析 java.util.concu...

    田小波
  • Java高并发编程基础三大利器之CyclicBarrier

    前面一篇文章我们《Java高并发编程基础三大利器之CountDownLatch》它有一个缺点,就是它的计数器只能够使用一次,也就是说当计数器(state)减到为...

    java金融
  • Java高并发编程基础三大利器之CyclicBarrier

    前面一篇文章我们《Java高并发编程基础三大利器之CountDownLatch》它有一个缺点,就是它的计数器只能够使用一次,也就是说当计数器(state)减到为...

    java金融
  • 对常用的并发操作 API 和工具类的总结

    Java并发包中通过Lock和Condition两个接口来实现管程,Lock用于解决互斥问题,Condition用于解决同步问题。 对于互斥问题,在java中...

    冬天里的懒猫
  • 基础篇:JAVA原子组件和同步组件

    在使用多线程并发编程的时,经常会遇到对共享变量修改操作。此时我们可以选择ConcurrentHashMap,ConcurrentLinkedQueue来进行安全...

    潜行前行
  • 同步器

    Java提供两种同步机制,一种是内置的synchronize,另外一种就是大名鼎鼎的AQS,基于AQS实现了很多同步器:倒数闩锁(CountDownLatch)...

    搬砖俱乐部
  • 搞懂这几个锁用法,多线程就懂一半了

    synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的。

    java乐园
  • Java Concurrent CyclicBarrier

    CyclicBarrier 是JUC 所提供的比较好用且应用面十分广泛的一个并发工具。 CyclicBarrier 字面意思:循环 屏障,也就是一种循环可使用...

    邹志全
  • 高并发编程-CyclicBarrier深入解析

    CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到所有线程都到达某个公共屏障点(也可以叫同步点),即相互等待的线程都完成调用await方法...

    JavaQ
  • 并发编程-18AQS同步组件之 CyclicBarrier 同步屏障

    CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直...

    小小工匠
  • 高并发之ReentrantLock、CountDownLatch、CyclicBarrier

    ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该...

    用户1289394
  • (81) 并发同步协作工具 / 计算机程序的思维逻辑

    查看历史文章,请点击上方链接关注公众号。 我们在67节和68节实现了线程的一些基本协作机制,那是利用基本的wait/notify实现的,我们提到,Java并发包...

    swiftma
  • AQS之同步器

    字面意思就是循环壁垒,使用上与CountDownLatch类似,不过实现上完全不一样,CyclicBarrier统计的的是调用了CyclicBarrier#aw...

    spilledyear
  • 理解Java并发工具类CyclicBarrier

    CyclicBarrier这个并发工具类和上篇文章中提到的CountDownLatch比较类似,可以把CyclicBarrier看做是可以可以复用的CountD...

    我是攻城师
  • 突击并发编程JUC系列-万字长文解密 JUC 面试题

    CAS(Compare And Swap)指比较并交换。CAS算法CAS(V, E, N)包含 3 个参数,V 表示要更新的变量,E 表示预期的值,N 表示新值...

    山间木匠
  • 源码阅读之CyclicBarrier

    源码阅读是基于JDK7,本篇主要涉及CyclicBarrier常用方法源码分析。文中代码若格式排版不对,可点击底部的阅读原文阅读。 1.概述 CyclicBar...

    JavaQ
  • 史上最强多线程面试44题和答案:线程锁+线程池+线程同步等

    原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

    Java架构师进阶
  • CyclicBarrier

    CyclicBarrier(循环栅栏) 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch ...

    黑洞代码
  • Java常用业务代码-线程篇

    join类似于同步,当A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行(如下代码), 但是B线程必须已经调用start(...

    每天学Java

扫码关注云+社区

领取腾讯云代金券