关于Java多线程的那些事

news2024/11/19 3:24:10

多线程

  • 多线程
    • 1. 关于多线程的理解
      • 1.1 进程和线程
      • 1.2 并行和并发
      • 1.3 线程调度
    • 2. 创建多线程的方式
      • 创建线程有哪几种方式?
        • 2.1 通过继承`Thread`类来创建并启动线程的步骤如下:
        • 2.2 通过实现`Runnable`接口来创建并启动线程的步骤如下:
        • 2.3 通过实现`Callable`接口来创建并启动线程的步骤如下:
    • 3 run()和start()有什么区别?
    • 4 线程是否可以重复启动,会有什么后果?
    • 5. 说一说sleep()和wait()的区别
    • 6. 说一说notify()、notifyAll()的区别
    • 7. 如何实现子线程先执行,主线程再执行?
    • 8. Thread 类的方法
      • 8.1 构造方法
      • 8.2 Thread 类的常用方法
    • 9. 线程的状态

多线程

1. 关于多线程的理解

1.1 进程和线程

每个进程有多个线程

  • 进程是系统运行程序的基本单元
  • 每个进程的内部数据和状态是完全独立的
  • 每一个应用程序运行的时候会产生一个进程
  • 线程:就是一个进程中的执行单元。一个进程可以启动多个线程。cpu 调度和分配的最小单元。
  • 线程必须在某个进程内执行。

所以多线程就是:在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程。

  • 进程与线程的区别
    • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少
      有一个线程。
    • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

1.2 并行和并发

  1. 并发(Concurrency)

    • 并发是指多个任务在相同的时间段内交替执行,每个任务可能只执行一小部分,然后切换到另一个任务。
    • 并发并不一定意味着多个任务同时在不同的处理器核心上执行。它可以在单个处理器上通过时间片轮转来实现,也可以在多个处理器核心上并行执行。
    • 通常,多线程程序是并发的,因为它们可以在单个处理器上通过线程切换实现并发执行。

    示例:多个线程在单个处理器上轮流执行,共享CPU时间。

  2. 并行(Parallelism)

    • 并行是指多个任务在相同的时间点上同时执行,每个任务都在不同的处理器核心上运行。
    • 并行通常需要多核处理器或多个计算资源,并且可以实现更高的性能,因为多个任务可以在不互相干扰的情况下并行执行。
    • 并行通常用于解决需要高性能的问题,如大规模数据处理或计算密集型任务。

    示例:多个线程在不同的处理器核心上同时执行,各自独立工作。

总结:

  • 并发是任务在时间上交替执行,可能在同一处理器核心上通过线程切换实现。
  • 并行是任务在同一时刻同时执行,通常需要多个处理器核心或多个计算资源来实现。
  • 并发可以提高系统的响应性和资源利用率,但并不一定提高吞吐量。
  • 并行通常用于提高性能,特别是在多核处理器上,可以实现更高的吞吐量

1.3 线程调度

每个程序至少自动拥有一个线程,称为主线程

  1. 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  2. 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java 使用的为抢占式调度。

2. 创建多线程的方式

创建线程有哪几种方式?

创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口

2.1 通过继承Thread类来创建并启动线程的步骤如下:
  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象

  3. 调用线程对象的start()方法来启动该线程。

/**
 * 实现多线程的第一种方式
 *      第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法
 */
public class ThreadDemo01 {
    public static void main(String[] args) {
        System.out.println("main start!");
        MyThread myThread = new MyThread();
        myThread.test();        // 不会启动线程
        // 只有调用s'tart
        myThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程");
        }
    }
}
// 自定义线程类
class MyThread extends Thread{
    @Override
    public void run(){
        Thread thread = Thread.currentThread();     // 获取当前线程
        thread.setName("自定义线程");    // 设置当前线程的名字
        String name = thread.getName();     // 获取当前线程的名字
        for (int i = 0; i < 10; i++) {
            System.out.println(name + i);
        }
    }
    public void test(){
        System.out.println("test");
    }
}
2.2 通过实现Runnable接口来创建并启动线程的步骤如下:
  1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
  2. 创建Runnable实现类的实例,并将其作为Threadtarget来创建Thread对象,Thread对象为线程对象。
  3. 调用线程对象的start()方法来启动该线程。
/**
 * 实现多线程的第二种方式
 *      第二种方式:编写一个类,实现java.lang.Runnable接口
 */
public class ThreadDemo02 {
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable myRunnable = new MyRunnable();
        // 创建线程
        Thread tr = new Thread(myRunnable,"线程1");
        // 启动线程
        tr.start();
        System.out.println("主线程");

        // 除了上面使用的自定义类实现Runnable接口之外,还可以使用匿名内部类、lambda表达式来实现Runnable接口
        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名内部类实现Runnable接口。。");
            }
        };
        new Thread(runnable1).start();
        Runnable runnable2 = ()->{
            String name = Thread.currentThread().getName();
            System.out.println(name + ":使用匿名内部类实现Runnable接口。。");
        };
        new Thread(runnable2).start();
    }
}
// 自定义类实现 Runnable 接口
class MyRunnable implements Runnable{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "实现线程的第二种方式.");
    }
}
2.3 通过实现Callable接口来创建并启动线程的步骤如下:
  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
/**
 * 创建线程的第三种方式:
 *      实现Callable接口
 */
public class ThreadDemo03 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 使用匿名内部类实现Callable接口
        Callable<Integer> callable = new Callable<>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i < 10; i++) {
                    sum += i;
                }
                // 返回一个结果
                return sum;
            }
        };
        // 创建一个FutureTask 对象,来接受异步计算的结果
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 创建线程
        Thread thread = new Thread(futureTask);
        thread.start();
        // futureTask.get() 是在等待执行完毕获取结果,所以是在线程启动之后,才获取。
        System.out.println(futureTask.get());
    }
}

采用继承Thread类的方式、实现Runnable、Callable接口的方式创建多线程的优缺点:

  • 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。而如果是已经继承Thread 类,则不能再继承其它父类了。(所以实现接口可以避免 java 中的单继承的局限性)
  • 在实现Runnable、Callable接口的这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同的程序代码的线程去共享同一个资源的情况,实现解耦操作,较好地体现了面向对象的思想。
  • 线程池只能放入实现 Runnable 或 Callable 类线程,不能直接放入继承 Thread 的类
  • 劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
  • 采用继承Thread类的方式创建多线程的优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
    鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。

3 run()和start()有什么区别?

run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程。调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。

4 线程是否可以重复启动,会有什么后果?

只能对处于新建状态的线程调用start()方法,否则将引发IllegalThreadStateException异常。

扩展阅读

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

5. 说一说sleep()和wait()的区别

  1. sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
  2. sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
  3. sleep()不会释放锁,而wait()会释放锁,并需要通过notify() / notifyAll()重新获取锁。

6. 说一说notify()、notifyAll()的区别

  • notify()

    用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

  • notifyAll()

    用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

7. 如何实现子线程先执行,主线程再执行?

启动子线程后,立即调用该线程的join()方法,则主线程必须等待子线程执行完成后再执行。

扩展阅读

Thread类提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

8. Thread 类的方法

8.1 构造方法

Thread中常用的构造方法有:

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字

8.2 Thread 类的常用方法

  1. public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

    Thread.currentThread(); // 执行这句代码的线程
    
  2. public void run(): 表示线程的任务。

    • 所有Thread的子类应该覆盖(重写)此方法
  3. public synchronized void start(): 线程开始执行;

    • 多次调用同一个线程的此方法是不合法的。
  4. public static native void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 线程不会丢失任何CPU的所有权。

    //线程休眠5S
    Thread.sleep(5000);	
    
  5. public static void sleep(long millis, int nanos) :使当前正在执行的线程以指定的毫秒数加上指定的纳秒数暂停(暂时停止执行)

  6. public final String getName():返回此线程的名称

    Thread.currentThread().getName();//获取当前线程名称
    
  7. public final synchronized void setName(String name):将此线程的名称更改为等于参数name

  8. public final int getPriority():返回此线程的优先级

  9. public final void setPriority(int newPriority):更改此线程的优先级,1~10之间

  10. public final native boolean isAlive():测试这个线程是否活着。 如果一个线程已经启动并且尚未死亡,那么线程是活着的

public static void main(String[] args) {
   Runnable runnable = new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    };
    Thread thread = new Thread(runnable);
    // isAlive():测试这个线程是否活着
    System.out.println(thread.isAlive());       // false
    thread.start();
    // 线程启动后尚未结束时返回 true
    System.out.println(thread.isAlive());       // true
}
  1. public final void join():等待线程死亡,等同于join(0)
  2. public final synchronized void join(long millis):等待这个线程死亡的时间最多为millis毫秒。 0的超时意味着永远等待
    • join可以理解为当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。(让父线程等待子线程结束之后才能继续运行)
 public static void main(String[] args){
     Thread th= new Thread(runnable);
     th.start();
     try {
         // 在main执行, main线程等待 th 执行完毕后再执行
         th.join();
         // main线程等待 th 执行 1s 后再执行
//            th.join(1000);
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
     }
     System.out.println("main end");
 }
  1. public void interrupt():中断这个线程,线程的中断状态标记为 true

  2. public static boolean interrupted():测试当前线程是否中断。 该方法可以清除线程的中断状态(设置中断状态为False) 。 换句话说,如果这个方法被连续调用两次,那么第二个调用将返回false(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)。

  3. public boolean isInterrupted():测试这个线程是否被中断。 线程的中断状态不受此方法的影响。不清除中断状态。

public static void main(String[] args) {
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            for (int i = 0; i < 10; i++) {
                if (i % 3 == 0) {
                    // 中断线程, 标记线程中断状态
                    thread.interrupt();
                }
                // 测试当前线程是否中断
                System.out.println(Thread.interrupted());//true false false true false false true false false true
            }

            int count = 0;
            while (!thread.isInterrupted()) {
                if (count == 5) {
                    // 当count == 5 时,线程就会被中断
                    thread.interrupt(); // 我想停
                }
                System.out.println("执行" + count ++);    // 执行0 执行1 执行2 执行3 执行4 执行5
            }
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
}
    • public static native void yield():导致当前执行线程处于让步状态。如果有其他可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。并不一定会让出去。
  1. public State getState():返回此线程的状态,返回值是Thread的一个内部类,枚举类State。线程状态可以是:

    • NEW: 尚未启动的线程处于此状态。
    • RUNNABLE: 在Java虚拟机中执行的线程(可以运行的线程)处于此状态。
    • BLOCKED: 被阻塞等待监视器锁定的线程处于此状态。
    • WAITING: 正在等待另一个线程执行特定动作的线程处于此状态。
    • TIMED_WAITING: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。 (sleep(1000),join(1000))
    • TERMINATED: 已退出的线程处于此状态。

9. 线程的状态

线程状态具体含义
NEW一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。
RUNNABLE当我们调用线程对象的start()方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED一个完全运行完成的线程的状态。也称之为终止状态、结束状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有六种状态。在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态。

线程从 Runnable (可运行)状态与非运行状态之间的转换问题:
在这里插入图片描述
多线程安全问题
(重点)关于多线程并发环境下,数据的安全问题
线程安全问题的出现主要是因为多线程并发访问共享资源时可能导致的竞态条件(Race Condition)和并发访问冲突。
具体内容点击这里:Java线程安全——关于多线程并发环境下,数据的安全问题

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

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

相关文章

充电江湖暗战,充电运营商为什么忌惮平台崛起?

配图来自Canva可画 话说&#xff1a;“天下大势&#xff0c;分久必合&#xff0c;合久必分”&#xff0c;日前充电运营商与第三方充电平台再次倾情上演分手戏码。 8月中旬&#xff0c;新能源汽车充电运营服务商特来电、星星充电与云快充&#xff0c;毫无预兆的下架第三方充电…

Qt创建线程(线程池)

1.线程池可以创建线程统一的管理线程&#xff08;统一创建、释放线程&#xff09; 2.使用线程池方法实现点击开始按钮生成10000个随机数&#xff0c;然后分别使用冒泡排序和快速排序排序这10000个随机数&#xff0c;最后在窗口显示排序后的数字&#xff1a; mainwindow.h文件…

CATIA | 如何创建曲面点云数据并保存

问题描述 CATIA中创建的曲面模型&#xff0c;如何转换成可读的点云数据&#xff08;点云坐标数据&#xff09;输出保存&#xff1b; 操作流程&#xff08;示例&#xff09; 1、打开软件 2、创建三维曲面模型 &#xff08;具体步骤省略&#xff09; 3、文件另存为“.stl” …

wallet connect简单使用

wallet connect简单使用 准备工作安装配置打包测试 准备工作 新建一个文件夹xxx 右键在终端中打开 npm init -y在文件夹中新建src目录 在src目录中新建index.html和index.js文件 目录大概就这样我这是打包过的 安装 按照官方文档先安装 官方页面长这样 我们需要用到的是we…

基于Dijkstra、A*和动态规划的移动机器人路径规划(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑…

【软件设计师-中级——刷题记录3(纯干货)】

目录 数据交换模式知识产权与标准化之侵权判断OSI专业模型&#xff1a;7-克制自己的同理心8-不要为不值得的人和事浪费时间9-做个长期主义者 每日一言&#xff1a;持续更新中... 个人昵称&#xff1a;lxw-pro 个人主页&#xff1a;欢迎关注 我的主页 个人感悟&#xff1a; “失…

20 mysql const 查询

前言 这里主要是 探究一下 explain $sql 中各个 type 诸如 const, ref, range, index, all 的查询的影响, 以及一个初步的效率的判断 这里会调试源码来看一下 各个类型的查询 需要 lookUp 的记录 以及 相关的差异 测试表结构信息如下 CREATE TABLE tz_test (id int(1…

基于SpringBoot的蜗牛兼职网站

目录 前言 一、技术栈 二、系统功能介绍 管理员功能模块 用户功能模块 前台首页功能模块 企业功能模块 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#x…

接口测试之什么是接口文档?

一、为什么要有接口文档&#xff1f; 没有接口文档的接口测试都是在抓瞎~前面的接口测试重点讲了协议&#xff0c;也讲了fiddler模拟接口请求&#xff0c;估计大部分还是不太懂怎么下手测试。这里小编专门拿出接口文档来做接口测试参考&#xff08;估计很多测试小伙伴没见过接口…

GDB的TUI模式(文本界面)

2023年9月22日&#xff0c;周五晚上 今晚在看GDB的官方文档时&#xff0c;发现GDB居然有文本界面模式 TUI (Debugging with GDB) (sourceware.org) GDB开启TUI的条件 GDB的文本界面的开启条件是&#xff1a;操作系统有适当版本的curses库 The TUI mode is supported only on…

期刊分类一览

分区情况 jcr分区 中科院分区 EI 理工科 一般是SCI的都是EI 国内的分区

基于eBPF的安卓逆向辅助工具——stackplz

前言 stackplz是一款基于eBPF技术实现的追踪工具&#xff0c;目的是辅助安卓native逆向&#xff0c;仅支持64位进程&#xff0c;主要功能如下&#xff1a; hardware breakpoint 基于pref_event实现的硬件断点功能&#xff0c;在断点处可读取寄存器信息&#xff0c;不会被用户…

【计算机网络】子网掩码、子网划分

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 更多计算机网络知识专栏&#xff1a;计算机网络&#x1f525; 给大家跳段…

为什么曲面函数的偏导数可以表示其曲面的法向量?

为什么曲面函数的偏导数可以表示其曲面的法向量&#xff1f; 引用资料&#xff1a; 1.知乎shinbade&#xff1a;曲面的三个偏导数为什么能表示法向量&#xff1f; 2.Geogebra羅驥韡 (Pegasus Roe)&#xff1a;偏導數、切平面、梯度 曲面 F ( x , y , z ) 0 F(x,y,z)0 F(x,y,…

Direct3D模板缓存

模板缓存是一个用于获得某种特效的离屏缓存&#xff0c;模板缓存的分辨率与后台缓存和深度缓存的分辨率完全相同&#xff0c;所以像素也是一一对应的&#xff0c;模板缓存允许我们动态的&#xff0c;有针对性的决定是否将某个像素写入后台缓存中。 例如实现镜面效果时&#xf…

3、Elasticsearch功能使用

第4章 功能使用 4.1 Java API 操作 随着 Elasticsearch 8.x 新版本的到来&#xff0c;Type 的概念被废除&#xff0c;为了适应这种数据结构的改 变&#xff0c;Elasticsearch 官方从 7.15 版本开始建议使用新的 Elasticsearch Java Client。 4.1.1 增加依赖关系 <propertie…

工作【当van-tab不满足固定在顶部】

背景 需要H5实现一下滑动列表&#xff0c;顶部tab栏可以切换&#xff0c;当向下滑动列表的时候tab栏固定到顶部。果断的看了一下官方文档&#xff1a; 就是这个&#xff0c;我一看还有扩展属性&#xff0c;非常友好。向下滑动查看文档 使用sticky实现的。众所周知&#xff0…

Python 计算三角形面积

"""计算三角形面积介绍&#xff1a;已知三角形边长分别为x、y、z&#xff0c;可以计算三角形半周长q&#xff0c;然后根据海伦公式计算三角形面积S三角形半周长&#xff1a;q (x y z) / 2三角形面积&#xff1a;S (q * (q-x) * (q-y) * (q-z)) ** 0.5知识点…

独辟蹊径”之动态切换进程代理IP

前言 项目中遇到这样一个需求&#xff0c;需要动态切换指定进程Sockets5代理IP&#xff0c;目前了解到可通过编写驱动拦截或者劫持LSP实现&#xff0c;LSP劫持不太稳定&#xff0c;驱动无疑是相对较好的解决方案&#xff0c;奈何水平不足便有了这"蹊径"。 初步尝试…

Mybatis SQL构建器

上一篇我们介绍了在Mybatis映射器中使用SelectProvider、InsertProvider、UpdateProvider、DeleteProvider进行对数据的增删改查操作&#xff1b;本篇我们介绍如何使用SQL构建器在Provider中优雅的构建SQL语句。 如果您对在Mybatis映射器中使用SelectProvider、InsertProvider…