第十章:聊聊ThreadLocal

news2024/11/29 0:38:55

  • 是什么?
  • 能干嘛?
  • 常用API
    • 案例一
    • 以上代码存在的问题?
    • 演示线程池复用本地变量的情况
  • ThreadLocal源码
    • Thread,ThreadLocal,ThreadLocalMap 三者的关系?
    • ThreadLocal 的 get 方法
    • set、remove 方法
    • 总结
  • ThreadLocal 之内存泄漏
    • 四大引用
    • ThreadLocal为什么要使用弱引用
    • ThreadLocal使用弱引用就没有问题了吗?
  • 总结

是什么?

官网描述

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

能干嘛?

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题

在之前多个线程共享同一个数据就有可能存在数据安全问题,因此使用 synchronized、lock/unlock、CAS 保证安全

而ThreadLocal可以让每个线程都有一份数据,这样就避免了多个线程同时操作同一个数据而带来的线程安全问题。

常用API

构造器:

Constructor and Description
ThreadLocal() 创建线程局部变量。

常用方法:

Modifier and TypeMethod and Description
Tget() 返回当前线程的此线程局部变量的副本中的值。
protected TinitialValue() 返回此线程局部变量的当前线程的“初始值”。
voidremove() 删除此线程局部变量的当前线程的值。
voidset(T value) 将当前线程的此线程局部变量的副本设置为指定的值。
static <S> ThreadLocal<S>withInitial(Supplier<? extends S> supplier) 创建线程局部变量。

案例一

通过一个案例熟悉使用 ThreadLocal

某房地产公司要求统计销售员总共卖出的房子(假设有5个销售员)

代码

public class SaleHouseDemo {
    public static void main(String[] args) {
        SaleHouse house = new SaleHouse();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5);
                System.out.println(Thread.currentThread().getName() + " 卖出 " + size);
                for (int i1 = 0; i1 < size; i1++) {
                    house.saleHouse();
                }
            }, String.valueOf(i)).start();
        }
        try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("总共卖出: " + house.saleCount);
    }
}

// 资源类
class SaleHouse {
    int saleCount = 0 ;

    public synchronized void saleHouse() {
        ++saleCount;
    }
}

5 卖出 4
3 卖出 4
1 卖出 2
2 卖出 4
4 卖出 3
总共卖出: 17

此时修改一下需求,不参与统计,每个销售员自己算自己的。

就需要使用 ThreadLocal 为每一个线程绑定属于自己的变量。不需要别的线程干涉。

ThreadLocal 初始化变量的俩种方式

  • 重写 initialValue 方法
    • 不推荐这种方式,太繁琐。
    // 原始版本,不推荐
    ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0; // 变量的初始值为0
        }
    };
  • 调用静态方法 withInitial
    // 调用静态方法: 推荐
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

代码演示

public class SaleHouseDemo {
    public static void main(String[] args) {
        SaleHouse house = new SaleHouse();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5);
                for (int i1 = 0; i1 < size; i1++) {
                    house.saleHouseByThreadLocal();
                }
                System.out.println(Thread.currentThread().getName() + " 卖出 " + house.threadLocal.get());
            }, String.valueOf(i)).start();
        }
        try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
        // System.out.println("总共卖出: " + house.saleCount);
    }
}

// 资源类
class SaleHouse {
    int saleCount = 0 ;

    public synchronized void saleHouse() {
        ++saleCount;
    }

    // 原始版本,不推荐
    // ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
    //     @Override
    //     protected Integer initialValue() {
    //         return 0;
    //     }
    // };

    // 调用静态方法: 推荐
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void saleHouseByThreadLocal() {
        threadLocal.set(1 + threadLocal.get());
    }
}

5 卖出 1
2 卖出 3
1 卖出 1
4 卖出 1
3 卖出 4

以上代码存在的问题?

在使用完本地变量后,要记着使用 remove 方法清除。否则会造成 内存泄漏 的问题。

其实在以上场景中,你会发现并没有出现问题,因为我们每次创建线程都是新 new 的,线程也没有重复使用。但是在线程池的场景下,就可能出现问题了,请看阿里巴巴规范的说明。

阿里巴巴规范

image-20221129221743063

当使用完 本地变量,要在 finally 语句块里清除。

修改以上代码
image-20221129222958892

演示线程池复用本地变量的情况

阿里规范中说明:如果本地变量使用完未清除,在线程池的场景下,很可能会出现重复利用的问题。

演示代码

public class ThreadLocalTest {
    public static void main(String[] args) {
        Resource resource = new Resource();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        try {
            for (int i = 0; i < 10; i++) {
                executorService.submit(() -> {
                    Integer begin = resource.threadLocal.get();
                    resource.add();
                    Integer end = resource.threadLocal.get();
                    System.out.println(Thread.currentThread().getName() + " begin = " + begin + " end = " + end);
                });
            }
        } finally {
            executorService.shutdown();
        }
    }
}

class Resource {
    int num = 0;

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void add() {
        threadLocal.set(1 + threadLocal.get());
    }
}

输出结果:

pool-1-thread-1 begin = 0 end = 1
pool-1-thread-3 begin = 0 end = 1
pool-1-thread-2 begin = 0 end = 1
pool-1-thread-1 begin = 1 end = 2
pool-1-thread-2 begin = 1 end = 2
pool-1-thread-3 begin = 1 end = 2
pool-1-thread-2 begin = 2 end = 3
pool-1-thread-1 begin = 2 end = 3
pool-1-thread-2 begin = 3 end = 4
pool-1-thread-3 begin = 2 end = 3

在每次执行的任务中,有的任务重复利用了之前任务的变量值,这很有可能影响后序的逻辑,因此在使用完本地变量时,要记着清除~!

image-20221129224107694

执行结果:

在这里插入图片描述

ThreadLocal源码

Thread,ThreadLocal,ThreadLocalMap 三者的关系?

线程类 Thread 中存在有 ThreadLocalMap的属性,每次新建Thread 都会有一个新的 ThreadLocalMap, 这就是为什么 每个线程独有一份 本地变量

image-20221130113918695

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,并且静态内部类Entry 继承了 WeakReference 弱引用

image-20221130115219017

三者关系图
在这里插入图片描述

  • threadLocalMap实际上就是一个以 threadLocal实例为key,任意对象为value的Entry对象。
  • 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

如果对这俩句话还是不懂的话,继续往下看源码。

ThreadLocal 的 get 方法

get 方法用于获取当前线程本地变量的副本

    public T get() {
        // 首先获取当前线程
        Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap,具体的获取方法就是调用Thread类中的 threadlocals 属性。参考图1
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 如果不为空,就获取 ThreadLocal对象的 Entry 对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 如果 e 不为null,就通过Entry对象获取value,也就是本地变量的值。
                T result = (T)e.value;
                return result;
            }
        }
        // 如果 map = null ,也就是在我们创建 ThreadLocal 没有进行初始化。会执行这个方法进行初始化
        return setInitialValue();
    }

在这里插入图片描述

初始化 ThreadLocalMap

    private T setInitialValue() {
        // 参考图二:对本地变量进行初始化 null ,这就是为什么说在创建ThreadLocal时一定要初始化变量,否则变量值就是 null
        T value = initialValue();
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // map 不为空,将当前对象(ThreadLocal)作为key,本地变量值作为 value 增加到map中
            map.set(this, value);
        else
            // 如果map为空,就新创建一个Map,参考图三
            createMap(t, value);
        return value;
    }

image-20221130114906192

创建一个 ThreadLocalMap,仍然是以 ThreadLocal对象为 key,本地变量值为 value

在这里插入图片描述

set、remove 方法

弄清楚了 get() 方法,set和 remove 方法就很好懂了。



    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

总结

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:

image-20221130120651236

JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中), 每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

ThreadLocal 之内存泄漏

阿里巴巴的规范手册中说明:

如果使用完本地变量后,不进行回收,可能会造成内存泄漏

在这里插入图片描述

为什么会造成内存泄漏呢?谁造成的呢?

正是存入 ThreadLocalMap 中的 Entry 对象。

回顾一下ThreadLocalMap

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个 弱引用对象;

(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>

在这里插入图片描述

四大引用

关于什么是 四大引用,我在 JVM 篇章中也有过介绍,这里不做多说明。包括 对象的finalization机制

弱引用和ThreadLocal的关系

image-20221130150840933

每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储:

调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~

ThreadLocal为什么要使用弱引用

看一下这个ThreadLocal代码:

    public void function01() {
        ThreadLocal<Object> tl = new ThreadLocal<>();
        tl.set(1);
        tl.get();
    }

对应的内存关系图

局部变量 tl 会保存虚拟机栈中,指向堆中的 THreadLocal 的对象,这就是强引用。

而在执行 set 方法时,该线程的 ThreadLocalMap 的key 会指向ThreadLocal 对象。

  • 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
  • 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

image-20221130153025854

ThreadLocal使用弱引用就没有问题了吗?

虽然 ThreadLocal 使用了若引用减少了内存泄漏的几率。但还可能会存在内存泄漏。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。

虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

总结起来就俩句话

  • ThreadLocalMap使用 ThreadLocal的弱引用作为 key,当 ThreadLocal没有外部引用 指向它的时候,能够被 GC 回收,也就是 将 key 设置为 null
  • 虽然 ThreadLocal 能被回收,但是 value 还是存在的(和HashMap一样,允许key为null 存在)。因此使用弱引用不能百分百保证不会出现内存泄漏,还需要在使用完 ThreadLocal 后调用remove方法清除

在上面说,set、get方法会去检查所有键为null的Entry对象,那么在源码中时如何体现的?

get方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hd5d12xm-1669802720879)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202211301804535.png)]

getEntryAfterMiss 调用了 expungeStaleEntry 方法,在此方法中,将value设置为了null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1FclbVe-1669802720880)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202211301804536.png)]

set 方法

image-20221130160326978

remove 方法:

同样调用了expungeStaleEntry 方法

image-20221130160500960

总结

从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。

总结

ThreadLocal 使用建议:

  1. 一定要进行初始化避免空指针问题ThreadLocal.withInitial(()- > 初始化值); 【强制】
  2. 建议把ThreadLocal修饰为static【建议】
  3. 用完记得手动remove 【强制】

阿里巴巴规范

image-20221130160824536

ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。

所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。


  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,
  • 该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry, cleanSome Slots,replaceStaleEntry这三个方法回收键为 null 的 Entry
    .(img-CkDpd1kt-1669802720882)]

ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。

所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。



各位彭于晏,如有收获点个赞不过分吧…✌✌✌

Alt


gongzhonghao 回复 [JUC] 获取MarkDown笔记

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

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

相关文章

Java自动化测试调试中遇到的问题

前言:记录下遇到的问题 Java自动化测试在调试中遇到的问题总结 1、遇到的Waring:must be unique [WARNING] dependencies.dependency.(groupId:artifactId:type:classifier) must be unique: com.vertica.jdbc:vertica-jdbc:jar -> duplicate declaration of version 10.…

python的安装及常用语法(一)

python的安装及简单使用python解释器的安装1. windows系统安装方法2. 测试安装是否成功PyCharm代码编辑器的安装python的基础语法1. “input”用法&#xff1a;用户输入信息2. “bin”用法&#xff1a;将十进制转换为二进制3. “print”用法&#xff1a;输出信息4. 练习1. 张三…

modbus协议讲解,上位机与PLC交互

开发过程中modbus-RTU需要电脑USB口对外交互&#xff0c;参考我的文章&#xff1a;C# 通过Com端口和单片机通讯&#xff08;232/485/USB&#xff09;_花开莫与流年错_的博客-CSDN博客_c#与单片机通信 上位机和PLC交互的消息在Excel中配置&#xff0c;配置后通过Modbus协议加载…

数据结构之-队列实现

队列是一个先进先出的结构&#xff0c;可以用链表呀&#xff0c;数组来实现它&#xff0c;我们今天用数组实现个队列&#xff0c;以优先级队列方式&#xff0c;我们看看怎么实现&#xff0c;优先级队列以队列存储时规则会将即将过期的或较小的数据存储在前面&#xff0c;这样取…

【生成式网络】入门篇(四):CycleGAN 的 代码和结果记录

CycleGAN是一个里程碑式的工作&#xff0c;开启了unpaired的风格迁移的先河&#xff0c;斑马转马的效果还是很震惊。 具体原理可以参考 https://zhuanlan.zhihu.com/p/402819206 老习惯&#xff0c;直接上code&#xff0c;然后按照code进行一些解释 代码参考自 https://github.…

自托管书签管理器LinkAce

本文完成于 9 月下旬&#xff0c;当时的版本是 v1.10.4&#xff0c;发稿时最新版本为 v1.10.5 什么是 LinkAce &#xff1f; LinkAce 是一个自托管档案&#xff0c;用于收集您喜爱的网站的链接&#xff0c;并保存文章以供日后阅读。LinkAce 提供了一个长期存档来存储指向网站、…

NVIDIA 7th SkyHackathon(二)开发套件的安装与测试

1.NeMo 开源工具包 1.1 关于 NeMo NeMo&#xff08;Neural Modules&#xff09;是 NVIDIA 发布的基于 PyTorch 的开源工具包&#xff0c;它允许开发者快速构建、训练和微调会话式人工智能模型 NeMo 由 NeMo Core 和 NeMo Collection 组成&#xff0c;NeMo Core 为所有模型和…

[附源码]Python计算机毕业设计Django的中点游戏分享网站

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

内容理解之情感计算

导语 概念定义&#xff1a;情感计算是自然语言处理领域的重要研究方向之一&#xff0c;其目标是赋予计算机类似于人一样的观察、理解和生成各种情感表达的能力&#xff0c;它是一个高度综合化的跨学科领域&#xff0c;涉及计算机科学、心理学、社会学和认知科学等。通过多学科…

物联网卡有哪些类型

伴随着科学技术的不断升级&#xff0c;不断发展&#xff0c;5G和物联网必定成为未来生活的主角&#xff0c;而现在5G已经慢慢在我们生活越来越常见&#xff0c;这是我们肉眼可以看见的&#xff0c;其实物联网如同5G一样&#xff0c;在我们生活中也随处可见&#xff0c;如我们平…

谈谈Go语言中函数的本质

在很多编程语言中&#xff0c;都会有函数一说&#xff0c;今天我们来聊聊Go语言中的函数。 废话不多说&#xff0c;咱们直接上代码~ 代码 package mainimport "fmt"func main() {fmt.Printf("%T\n", function1)fmt.Printf("%T\n", function2)…

跨越速运如何构建实时统一的运单分析

作者&#xff1a;张杰&#xff0c;跨越速运大数据架构师&#xff08;本文为作者在 StarRocks Summit Asia 2022 上的分享&#xff09; 作为大型现代化综合速运企业&#xff0c;跨越速运拥有 3000 多家服务网点 &#xff0c;日均处理 30 多万票运单。海量运单数据涌来&#xff…

博科交换机使用

博科交换机使用 ip查询 博科交换机的默认IP地址是10.77.77.77&#xff0c;用户名admin&#xff0c;密码&#xff1a;password。 ipaddrshowip修改 ipaddrset端口查询 交换机的端口表示为&#xff08;A,B&#xff09;或者&#xff08;A,B;C,D&#xff09;。 A,C表示交换机的…

使用JLINK给GD32下载程序

使用JLINK给GD32下载程序关于GD32单片机需要的工具和软件包①Jlink仿真器一个②相关软件包下载准备①选择好芯片②在DEBUG中选择JLINK下载现象总结关于GD32单片机 GD32是兆易创新基于Arm Cortex-M内核和RISC-V内核&#xff0c;推出的32位通用微控制器&#xff0c;对比了下两者…

[附源码]Python计算机毕业设计SSM流浪宠物申领信息平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Java中进制基础知识与算法题

本篇文章旨在给大家普及下计算机内部数据的机器级表示方式&#xff0c;即&#xff1a;二进制、八进制、十进制、十六进制… 对于进制&#xff0c;我们从小最先接触的是十进制&#xff0c;这个也是我们日常生活中应用最多的数值统计方式。然而&#xff0c;现实中我们感觉到的媒体…

python的opencv操作记录(十)——图像融合

文章目录前言opencv中的一个方法泊松融合图像梯度图像散度融合图像散度通过散度场进行图像重建泊松融合的一般逻辑前言 最近碰到一个项目上的难题&#xff0c;是要从电动显微镜对焦的多张图像进行融合。因为&#xff0c;显微镜物镜的景深范围较小&#xff0c;可能在同一视野中…

地理空间数据共享资源大汇总

1.全国12.5米分辨率ALOS高程数据 全国12.5米分辨率DEM数据&#xff0c;该数据由锐多宝的地理空间提供并进行分省裁剪与镶嵌&#xff0c;由测绘营地进行影像坐标系转换。 ALOS-12.5m高程数据参数&#xff1a; 覆盖范围&#xff1a;全国&#xff08;仅有小部分区域存在数据空白…

本机使用python操作hdfs搭建及常见问题

一.虚拟机安装CentOS7并配置共享文件夹 二.CentOS 7 上hadoop伪分布式搭建全流程完整教程 三.本机使用python操作hdfs搭建及常见问题 四.mapreduce搭建 五.mapper-reducer编程搭建 本机使用python操作hdfs搭建及常见问题一、环境搭建1.打开虚拟机系统&#xff0c;打开hadoop2.修…

【JavaScript 逆向】极验四代滑块验证码逆向分析

前言 相较于三代滑块&#xff0c;四代的逻辑流程更简短&#xff0c;底图没混淆&#xff0c;某些点校验不严格 声明 本文章中所有内容仅供学习交流&#xff0c;相关链接做了脱敏处理&#xff0c;若有侵权&#xff0c;请联系我立即删除&#xff01; 案例目标 滑动验证码&…