CountDownLatch背后的原理

news2024/11/16 6:25:57

前言:

在日常工作中常用到多线程,如果使用多线程处理那么就要考虑同步问题,一般我们会考虑使用加锁来解决。但是还有一些场景,如下:
场景:小升初考试,考生做题,监考老师要等待所有考生交试卷后才可以离开,那么把考生比作多个线程,老师比作主线程。

使用join来实现上述场景:

public void testThread() throws InterruptedException {
    ConcurrentSkipListSet concurrentSkipListSet = new ConcurrentSkipListSet();

    for (int i=0; i< 10; i++) {
        final int t = i;
        Thread  thread = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                System.out.println(name);
                ThreadUtils.doSleep(1000L * t);
                concurrentSkipListSet.add("cz" + t + "--" + name);
            }

        });
        thread.start();
        thread.join();
    }
    System.out.println(concurrentSkipListSet.size());

    for (Object s : concurrentSkipListSet) {
        System.out.println(s.toString());
    }

}

耗时情况:

image.png
join的意思让当前线程陷入等待,主线程启动了thread1后陷入等待,然后等待thread1执行完毕继续启动thread2,然后等待thread2执行完毕继续启动下一个,其实使用了join之后多线程已经成为了串行执行了。

使用CountDownLatch来实现上述场景

public static void testCountDownLatch() throws InterruptedException {
    TimeLag lag = new TimeLag();
    CountDownLatch countDownLatch = new CountDownLatch(10);
    ConcurrentSkipListSet concurrentSkipListSet = new ConcurrentSkipListSet();

    for (int i=0; i< 10; i++) {
        final int t = i;
        Thread  thread = new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                System.out.println(name);
                ThreadUtils.doSleep(1000L * t);
                concurrentSkipListSet.add("cz" + t + "--" + name);
            }

        });
        thread.start();
    }
    countDownLatch.await();

    System.out.println(concurrentSkipListSet.size());

    for (Object s : concurrentSkipListSet) {
        System.out.println(s.toString());
    }

    System.out.println(lag.cost());
}

执行效果如下:

image.png

这次很明显是多线程并行执行的。效率上远远高于使用join,而且也达到了想要的效果。

下面来看下CountDownLatch是什么原理。

image.png

通过api文档可以了解基础用法,要洞悉原理还需要仔细阅读源码和相关文档。
源码中有一个重要的概念就是AQS(AbstractQueuedLongSynchronizer)AQS是java中一个同步器的实现方案,java中除去synchronized关键字之外,其他锁的实现基本上都是基于AQS。

AQS实现原理可以简单的理解为是:
程序实现了一链表,该链表对所有线程可见(使用volatile修饰),线程去竞争锁,竞争到了修改锁状态,竞争失败的要依次排队,最终形成链表。加锁时往链表尾部添加节点,解锁时将头节点删除。这样就形成了一个加锁解锁机制。AQS还提供了共享锁和独占锁的实现

CountDownLatch就是基于AQS来实现的。内部维护一个Sync内部类来继承AQS实现了共享锁的加锁解锁。

AQS

AQS中调用了一个重要的类Unsafe,该类中的方法是一些native修饰的方法,大家都知道用native修饰的方法是调用底层c++实现的,可以和底层硬件交互,Unsafe提供了一个系类重要的方法就是compareAndSwapXXX(XXX包括Object,Int,Long),这个方法可以保证多个线程修改同一个值而不出现并发问题。AQS通过调用Unsafe中的compareAndSwapxxx方法保证了锁的状态不被篡改。

下面模仿AQS原理实现一个自己的锁:

package com.cz.lock;

import pers.cz.utils.LogUtils;
import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.LockSupport;
import java.util.stream.Collectors;

/**
 * @program: Reids
 * @description:
 * @author: Cheng Zhi
 * @create: 2023-02-22 11:06
 **/
public class JefSimpleLock {

    private static JefLogLevel logLevel = JefLogLevel.ERROR;

    private static boolean useUnsafe = true;

    /**
     * 内部类通过反射获取Unsafe
     */
    public static class JefUnsafe {
        public static Unsafe getJefUnsage() {
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                return (Unsafe) field.get(null);
            } catch (Exception e) {
                LogUtils.error(e.getMessage(), e);
            }
            return null;
        }
    }
    /**
     * 使用Unsafe来实现CAS,因为普通类Unsafe使用时必须使用主类加载器加载,否则会抛出异常java.lang.SecurityException: Unsafe
     *  所以private static final Unsafe unsafe = Unsafe.getUnsafe();这种写法是不对的,要通过反射获取
     */
    private static final Unsafe unsafe = JefUnsafe.getJefUnsage();

    /**
     * 维护一个队列用来存储需要等待的线程
     */
    private static Queue<Thread> threadQueue = new ConcurrentLinkedQueue<Thread>();

    /**
     * 记录当前持有锁并且在运行的线程
     */
    private static Thread currentRunThread;

    /**
     * 定义一个变量做为锁的标识, state: 0表示当前锁未被持有,1表示当前锁已被持有
     */
    private static int state = 0;

    private static final long stateOffset;

    static {
        try {
            stateOffset = unsafe.objectFieldOffset // 获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量(对象中的地址)
                    (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }
    /**
     * 尝试获取锁,如果队列中存在线程,说明现在有线程在等待锁,
     * @return
     */
    private boolean tryGetLock() {

        int lockState = state;
        if (threadQueue.size() > 0) {
            if (logLevel.equals(JefLogLevel.DEBUG)) {
                LogUtils.debug("当前等待线程:" + threadQueue.stream().map(p -> p.getName()).collect(Collectors.toList()));
            }
            // 如果当前线程和队列中的队头线程
            Thread thread = Thread.currentThread();
            if (lockState == 0 && thread == threadQueue.peek()) {
                // 如果当前锁不被人持有,并且当前线程是队头,那么这个线程可以运行,
                if (useUnsafe) {
                    compareAndSwap(0, 1) ;
                } else {
                    state = 1;
                }
                // 如果获取到锁,则记录该线程
                currentRunThread = thread;
                if (logLevel.equals(JefLogLevel.DEBUG)) {
                    LogUtils.debug("获取锁成功:" + currentRunThread.getName());
                }
                return true;
            }
        }

        return false;
    }

    /**
     * 加锁,这里加锁的原理就是为了让其他线程等待,只有一个线程运行
     */
    public void lock() {

        // 如果获取到锁,则直接运行。
        if (tryGetLock()) {
            return;
        }
        Thread thread = Thread.currentThread();
        if (logLevel.equals(JefLogLevel.DEBUG)) {
            LogUtils.debug(thread.getName() + "当前线程状态为:" + thread.getState());
        }
        // 如果没有获取到锁,则把这个线程放到队列里去排队里去,然后循环等待
        threadQueue.add(thread);

        while(true) {
            if (tryGetLock()) {
                threadQueue.poll();
                return;
            }
            // 如果获取不到锁则阻塞,等待循环去获取锁。
            LockSupport.park(thread);
        }
    }
    /**
     * 解锁
     */
    public void unlock() {
        Thread currentThread = Thread.currentThread();
        if (currentThread == currentRunThread) {
            // 如果当前线程为持有锁的线程,则释放锁,修改状态
            if (useUnsafe) {
                compareAndSwap(1, 0) ;
            } else {
                state = 0;
            }
            currentRunThread = null;

            Thread peek = threadQueue.peek();
            if (peek != null) {
                // 唤醒队头线程让它去争抢锁
                LockSupport.unpark(peek);
            }
        }
    }

    /**
     * 使用CAS来做状态变更,保证state值不被篡改
     * @param current
     * @param update
     */
    private void compareAndSwap(int current, int update) {
        // 四个参数:
        unsafe.compareAndSwapInt(this, stateOffset, current, update);
    }

    /**
     * 设置日志级别
     * @param logLevel
     */
    public void setLogLevel(JefLogLevel logLevel) {
        this.logLevel = logLevel;
    }

    /**
     * 设置是否使用CAS机制
     * @param isUseUnsafe
     */
    public void isUseUnsafe(boolean isUseUnsafe) {
        useUnsafe = isUseUnsafe;
    }
}

enum JefLogLevel {
    DEBUG,
    INFO,
    WARE,
    ERROR;
}

实现思路:
锁对象内部维护一个队列并且维护一个锁标识。
加锁:如果获取锁成功则修改标识为1,如果获取失败则将线程添加到队列中,然后循环去获取锁(这里用到了一个LockSupport类,该类提供了阻塞线程的功能)。
解锁:解锁时,如果判断当前持有锁的线程和要解锁的线程是同一个,则将锁标识修改为0,然后将队列的第一个线程唤醒。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第19天,点击查看活动详情

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/431392.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文阅读【2】-SepViT: Separable Vision Transformer论文结构漫谈与Python实现测试

可分离卷积ViT实现轻量级transformer结构1. 论文主要工作1.1 摘要内容1.2 写作动机&#xff08;Motivations&#xff09;1.2.1 Transformer Patch结构的巨大计算量问题1.2.2 Swin&#xff1a;针对计算量的优化1.2.3 Twins&#xff1a;针对边缘端部署优化1.2.4 Cswin&#xff1a…

【c#串口通信(2)】串口相关参数介绍

1、端口号&#xff08;Port&#xff09; 我们使用一个串口的时候&#xff0c;首先是要打开这个串口&#xff0c;那么我们怎么知道电脑上现在支持几个串口呢&#xff1f;对应的端口号又是什么呢&#xff1f; 由于我的电脑系统是window11&#xff0c;下面就以window11为例介绍如…

部分国产水文水动力模型介绍

一、HydroMPM模型 1、模型介绍 2016年度自立项目HydroMPM系统开发与集成完成的洪水分析模拟软件等成果经权威专家鉴定整体达到国际领先水平&#xff0c;HydroMPM_FloodRisk入选国家防总《全国重点地区洪水风险图编制项目可选软件名录》。成果应用项目100余项&#xff0c;累计…

spring自定义命名空间

命名空间 如果你曾经在配置datasource是用过properties文件储存我们的数据库连接信息&#xff0c;那么一定在xml文件中配置过这样的语句。 <context:property-placeholder location"classpath:jdbc.properties"/>而我们的spring当中很明显是没有这个context的…

【git】git的一些基础操作

文章目录一.git下载二.git初次操作1.生成公钥2.修改全局用户名和邮箱地址&#xff1a;3.本地仓库关联远端仓库4.本地初始化5.将项目上所有的文件添加到本地仓库6.提交到本地仓库7.创建main分支8.推送到main分支三.git其他操作1.develop分支2.查看分支3.切换分支4.查看分支历史一…

python wannier90 基于wannier90的*_hr.dat文件选取截断hopping绘制能带图

我们知道wannier90可以根据选取TMDs的轨道信息生成详细的hopping energy *_hr.dat文件&#xff0c;选取所有的hopping绘制起来的时候比较简单&#xff0c;但是我们发现取几圈的近似hopping也可以将band表示出来&#xff0c;类似的思想有Pybinding的三带近似&#xff08;DOI: 10…

区块链技术在软件开发中的应用

如果你是一名软件开发者或者IT从业者&#xff0c;你一定已经听说过区块链技术。区块链是一种基于密码学的分布式账本技术&#xff0c;被广泛应用于数字货币、金融、物联网等领域。但是&#xff0c;除了这些领域之外&#xff0c;区块链技术还可以在软件开发中发挥重要作用。本文…

CLIP 论文解读

文章目录模型训练推理实验与Visual N-Grams 相比较分布Shift的鲁棒性不足参考现有的计算机视觉系统用来预测一组固定的预订对象类别&#xff0c;比如ImageNet数据集有1000类&#xff0c;CoCo数据集有80类。这种受限的监督形式限制了模型的通用性和可用性。使用这种方法训练好的…

《花雕学AI》02:人工智能挺麻利,十分钟就为我写了一篇长长的故事

ChatGPT最近火爆全网&#xff0c;上线短短两个多月&#xff0c;活跃用户就过亿了&#xff0c;刷新了历史最火应用记录&#xff0c;网上几乎每天也都是ChatGPT各种消息。国内用户由于无法直接访问ChatGPT&#xff0c;所以大部分用户都无缘体验。不过呢&#xff0c;前段时间微软正…

Nginx实现会话保持,集群模式下session域共享

前言 生产环境下&#xff0c;多数系统为了应对线上多种复杂情况而进行了集群架构的部署&#xff0c;保证系统的高性能、价格有效性、可伸缩性、高可用性等。通常将生产环境下的域名指向Nginx服务&#xff0c;通过它做HTTP协议的Web负载均衡。 session是什么 在计算机中&…

13.广度优先搜索

一、算法内容 1.简介 广度优先搜索BFS&#xff08;Breadth First Search&#xff09;按照广度优先的方式进行搜索&#xff0c;可以理解为“尝试所有下一步可能”地穷举所有可行的方案&#xff0c;并不断尝试&#xff0c;直到找到一种情况满足问题问题的要求。 BFS从起点开始…

C语言——学生信息管理系统(数组)

文章目录一、前言二、目的三、框架1.菜单1.1主菜单1.2子菜单2.流程图2.1总流程图2.2开始流程图2.3增加学生信息流程图2.4.删除学生信息流程图2.5修改学生信息流程图2.6查询学生信息流程图2.7对学生信息排序流程图3.思路四、代码五、演示视频一、前言 因为最近是在赶进度总结&a…

无人驾驶--工控机安装autoware

时隔好久&#xff0c;又来写文章了&#xff0c;这次有高人指点&#xff0c;要系统的学习一下无人驾驶了。 使用的是易咖的底盘车&#xff0c;工控机是米文动力Apex Xavier II&#xff0c;基于autoware框架 首先是在工控机上安装autoware&#xff0c;工控是ubuntu18环境。 参…

Python入门教程+项目实战-9.2节: 字符串的操作符

目录 9.2.1 字符串常用操作符 9.2.2 操作符&#xff1a;拼接字符串 9.2.3 *操作符&#xff1a;字符串的乘法 9.2.4 []操作符&#xff1a;索引访问 9.2.5 [:]操作符&#xff1a;分片字符串 9.2.6 in操作符&#xff1a;查找子串 9.2.7 %操作符&#xff1a;格式化字符串 9…

为什么要做软件测试

随着信息技术的发展和普及&#xff0c;人们对软件的使用越来越普及。但是在软件的使用过程中&#xff0c;软件的效果却不尽如人意。为了确保软件的质量&#xff0c;整个软件业界已经逐渐意识到测试的重要性&#xff0c;软件测试已经成为IT 领域的黄金行业。本篇文章将会带领大家…

使用Tensorboard多超参数随机搜索训练

文章目录1超参数训练代码2远端电脑启动tensorboard完整代码位置https://gitee.com/chuge325/base_machinelearning.git 这里还参考了tensorflow的官方文档 但是由于是pytorch训练的差别还是比较大的&#xff0c;经过多次尝试完成了训练 硬件是两张v100 1超参数训练代码 这个…

Android Studio升级Gradle Plugin升级导致项目运行失败问题

背景&错误 升级Android Studio 旧项目无法运行&#xff0c;奇奇怪怪什么错误都有 例如&#xff1a; java.lang.IllegalAccessError: class org.gradle.api.internal.tasks.compile.processing.AggregatingProcessingStrategy (in unnamed module 0x390ea9fb) cannot acce…

传智健康-day2

一.需求分析(预约管理功能开发) 预约管理功能&#xff0c;包括检查项管理、检查组管理、体检套餐管理、预约设置等、预约管理属于系统的基础功能&#xff0c;主要就是管理一些体检的基础数据。 检查组是检查项的集合 二.基础环境搭建 1导入预约管理模块数据表 需要用到的…

Ubuntu安装MySQL及常用操作

一、安装MySQL 使用以下命令即可进行mysql安装&#xff0c;注意安装前先更新一下软件源以获得最新版本&#xff1a; sudo apt-get update #更新软件源 sudo apt-get install mysql-server #安装mysql 上述命令会安装以下包&#xff1a; apparmor mysql-client-5.7 mysql-c…

不定期更新:我对 ChatGPT 进行多方位了解后的报告,超级全面,建议想了解的朋友看看

优质介绍视频&#xff1a; GPT4前端【AI编程新纪元】 【渐构】万字科普GPT4为何会颠覆现有工作流&#xff1b;为何你要关注微软Copilot、文心一言等大模型 此文章不定期更新&#xff08;一周应该会更新一次&#xff09; 最近一次更新&#xff1a;2023.4.16 12:00 ChatGPT 是什…