Java EE|多线程代码实例之定时器与线程池

news2024/11/20 9:46:10

文章目录

    • 🔴定时器
      • 什么是定时器以及开发中的作用
      • 标准库中的定时器
      • 定时器的实现
    • 🔴线程池
      • 什么是线程池
      • 标准库中的线程池
        • 创建一个线程池
        • ThreadPoolExecutor构造方法解析
      • 线程池的实现

🔴定时器

什么是定时器以及开发中的作用

程序中的定时器功能与我们现实生活中的定时器功能相似,都有起提示作用,但是与现实生活中闹钟不同的是,程序里的闹钟不仅是提醒,还能真正的去做事情。也就是说它的权限更大,更像是一个机器人,我们给它设定一个时间点让它去做什么事情,而不是说像闹钟一样,只能提醒我们,但是改变不了我们的想法,到底做不做这件事。

我们以后开发中,也会经常使用到定时器,这是软件开发中的一个重要组件。尤其是“网络编程”,比如说我们访问一个网页的话,很容易出现卡的现象,这时我们就可以使用定时器,来进行“止损”。一旦超时,就结束这次访问,不再阻塞/等待。

标准库中的定时器

对于定时器,标准库中提供了一个Timer类,我们可使用这个Timer来做我们想定时做的事情。

Timer类的核心方法是schedule,它包含两个参数.

第一个参数是即将要执行的任务的代码,以Runnable接口的形式呈现或者说这个TimerTask这个抽象类实现了Runnable接口,我们只需要继承这个类重写它的run方法即可;

第二个参数是指定多长时间后执行,单位为毫秒(millisecond)。

public class Code28_TimerTest {
    public static void main(String[] args) {
        Timer timer=new Timer();
        System.out.println("已经设置好定时器");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行任务1");
            }
        },1000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行任务2");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行任务3");
            }
        },3000);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hh46HNBk-1673685943980)(F:\typora插图\image-20230113231532477.png)]

定时器的实现

(一)思路分析

我们已经知道怎么使用标准库中的定时器,下边我们来自己实现一个定时器。那么在实现定时器之前,我们需要知道定时器需要做什么,才能更好的实现。

1.让被注册的任务能够在指定时间内执行

2.一个定时器可以注册多个任务,并且按照时间的先后执行

那么,接下来我们想想怎么才能达到这样的目的。

首先,他需要按照推迟时间的长短存放我们的任务,我们需要一个数据结构存放,不难想到需要队列,又因为由时间先后来决定先后,所以我们可以采用一个优先级队列,又因为定时器可以在多线程环境下正常工作,所以我们还需要保证线程安全,所以这里我们最终存储任务的数据结构就是基于堆实现的阻塞队列即PriorityBlockingQueue,因为这里使用的是时间戳,所以不需要额外传比较器,直接创建的就是小根堆.

其次,我们需要一个扫描线程,用来判断是不是到该执行的时间了,确保MyTImer一旦被实例化就能够这个线程就开始工作,所以,我们需要在这个类的构造方法中创建这个线程(不理解,可以先记住在构造方法中需要创建线程这个点)。

然后,对比原来的Timer,还有一个非常重要的成员方法schedule,用来把用户设置的任务和时间啥的放进任务队列,相当于普通队列的offer功能。

最后,因为线程是抢占执行、随机调度的,我们这里就通过wait/notify来控制线程的执行顺序。wait/notify方法的调用需要一个对象,它的阻塞队列和我们存放任务的阻塞队列相互呼应,只不过我们外部看不到。所以我们这里再定义一个私有的Object类对象。

综上我们的MyTimer={私有Object类型对象+存放任务的阻塞队列+连接对象阻塞队列和存放任务队列的扫描线程}。

具体实现细节我们在下边讨论

(二)代码实现

因为我们的任务都是Runnable类型的,与此同时,我们还需要给它配一个时间,所以我们不妨自定义一个MyTask类。因为,任务之间我们是需要排优先级的,是可比较的,所以我们需要实现比较器,这里我们采用实现Comparable接口。随之而来的,我们需要重写compareTo方法。因为这个任务是以runnable形式存在的,而这个runnable我们又是定义在类中的,它是需要显式调用,我们的任务才能工作,所以我们这里需要提供run方法,供外部调用,启动任务。

因为我们可以很容易的通过本地方法currentTimeMillis得到当前的时间,但是我们通过记录每次任务安排时间的时刻,但是这样做免不了有些麻烦,所以我们不如直接放任务时刻就设置成具体的时间点,也就说我们在schedule时时间在原来的基础上在加上当前的时间。

最后,我们需要明确wait和notify的位置以及过程的模拟其实也就是线程怎么周期性扫描的问题。

wait、notify的话肯定是locker调用,然后呢,我们每次去取任务时,如果到时间了,不就直接执行了吗,但是如果不到时间,那么我们需要阻塞等待,所以说,我们的wait就在if逻辑里边的put(会触发堆的调整)之后。又因为wait其实是和join方法一样,可以规定等待的时间,不死等的,那么这个时间定的肯定是现在时间和目标时间的时间差。wait这里就安排好了,记得进行异常处理哦。

那么notify呢?因为上边如果不到时间的话,线程其实已经进入了阻塞等待状态,这个时候我们新加入的任务如果执行时间早于原来等待时间的话,就错过了,所以,这里我们每次新任务加进来的时候,就进行通知。原来如果在阻塞等待,那么就解除阻塞状态,如果没有在等待状态,空打一枪也没关系。这样notify的位置我们也安排好了,记得加锁。

最后,对于线程安全,我们把读写操作捆绑,让这个操作是原子的。

【一般锁的范围我们需要合理控制】

class MyTask implements Comparable<MyTask>{
    //需要执行任务的内容
    private Runnable runnable;
    //推迟的时间
    private long delaytime;

    public MyTask(Runnable runnable, long delaytime) {
        this.runnable = runnable;
        this.delaytime = delaytime;
    }
    public long getDelaytime() {
        return delaytime;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.delaytime-o.delaytime);
    }
    //执行任务!!!!
    public void run(){
        runnable.run();
    }
}
class MyTimer{
    //用来控制线程执行顺序的对象(利用它的阻塞队列)
    private Object locker=new Object();
    //用来存放任务的队列
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
    //扫描线程
    private Thread t;
    public MyTimer(){
        t=new Thread(){
            @Override
            public void run() {
                while (true) {
                    try {
                        synchronized (locker){
                            MyTask myTask=queue.take();
                            long curTime=System.currentTimeMillis();
                            if(curTime<myTask.getDelaytime()){
                                //不到时间,不执行,把任务再塞回去
                                queue.put(myTask);
                                //这里的等待是最长等待时间
                                locker.wait(myTask.getDelaytime()-curTime);
                            }else{
                                myTask.run();
                            }
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();
    }
    public void schedule(Runnable runnable,long after){
        MyTask myTask=new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
}
public class Code29_MyTimer {
    public static void main(String[] args) {
        MyTimer myTimer=new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务1");
            }
        },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务2");
            }
        },2000);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OpABWJho-1673685943982)(F:\typora插图\image-20230114004829899.png)]

🔴线程池

什么是线程池

当当前代码能满足需求的前提下,我们不免想要压缩时间来提高编程的效率。我们已经知道线程是为了解决并发程度很高的情况下,创建/销毁进程时间开销很大的一种优化办法。然而,很多东西需要有对比,当并发程度进一步提高时,多线程确实要比多进程编程效率要高,但是这跟我们预期的效率还差点意思,所以,为了进一步提高并发编程下的效率,前辈们提出了一些方法供我们使用。

1.纤程也称为“轻量级线程”。虽然这种办法能给并发编程带来一系列的优势,但是但是它并没有被广泛纳入标准库中,java就位列其中。不过近些年比较火的GO语言,将它纳入标准库了。

2.“线程池”。与字符串常量池、数据库连接池类似的是,线程池也是提前创建好,随用随去,不需要反复创建/销毁,效率会比较高。我们在java中,还是使用线程池比较多一些。

对于它的概念我们不必抠字眼,只需要理解它的意思,知道它大概是在干什么就可以了。

不过,这里边可能会有一个疑问,为什么从池子中拿和放比反复创建/销毁效率要高?反映到计算机本身上的解释又是什么?

对此,这里给出一种解释。

创建线程/销毁线程都是由操作系统内核来做的;而从池子中获取线程,把线程还到池子里边,我们自己用代码实现。

那么问题进一步转化成为了,为什么由OS内核做事情速度<用户直接做这些事情速度呢?

这里,我们不妨来看个例子:银行管理系统

对于普通用户来讲,他假设正在办理一个业务,需要用到身份证复印件,但是呢,他只带了原件。这个时候,柜员给它提供了两种选择:第一,他帮他去他们的后台复印;第二,用户自己去大厅里边复印。这里我们将情况理想化,假设大厅复印位置无限多或者需要复印的用户无限少,此用户复印之后无需再排队。那么此时就意味着用户直接复印无限快。又因为银行后台不可见,我们只是知道柜员拿着原件去复印了,但是它有没有借此机会去做其他事情或者到底是先做复印这件事还是先复印趁机再做一些其他事情,这些我们都无从得知。但是一般情况下,他们是会的,上厕所或者摸会鱼……那么这就意味着,速度会相对慢。

而实际计算机在执行线程的任务时,因为操作系统内核需要负责的任务比较多,当我们把任务交给它时,其实也就是将任务放到了它的任务队列里边,很大概率不能第一时间执行。(它不存在摸鱼情况,它是一个机器,只是负担太大,忙不过来)而我们如果采用线程池,自己取线程,自己放回(其实run方法执行完了,执行此任务的线程自动解放回归池子),就很大简单了操作系统内核的负担。所以这就是为什么OS内核做事情速度<用户直接做这些事情速度。

另外,我们这里解释一下什么叫做用户态什么叫做内核态。整个计算机等价于创建银行的假设是政府,政府把这个银行的管理员权限交给了柜员,而操作系统内核等价于柜员,剩余的操作系统空间等价于普通用户。一些操作我们不需要内核来做,就可以直接做,而有些必须要更高一级的权限,也就是说我们把部分功能(黑盒)的实现交给了内核,OS内核把黑盒怎么使用给计算机的其他部分说明了。这个黑盒也可以反映到代码上就是api。

程序借用api完成完整操作的过程叫做系统调用,驱动内核完成一些工作。这些黑盒到底是怎么实现的,执行效率是快是慢,我们都无法控制,都是由OS内核独立完成的。

所以,相对而言,用户态程序的执行行为整体是可控的,内核态的执行行为整体是不可控的。

标准库中的线程池

创建一个线程池

java标准库中也提供了现成的线程池,可以直接使用。但是这里还是有些不同的。下边我们来讨论一下,然后给出测试代码。

这个被提供的类叫ThreadPoolExecutor,这里我们需要重点掌握它的构造方法的各个参数的含义,以及submit这个给线程池提交任务的方法。又因为这个类提供的功能过于强大用起来比较麻烦,所以我们一般使用被工厂类Executors包装过的工厂方法构造线程池。下边我们结合测试代码分析。

//for test
public class Code30_ExecutorSevicePoolTest {
    public static void main(String[] args) {
        ExecutorService pool= Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            int n=i+1;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("在线程池中执行任务:"+n);
                }
            });
        }
    }
}

这里跟其他提供组件的使用略有区别,这里使用的是Executors这个类的静态方法,直接构造出对象来,相当于是把new操作隐藏到静态方法里边了。我们每次可以使用submit方法,将任务以Runnable接口的方式交给线程池。

这样把new操作隐藏在静态方法里边的方法就是工厂方法。提供工厂方法的类就叫做工厂类。这种设计模式叫做工厂模式。

那么工厂模式有什么作用呢?

尽可能的避免了构造方法上的坑,比如创建坐标点,有笛卡尔坐标系和极坐标两种体系,这两个参数我们一般都设置成double,此时我们试图通过重载完成任务时,就会发现不能成功。工厂模式这里就是尽可能的填了java语法上的坑。

需要特别说明的是,我们基本可以认为,设计模式就是为了填语法上的坑。又因为不同的语言语法规定不同,有些设计模式已经融入到语法当中了,所以每个语言上使用的设计模式也不尽相同。

Executors给我们提供了很多种风格的线程池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m6ORpfWB-1673685943983)(F:\typora插图\image-20230114152012852.png)]

而这些线程池,本质上都是通过包装ThreadPoolExecutor来实现出来的,而这个线程池用起来比较麻烦,功能更强大。

再有,运行之后,我们发现,main线程虽然结束了,但是整个进程并没有结束,这是因为线程池中的线程都是前台线程,会阻止进程结束。定时器中的各个任务也是前台线程,所以最后并没有Process finished巴拉巴拉的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWi1DDWy-1673685943984)(F:\typora插图\image-20230114150136720.png)]

如果我们想要它强制停止,可以点击右上角的stop按钮。

另外,这里还涉及到lambda的一个小的语法点——变量捕获。变量i是main线程中的局部变量,run方法是属于Runnbale接口的,并不一定是立刻马上去执行,而线程池中带着任务的线程和主线程基本上是并行的关系,有可能主线程结束了,它这部分的代码块已经销毁了,他们还没结束或者在线程池中还没排到,所以这里再去取一直变化的i是不恰当的,所以java官方给出了这样一个语法,如果拿到的变量是不可变的或者final修饰(jdk1.8以后)就可以。所以需要再次定义个中间变量n.这是为了避免变量生命周期的不同带来的错误。

再有,当线程任务耗时是差不多时,基本上可以认为每个线程负责的任务数是平均的。

ThreadPoolExecutor构造方法解析

关于这个简单的测试代码我们搞明白了,我们下边来看重头戏,ThreadPoolExecutor这个类的构造方法!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MjIpx4l-1673685943984)(F:\typora插图\image-20230114161339311.png)]

下边我们来讨论一个问题

  1. corePoolSize和maximumPool设置多少合适?

    不同的程序特点不同,此时要设置的线程数也是不同的。考虑两个极端情况。

    1. cpu密集型,每个线程要执行的任务都是狂转cpu(进行一些列的算数运算),此时线程池线程数最多不应超过cpu核数。因为此时cpu一直占着,弄太多线程也没坑填它。
    2. io密集型,每个线程干的工作就是等待io(读写硬盘、网卡、等待用户输入……),不吃cpu,此时这样的线程处于阻塞状态,不参与cpu调度……这个时候可以多搞一些线程都无所谓,不受制于cpu核数,线程数设置的可以尽可能的大。

    然而,实际开发中没有程序符合这两种理想模式,真实的程序,往往是一部分吃cpu,一部分等待io。因此我们需要根据具体占比进行设置,一般是通过测试的方法。

线程池的实现

不难确定,线程池={阻塞队列=》存放任务+若干工作线程(类似定时器,也是在构造方法中)+注册任务的submit方法}

class MyThreadPool{
    //不涉及时间,直接BQ
    private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
    //构造方法中创建出工作的线程
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t=new Thread(()->{
               while(true){
                   Runnable runnable= null;
                   try {
                       runnable = queue.take();
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
                   runnable.run();
               }
            });
            t.start();
        }
    }
    //用来注册任务的方法
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
//for test
public class Code31_MyThreadPool {
    public static void main(String[] args) {
        MyThreadPool pool=new MyThreadPool(10);
        for (int i = 0; i < 98; i++) {
            int n=i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行线程池中的任务"+n);
                }
            });
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sch3sBsr-1673685943985)(F:\typora插图\image-20230114163745054.png)]

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

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

相关文章

【互联网大厂机试真题 - 华为】九宫格

题目描述 九宫格是一款广为流传的游戏,起源于河图洛书。游戏规则是:1到9九个数字放在3x3的格子中,要求每行、每列以及两个对角线上的三数之和都等于15. 在金麻名著《射雕英雄传》中黃蓉曾给九宫格的一种解法,口诀:戴九恩一,左三右七,二四有肩,八六为足,五居中央。解法…

【云原生进阶之容器】第四章Operator原理4.3节--Operator模式

1 Operator概述 1.1 诞生背景 Kubernetes实际是期望状态管理器。先在Kubernetes中指定应用程序期望状态(实例数,磁盘空间,镜像等),然后它会尝试把应用维持在这种状态。Kubernetes的控制平面运行在Master节点上,它包含数个controller以调和应用达到期望状态: 检查当前的…

【阶段三】Python机器学习30篇:机器学习项目实战:智能推荐系统的基本原理与计算相似度的常用方法

本篇的思维导图: 智能推荐系统模型 智能推荐系统属于非监督式学习,是机器学习一个非常重要的应用领域,它能带来的经济价值往往是直接且非常可观的。 智能推荐系统的基本原理 智能推荐系统的应用场景 互联网每天都在产生海量信息,用户行为数据呈现爆发式增长…

PyTorch - 常见神经网络

文章目录LeNetAlexNetDropoutAlexNet 网络结构torchvision中的AlexNet的实现ZFNetVGG-NetsVGG 各网络VGG-16 网络结构GoogLeNet代码实现ResNetDenseNetRNNLSTMGRULeNet 1998年&#xff0c;由 LeCun 提出用于手写数字识别任务只有5层结构&#xff1b;目前看来不输入深度学习网络…

智能售货机系统帝可得

智能售货机 概述项目使用springcloudalibaba中提供的短信服务图形验证码生成多端登录/网关统一鉴权对象存储服务代码的自动填充微服务集成emq&#xff0c;发送emq工单业务流 接收工单 拒绝工单 运维工单补货工单使用xxl-job进行任务调度lkd集成xxl-job自动创建维修工单自动…

设计模式_结构型模式 -《代理模式》

设计模式_结构型模式 -《代理模式》 笔记整理自 黑马程序员Java设计模式详解&#xff0c; 23种Java设计模式&#xff08;图解框架源码分析实战&#xff09; 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承…

Docker 架构和安装

Docker 包括三个基本概念: 镜像&#xff08;Image&#xff09;&#xff1a;Docker 镜像&#xff08;Image&#xff09;&#xff0c;就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。 容器&#xff08;Con…

<Linux> Linux项目自动化构建工具—makemakefile的使用

< Linux> Linux 项目自动化构建工具—make/makefile的使用 文章目录< Linux> Linux 项目自动化构建工具—make/makefile的使用一、make/makefile的背景二、如何编写 makefile1.依赖关系和依赖方法2.makefile的使用3.clean的使用4.多文件编译5.伪目标 .PHONY6.三个时…

无约束优化——线性搜索法

无约束优化——线性搜索法&#xff08;line search&#xff09;前言概述构建关于步长的函数Wolfe条件Strong Wolfe条件Zoutendijk条件后记前言 该系列为学习笔记系列&#xff0c;所有内容可以在 Numerical Optimization (2nd Edition) 中找到&#xff0c;该书十分有用经典建议…

组件封装 - Tabs组件

首先我们先来看一下 Element UI 里面的 Tabs 是怎么样的 <template><el-tabs v-model"activeName" class"demo-tabs" tab-click"handleClick"><el-tab-pane label"User" name"first">User</el-tab-pa…

基于matlab的汽车牌照识别程序详细教程

设计一个基于matlab的汽车牌照识别程序&#xff0c;能够实现车牌图像预处理&#xff0c;车牌定位&#xff0c;字符分割&#xff0c;然后通过神经网络对车牌进行字符识别&#xff0c;最终从一幅图像中提取车牌中的字母和数字&#xff0c;给出文本形式的车牌号码。关键词&#xf…

C语言 一个特殊的数组【柔性数组】

文章目录前言柔性数组的特点柔性数组的使用柔性数组的优势写在最后前言 也许你从来就没有听过柔性数组&#xff08;flexible array&#xff09;这个概念&#xff0c;但他是真的存在&#xff1b;柔性数组的概念存在于C99标准当中&#xff0c;C99标准表示&#xff1a;结构体的最后…

Linux-进程概念

目录 进程状态&#xff1a; 操作系统简图 调用进程bin.exe的详细过程 cpu运行队列的结构 R进程和阻塞进程 进程状态&#xff1a;挂起&#xff1a; Linux操作&#xff1a; ​编辑 R运行状态 S休眠状态 T暂停状态&#xff1a; kill kill -18 表示继续 kill -9 杀死进程…

肿瘤内微生物群在癌症转移中的新作用

谷禾健康 癌症是一种复杂的疾病&#xff0c;归因于多因素变化&#xff0c;导致治疗策略困难。 90%的癌症患者死于复发或转移。癌症转移是恶性肿瘤进展的关键步骤&#xff0c;由癌细胞内在特性和外在环境因素决定。 一些微生物组通过诱导癌性上皮细胞和慢性炎症促进癌发生、癌症…

光耦合器:类型及其应用

光耦合器&#xff1a;类型及其应用 介绍 光耦合器&#xff08;也称为光隔离器&#xff09;是一种在两个隔离电路之间传输电信号的半导体器件。光耦合器由两部分组成&#xff1a;发射红外光的LED和检测LED光的光敏器件。这两个部件都装在一个带有连接销的黑匣子中。输入电路接收…

数据可视化笔记记录(二)pandas

笔记内容是根据B站上面的一位老师的视频而记录了&#xff0c;因为我也还在学习&#xff0c;所以个人理解可能会出现错误&#xff0c;若有错误请指出。另外这篇文章会很长 B站视频连接、 numpy,matplotlib的学习记录 pandas 学习记录 SerIes结构&#xff0c;一图胜千言 Seri…

实用工具:FastDoc 文档提取工具

实用工具&#xff1a;FastDoc 文档提取工具 简单、实用的 HTTP API 文档生成工具&#xff0c;支持 Spring MVC/Spring Boot 下的控制器信息提取&#xff0c;类似 Swagger 但稍有不同的机制。 在线演示地址在 https://framework.ajaxjs.com/demo/fast-doc/。 关于研发者这工具…

二三层转发原理

二层转发原理 数据帧 二层即数据链路层的转发是以数据帧的格式进行转发&#xff0c;数据帧的格式如下&#xff1a; 目的地址(Destination Address,DA) &#xff1a;可以是单独的地址,或者是广播或组播MAC地址。 源地址(Source Address,SA) &#xff1a;用来识别发送没备,在S…

听说你想用开发者工具调试我的网站?挺可以的啊。25

本篇博客重点为大家介绍&#xff0c;如何禁止用户在浏览器中查看源码&#xff0c;禁用开发者工具调试等前端需求 案例已更新到 爬虫训练场 文章目录禁用右键&#xff0c;禁用 F12禁用 ctrl U 查看源代码&#xff0c;禁用 ctrl shift i 打开开发者工具实现开发者工具无限 deb…

arcgis使用Python脚本进行批量截图

arcgis使用Python脚本进行批量截图 介绍 最近公司数据部那边有个需求&#xff0c;需要结合矢量数据和影像数据&#xff0c;进行批量截图&#xff0c;并且截图中只能有一个图斑&#xff0c;还要添加上相应的水印。 思路 一开始我是准备使用QGIS直接进行批量出图&#xff0c;…