01入门及简单应用-ReentrantReadWriteLock原理-AQS-并发编程(Java)

news2024/11/19 5:53:54

文章目录

    • 1 概述
    • 2 性质
    • 3 简单测试
    • 4 模拟数据缓存
      • 4.1 应用初始化无缓存
      • 4.2 加入缓存改造
    • 5 后记

1 概述

ReentrantReadWriteLock 是读写锁,和ReentrantLock会有所不同,对于读多写少的场景使用ReentrantReadWriteLock 性能会比ReentrantLock高出不少。在多线程读时互不影响,不像ReentrantLock即使是多线程读也需要每个线程获取锁。不过任何一个线程在写的时候就和ReentrantLock类似,其他线程无论读还是写都必须获取锁。需要注意的是同一个线程可以拥有 writeLock 与 readLock (但必须先获取 writeLock 再获取 readLock, 反过来进行获取会导致死锁)

ReentrantReadWriteLock 类结构图如下1-1所示:在这里插入图片描述

2 性质

ReentrantReadWriteLock锁分读锁和写锁,那么ReentrantReadWriteLock是有2把锁吗?为解答这个问题,我们来看下ReentrantReadWriteLock获取读锁和写锁的源代码,如下2-1所示:

private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;


public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

通过查看源代码,我们知道ReentrantReadWriteLock的读锁和写锁公用一个同步器,公用同一个锁竞争队列,公用一个锁状态。

公用一个锁竞争队列的话如何区分是读锁阻塞的还是写锁阻塞的呢?这个问题等下面我们讲解加锁和解锁原理的时候讲解。

公用一个锁状态state,怎么区分是加的读锁还是写锁,以及怎么记录锁重入的呢?我们继续查看源代码,如下:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • SHARED_SHIFT:读锁占用state的高位位数16
  • SHARED_UNIT:读锁加锁状态+1对应的数值就是加 2 16 2^{16} 216
  • MAX_COUNT:最大锁重入次数 2 16 − 1 2^{16}-1 2161
  • EXCLUSIVE_MASK:计算写锁计数掩码 2 16 − 1 2^{16}-1 2161
  • sharedCount():计算读锁计数,我们知道读锁占用int state高16位,这里通过直接无符号右移16位得到读锁计数
  • exclusiveCount():计数写锁计数,写锁占用int state低16位,通过&EXCLUSIVE_MASK运算,得到写锁计数
    • 对位运算不熟悉的自行查询相关文档

特点:

  • ReentrantReadWriteLock 在多线程环境下的锁关系:读读共享,其他都是互斥,包括读写,写写

    • 我们通过下面一个小测试来验证下。
  • 重入时升级不支持,即持有读锁的情况下获取写锁,会导致回去读锁永久等待。

    • 原理,在下面我们分析加锁原理的时候讲解。
  • 重入时支持锁降级,即持有写锁的情况下获取读锁

  • 读锁不支持条件变量,写锁支持条件变量

3 简单测试

简单做个测试,看看ReentrantReadWriteLock的读写锁如何使用,及验证下锁共享和互相关系,代码如下2-1所示:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author Administrator
 */
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
    public static void main(String[] args) {
        DataContainer dataContainer = new DataContainer();
//        new Thread(() -> {
//            dataContainer.read();
//        }, "t1").start();
//
//
//        new Thread(() -> {
//            dataContainer.write();
//        }, "t2").start();

        new Thread(() -> {
            dataContainer.writeRead();
        }, "t3").start();
    }
}

@Slf4j(topic = "c.DataContainer")
class DataContainer {
    private Object data;
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    public Object read() {
         log.debug("获取读锁...");
         r.lock();
         try {
             log.debug("读取。。。");
             TimeUnit.SECONDS.sleep(1);

         } catch (InterruptedException e) {
             e.printStackTrace();
         } finally {
             log.debug("释放读锁。。。");
             r.unlock();
             return data;
         }
    }

    public void write() {
        log.debug("获取写锁...");
        w.lock();
        try {
            log.debug("写数据。。。");
            TimeUnit.SECONDS.sleep(1);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.debug("释放写锁。。。");
            w.unlock();
        }
    }

    public void readWrite() {
        log.debug("获取读锁...");
        r.lock();
        try {
            log.debug("获取写锁...");
            try {
                w.lock();
                log.debug("写数据。。。");
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                w.unlock();
            }

        } finally {
            log.debug("释放读锁。。。");
            r.unlock();
        }
    }

    public Object writeRead() {
        log.debug("获取写锁...");
        w.lock();
        try {
            try {
                log.debug("获取读锁...");
                r.lock();
                log.debug("读数据。。。");
                TimeUnit.SECONDS.sleep(1);
                return data; 
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            } finally {
                log.debug("释放写锁。。。");
                w.unlock();
            }
        } finally {
            log.debug("释放读锁。。。");
            r.unlock();
        }
    }
}

4 模拟数据缓存

场景描述:在很多应用中,我们都需要对数据库进行读写操作,获取需要的数据。在大多数情况下,数据的读取远大于数据的修改、删除等操作,如果在查询路径及条件一样的情况下,每次都从新从数据库获取数据,很影响系统的性能。这时我们考虑加入缓存,那么在多用户(多线程)环境下,如何保证缓存数据的一致性呢?考虑到读多写少的情况,我们使用ReentrantReadWriteLock读写锁加锁。

关于缓存更新,有2种策略:

  • 先清空缓存在更新数据库
    • 多线程环境下,先清空缓存,此时如果数据还没有更新完成,有其他线程来读取数据,读取就是旧数据,放入缓存。等数据库更新完成,数据库的数据更新了,但是应用一直读取的是旧数据。
  • 先更新数据库在清空缓存
    • 多线程环境下,先更新数据库。在没有更新完数据的情况下,就算有线程读取旧数据,放入缓存。等数据更新完成,情况缓存之后,之后的数据读取都是新数据。此时出现数据不一致的概率大大降低。

要想保证数据的强一致性,需要数据库加锁相关的知识,等后面讲解到Mqsql时,详细说明。这里我们选择先更新数据库在清空缓存的方式。

4.1 应用初始化无缓存

构建个GenericDao工具类实现从数据库读写操作,源代码下:

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.*;

public class GenericDao {
    static String URL = "jdbc:mysql://localhost:3306/exercise";
    static String USERNAME = "root";
    static String PASSWORD = "root";
    {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
        System.out.println("sql: [" + sql + "] params:" + Arrays.toString(args));
        BeanRowMapper<T> mapper = new BeanRowMapper<>(beanClass);
        return queryList(sql, mapper, args);
    }

    public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
        System.out.println("sql: [" + sql + "] params:" + Arrays.toString(args));
        BeanRowMapper<T> mapper = new BeanRowMapper<>(beanClass);
        return queryOne(sql, mapper, args);
    }

    private <T> List<T> queryList(String sql, RowMapper<T> mapper, Object... args) {
        try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD)) {
            try (PreparedStatement psmt = conn.prepareStatement(sql)) {
                if (args != null) {
                    for (int i = 0; i < args.length; i++) {
                        psmt.setObject(i + 1, args[i]);
                    }
                }
                List<T> list = new ArrayList<>();
                try (ResultSet rs = psmt.executeQuery()) {
                    while (rs.next()) {
                        T obj = mapper.map(rs);
                        list.add(obj);
                    }
                }
                return list;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private <T> T queryOne(String sql, RowMapper<T> mapper, Object... args) {
        List<T> list = queryList(sql, mapper, args);
        return list.size() == 0 ? null : list.get(0);
    }

    public int update(String sql, Object... args) {
        System.out.println("sql: [" + sql + "] params:" + Arrays.toString(args));
        try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD)) {
            try (PreparedStatement psmt = conn.prepareStatement(sql)) {
                if (args != null) {
                    for (int i = 0; i < args.length; i++) {
                        psmt.setObject(i + 1, args[i]);
                    }
                }
                return psmt.executeUpdate();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    interface RowMapper<T> {
        T map(ResultSet rs);
    }

    static class BeanRowMapper<T> implements RowMapper<T> {

        private Class<T> beanClass;
        private Map<String, PropertyDescriptor> propertyMap = new HashMap<>();

        public BeanRowMapper(Class<T> beanClass) {
            this.beanClass = beanClass;
            try {
                BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
                PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
                for (PropertyDescriptor pd : propertyDescriptors) {
                    propertyMap.put(pd.getName().toLowerCase(), pd);
                }
            } catch (IntrospectionException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public T map(ResultSet rs) {
            try {
                ResultSetMetaData metaData = rs.getMetaData();
                int columnCount = metaData.getColumnCount();
                Constructor<T> constructor = beanClass.getDeclaredConstructor();
                constructor.setAccessible(true);
                T t = constructor.newInstance();
                for (int i = 1; i <= columnCount; i++) {
                    String columnLabel = metaData.getColumnLabel(i);
                    PropertyDescriptor pd = propertyMap.get(columnLabel.toLowerCase());
                    if (pd != null) {
                        Method method = pd.getWriteMethod();
                        method.setAccessible(true);
                        method.invoke(t, rs.getObject(i));
                    }
                }
                return t;
            } catch (SQLException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

注:基于msyql不同版本,选择合适的msyql驱动器。

  • queryList():查询结果为集合
  • queryOne():查询一个
  • update():更新操作
  • BeanRowMapper():将查询结果集转化为对应的Bean

进行二次同样的查询和一次更新,测试结果如下:

=============>查询
sql: [select * from emp where empno = ?] params:[7369]
Emp(empno=7369, ename=王五, job=销售员, sal=1000.00)
sql: [select * from emp where empno = ?] params:[7369]
Emp(empno=7369, ename=王五, job=销售员, sal=1000.00)
================>更新
sql: [update emp set sal = ? where empno = ?] params:[1500, 7369]
sql: [select * from emp where empno = ?] params:[7369]
Emp(empno=7369, ename=王五, job=销售员, sal=1500.00)

4.2 加入缓存改造

如果确定查询是一样的呢?这里我们构建map,key为SqlPair,值为对应的查询结果。SqlPari成员变量sql模板语句和对应的参数值,通过重写hashcode来保证如果查询语句和参数都相同,那么查询就是一样的。

缓存我们用HashMap模拟。因为是简单的模拟,我们把相应的类和测试放一个文件中。

改造后的带缓冲的dao源代码如下:

static class GenericDaoCached extends GenericDao {
        private GenericDao dao = new GenericDao();
        private Map<SqlPair, Object> map = new HashMap<>();
        private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();

        @Override
        public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
            return dao.queryList(beanClass, sql, args);
        }

        @Override
        public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
            // 先从缓存中找,找到直接返回
            SqlPair key = new SqlPair(sql, args);
            ;
            rw.readLock().lock();
            try {
                T value = (T) map.get(key);
                if (value != null) {
                    return value;
                }
            } finally {
                rw.readLock().unlock();
            }
            rw.writeLock().lock();
            try {
                // 多个线程
                T value = (T) map.get(key);
                if (value == null) {
                    // 缓存中没有,查询数据库
                    value = dao.queryOne(beanClass, sql, args);
                    map.put(key, value);
                }
                return value;
            } finally {
                rw.writeLock().unlock();
            }
        }

        @Override
        public int update(String sql, Object... args) {
            rw.writeLock().lock();
            try {
                // 先更新库
                int update = dao.update(sql, args);
                // 清空缓存
                map.clear();
                return update;
            } finally {
                rw.writeLock().unlock();
            }
        }

        class SqlPair {
            private String sql;
            private Object[] args;

            public SqlPair(String sql, Object[] args) {
                this.sql = sql;
                this.args = args;
            }

            @Override
            public boolean equals(Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }
                SqlPair sqlPair = (SqlPair) o;
                return Objects.equals(sql, sqlPair.sql) &&
                        Arrays.equals(args, sqlPair.args);
            }

            @Override
            public int hashCode() {
                int result = Objects.hash(sql);
                result = 31 * result + Arrays.hashCode(args);
                return result;
            }
        }

查找流程如下:

  • 根据sql语句和参数构建SqlPair
  • 加读锁,先从缓存中获取。
  • 缓存有返回数据,读锁解锁
  • 如果没有加写锁,在此判断缓存中是否没有。如果还是没有从数据库读取。
    • 双重判断,多线程环境下
  • 返回数据,读锁解锁

更新流程:

  • 加写锁
  • 更新数据库
  • 清空缓存
  • 写锁解锁

测试代码部分,GenericDao的实现改为GenericDaoCached,其他同上,测试结果:

=============>查询
sql: [select * from emp where empno = ?] params:[7369]
Emp(empno=7369, ename=王五, job=销售员, sal=1500.00)
Emp(empno=7369, ename=王五, job=销售员, sal=1500.00)
================>更新
sql: [update emp set sal = ? where empno = ?] params:[1600, 7369]
sql: [select * from emp where empno = ?] params:[7369]
Emp(empno=7369, ename=王五, job=销售员, sal=1600.00)

此时相同的查询只在第一次的时候从数据库获取,其他查询从缓存获取。更新后正确的获取更新后的数据。完整代码见下面代码仓库。

下面我们从源代码层面,对ReentrantReadWriteLock进行详细的分析。

5 后记

如有问题,欢迎交流讨论。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

参考:

[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-12-12.p247~p252.

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

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

相关文章

技术分享 | 测试平台开发-前端开发之数据展示与分析

测试平台的数据展示与分析&#xff0c;我们主要使用开源工具ECharts来进行数据的展示与分析。 ECharts简介与安装 ECharts是一款基于JavaScript的数据可视化图表库&#xff0c;提供直观&#xff0c;生动&#xff0c;可交互&#xff0c;可个性化定制的数据可视化图表&#xff…

展锐Android 10平台OTA升级

OTA 整体升级包制作步骤&#xff08;以SC9863A平台为例&#xff09; 下载项目 AP 的代码。通过以下命令设置编译环境。 source build/envsetup.sh lunch kheader 通过 make 命令全编整个工程。进入“device/sprd/sharkle/sl8541e_1h10_32b/”目录&#xff08;board 对应目录&a…

5G无线技术基础自学系列 | 站点详细勘测

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 站点的勘测结果非常重要&#xff0c;直…

高压放大器在硅氧烷近晶相单体合成中的应用

实验名称&#xff1a;高压放大器在硅氧烷近晶相单体合成中的应用 研究方向&#xff1a;晶体材料 测试目的&#xff1a; 双稳态包括向列相双稳态、近晶&#xff21;相双稳态和胆甾相双稳态&#xff0c;目前主要的研究是在近晶&#xff21;相双稳态&#xff0c;由近晶&#xff21…

自动驾驶专题介绍 ———— 转向系统

文章目录转向系统转向器齿轮齿条式循环球式蜗杆曲柄指销式转向助力液压转向助力系统电动转向助力系统发展转向系统 转向系统是按照驾驶员的意图改变或保持汽车行驶方向的系统。根据转向能源的不同&#xff0c;可以将转向系统分为机械转向系统和动力转向系统。   1. 机械转向系…

188: vue+openlayers上传GeoJSON文件,导出CSV格式文件

第188个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+openlayers上传geojson文件,解析文件在地图上显示图形,同时利用上传的文件,获取features数据,整合重新配置格式,导出CSV(Comma Separated Values)形式的文件。如果文件仅包含点要素,则会添加经度和纬度列。 …

能源管理系统的主要功能|瑜岿科技|能源监测

能源管理系统利用过程控制技术、网络通信技术、教据库技术将分布在现场的数据采集监测站、现场控制站以及管理控制中心联系了起来&#xff0c;实现对企业生产数据采集、存储、处理、统计、查询及分析的功能&#xff0c;以及对企业生产数据的监控、分析和诊断&#xff0c;通过对…

Dynamic Few-Shot Visual Learning without Forgetting

摘要 人类视觉系统有显著的能力去毫不费力的从零样本示例中学习新颖概念。机器学习视觉系统上模仿相同的行为是一个有趣的非常具有挑战性的研究问题&#xff0c;这些研究问题有许多实际的优势在真实世界视觉应用上。在这篇文章中&#xff0c;我们目标是去设计一个零样本视觉学…

mysql中MVCC--多版本并发控制

读读:不存在任何安全问题&#xff0c;不需要并发控制 读写:有线程安全问题&#xff0c;脏读、幻读、不可重复读 写写:有线程安全问题&#xff0c;更新丢失 为了解决读写的并发问题 什么是MVCC 只有InnoDB引擎支持mvcc&#xff0c;mysql默认支持可重复读&#xff0c;就是依赖…

常用数据库之mysql的搭建与使用

1. 简介 mysql为关系型数据库&#xff0c;是由瑞典的MySQLAB公司开发的&#xff0c;但是几经辗转&#xff0c;现在是Oracle产品。它是以“客户&#xff0f;服务器”模式实现的&#xff0c;是一个多用户、多线程的小型数据库服务器。而且MySQL是开源数据的&#xff0c;任何人都可…

Qt 模型视图编程之自定义只读数据模型

背景 Qt 中的模型视图架构是用来实现大量数据的存储、处理及其显示的&#xff0c;主要原理是将数据的存储与显示分离&#xff1a;模型定义了标准接口对数据进行访问&#xff1b;视图通过标准接口获取数据并定义显示方式&#xff1b;模型使用信号与槽机制通知视图数据变化。 Q…

基于Java Web技术的动车购票系统

毕 业 设 计 中文题目基于Java Web技术的动车购票系统英文题目Train ticket system based on Web JavaTechnology毕业设计诚信声明书 本人郑重声明&#xff1a;在毕业设计工作中严格遵守学校有关规定&#xff0c;恪守学术规范&#xff1b;我所提交的毕业设计是本人在 指导教师…

零入门容器云网络-7:基于golang编程实现给ns网络命名空间添加额外的网卡

已发表的技术专栏&#xff08;订阅即可观看所有专栏&#xff09; 0  grpc-go、protobuf、multus-cni 技术专栏 总入口 1  grpc-go 源码剖析与实战  文章目录 2  Protobuf介绍与实战 图文专栏  文章目录 3  multus-cni   文章目录(k8s多网络实现方案) 4  gr…

“美亚杯”第三届中国电子数据取证大赛答案解析(个人赛)

试题 1 Gary的笔记本电脑已成功取证并制作成镜像 (Forensic Image)&#xff0c;下列哪个是其MD5哈希值。 A. 0CFB3A0BB016165F1BDEB87EE9F710C9 B. 5F1BDEB87EE9F710C90CFB3A0BB01616 C. A0BB016160CFB3A0BB0161661670CFB3 D. 16160CFB3A0BB016166A0BB0161661…

独立产品灵感周刊 DecoHack #041 - 那些独立开发者是怎么养活自己的

本周刊记录有趣好玩的独立产品设计开发相关内容&#xff0c;每周发布&#xff0c;往期内容同样精彩&#xff0c;感兴趣的伙伴可以点击订阅我的周刊。为保证每期都能收到&#xff0c;建议邮件订阅。欢迎通过 Twitter 私信推荐或投稿。 &#x1f4bb; 产品推荐 1. SOCCER STREAM…

分布式的设计思想

一、分布式设计基础 传统架构问题 ① 单机资源不足 存储&#xff1a;3台机器&#xff0c;每台机器都有2T的硬盘空间&#xff0c;但是现在有1个3T的文件要存储计算&#xff1a;3台机器&#xff0c;每台机器都有8核CPU和8GB内存&#xff0c;但是现在有1个程序需要12核CPU和24G…

启明智显分享| Sigmastar SSD212 SPI+RGB点屏示例(2.1寸 480*480圆屏,可应用于旋钮)

SSD20X 点SPIRGB屏和SSD212 类似&#xff0c;区别在于对应文件名不同、SSD20X没有config.ini文件。 SSD20X SPI初始化文件&#xff1a;vi boot/common/cmd_customer_init.c SSD20X由于没有config.ini 可以用jpeg2disp 中.h 屏参头文件的方式实现显示logo。 这里以SSD212 点屏为…

Java——AVL树

平衡二叉树 在之前的blog中讲到&#xff0c;平衡二叉树是一棵树&#xff0c;任意一个节点的左树的所有节点都小于这个节点&#xff0c;右树的所有节点都大于这个节点 因此&#xff0c;可以利用这个性质来中序遍历&#xff0c;就可以得到一个有序的序列&#xff0c;而如果我们要…

谷歌地图商家抓取工具 G-Business Extractor 7.5

G 业务提取器 | 谷歌地图抓取工具 G-Business Extractor是一款功能强大的工具&#xff0c;可帮助您从 Google 地图中寻找商机。它是最好的Google Maps Scraper工具&#xff0c;能够从最重要的企业目录中提取数据。 Google 地图是一个来源&#xff0c;您可以在其中找到按类别和位…

“美亚杯”第三届中国电子数据取证大赛答案解析(团体赛)

Questions Gary被逮捕后,其计算机被没收并送至计算机取证实验室。经调查后&#xff0c;执法机关再逮捕一名疑犯Eric&#xff0c;并检取其家中计算机(window 8), 并而根据其家中计算机纪录, 执法机关再于其他地方取得一台与案有关的服务器,而该服务器内含四个硬盘。该服务器是运…