【JavaEE】线程安全与线程状态

作者主页:paper jie_博客

本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。

本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将MySQL基础知识一网打尽,希望可以帮到读者们哦。

其他专栏:《MySQL》《C语言》《javaSE》《数据结构》等

内容分享:本期将会分享线程安全与线程状态~

目录

 线程状态

线程的所有状态

状态的意义

状态图

查看状态

线程安全

什么是线程安全

经典栗子

原因

导致线程不安全的原因

解法方法

加锁 - synchronized

加锁如何操作

加锁后的代码

注意

内存可见性问题

经典栗子

原因

解决方法

Java中锁的特性

互斥性

可重入性

死锁

死锁问题的常见三种情况

解决方法

线程的通知等待 - wait和notify

wait方法

wait的使用

notify方法

注意

wait和sleep的区别

Java标准库中的线程安全类

线程不安全类

线程安全类


 线程状态

线程的所有状态

1. NEW Thread对象创建好了,但还没有调用start()去系统中创建线程

2. RUNNABLE 调用了start(),线程正在执行或者准备就绪随时准备被调度

3. TERMINATED Thread对象还在,但是系统中的线程已经执行完销毁了.

4. TIMED_WAITING 有时间现在的堵塞状态,到达一定时间会解除堵塞

5. WATING 死等的堵塞状态,需要达到一定的条件才会解除堵塞

6. BLOCKED 由于锁竞争引起的堵塞

状态的意义

状态存在的最大用处就是我们去调试多线程出现的bug时会给我们提供很大的参考意义.比如: 程序卡住了,那可能就是一些相关的线程进入了堵塞状态. start()一个Thread对象只能使用一次这是和NEW密切相关的,只有在NEW状态才能使用start(),使用start()后就进入了另一个状态.

状态图

查看状态

我们可以通过JDK的jconsole来去查看进程里的线程的状态和调用栈的情况.我们可以根据这个来观察线程是不是堵塞了,为什么堵塞,执行到哪行堵塞了.

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while(true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
        while(true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }

    }
}

线程安全

什么是线程安全

一段代码不论是在单线程上还是在多线程上都可以通过执行,不会出现bug,这就是"线程安全".

一段代码再单线程上可以通过,但是在多线程上会出现bug,这就是"线程不安全"或者"线程安全问题"

经典栗子

public class ThreadDemo6 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
           for(int i = 0; i < 50000; i++) {
               count++;
           }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count: " + count);
    }
}

上述这个代码,我们预期的结果是100000,但是我们运行后发现结果不是100000.

原因

这就需要我们站在硬件的角度来看软件了. 我们知道count++在cpu中实际是三条指令:

1. load 去内存中拿到count的值放到cpu中的寄存器中

2. add 将寄存器中的值+1

3.将寄存器中的结果放到内存中

这里如果是一个线程执行结果肯定是不会出错的.但多个线程,那线程的调度也是随机的.这些指令的先后顺序就会产出多种情况.有的是正确的,有的是错的:

这里列出了几种情况,但其实这样的情况有无数种:

这里两个 线程是并发还是并行我们都不知道,反正两个线程都有自己的PCB,有各自的上下文,互不干扰(各自一套寄存器里的值,互不干扰).

通过观察我们发现,知道一个线程的save没执行,另一个线程的load执行了的话,那这个结果就不对,使用正确的情况应该是一个线程的save需要先执行完才能执行另一个线程的load.

导致线程不安全的原因

1. 根本原因: 这是因为操作系统线程是被随机调度的,抢占式执行,这可能就是导致指令的执行顺序不同.

2. 代码结构: 多个线程同时改变一个变量. 这里多个线程改变不同变量,多个线程读一个变量,一个线程改变一变量是都不会造成线程安全问题的.

3. 直接原因: 代码没有具有原子性. 这里count++虽然只有一个代码,但其实它有三个指令.在执行到一半的时候可能会被调度走,其他的线程就有机可乘插队进来.这可能就会导致错误.这里我们可以将count++的多个指令理解为一个整体.需要全部执行完才能执行其他的指令.这样才具有原子性.

4. 内存可见

5. 指令重排序

解法方法

知道了这几个方面的原因我们就可以对症下药了:

第一个问题的随机调度是操作系统控制的,我们没法改变操作系统,我们无从下手.

第二个问题我们在写代码的时候需要注意代码的结构,避免出现多个线程同时改变一个变量的问题,但有的时候是无法避免的.

第三个问题我们可以通过加锁的方法来将需要执行的代码指令打包成一个整体,这样就具有原子性了.

加锁 - synchronized

加锁的目的就是为了将需要的代码打包成一个整体,令他们具有原子性.加锁的特点就是排他性,互斥性. 这里就是一个线程在执行加锁操作时,其他的线程是不能执行这个加锁对象里的代码的.

举个栗子:

这就像有一个厕所,多个滑稽需要上厕所,一个滑稽进去后将门关上其他滑稽进不来看不到就叫做加锁,上完厕所出去就叫做解锁.这时其他的滑稽才可以进来.

加锁如何操作

在加锁前,我们需要引入一个类对象,加锁和解锁都是依托这个类进行的.这个类对象可以是Object类或者是它的任意一个子类.加锁在Java中是一个关键字 - synchronized.它的括号里面放所对象,花括号里面就是加锁,花括号后就是解锁.

这里加锁的核心就是一个线程对一个所对象进行加锁了,其他的线程再对这个锁对象进行加锁就会导致堵塞.一直到前面的线程解锁才会解除堵塞.这里就是所谓的锁竞争造成的堵塞.

且我们需要知道原子性这个说法不够准确. 不是说加锁了这里里面的指令就一定会完成或者都不完成.它中途还是会被调度出去的.只是说第一个加锁的线程可以保证后面对这个所对象加锁的线程指令不会插队到第一个线程指令中间执行.并不是说不能调度出CPU.

加锁后的代码

public class ThreadDemo10 {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                synchronized(object) {
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                synchronized(object) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }
}

我们可以发现这时的结果就是正确的了.

注意

这里需要注意几个点:

一个线程加锁,一个线程不加锁.或者不同的线程加不同的锁这都会造成线程安全问题.

需要加锁的线程的所对象必须是同一个.

这里this和类名.class也是可以作为所对象的.

this:

这里this就是直接指代的test.

class Test {
    public int count = 0;
    public void add() {
        synchronized (this) {
            count++;
        }
    }
}
public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                test.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                test.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + test.count);

    }
}

类名.class:

这里因为java进程中的一个类只有一个类对象,这样不同的线程使用的还是同一个对象,锁竞争还是会存在.

class Test {
    public int count = 0;
    public void add() {
        synchronized (Test.class) {
            count++;
        }
    }
}
public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                test.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                test.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + test.count);

    }
}

内存可见性问题

内存可见性问题和JVM的代码优化息息相关. 一个线程读,一个线程写也可能导致线程安全问题.

经典栗子

while(flag == 0)

这里我们预期的是通过输入1来跳过t1线程的循环,但是我们发现循环并没有跳过,光标还在闪烁. 

原因

这里我们需要知道while(flag == 0) 这句代码其实有两条指令:

1. lode 将内存的中flag读取到CPU的寄存器中

2. 将寄存器中的值与0比较(条件跳转指令)

整体的情况就是t1线程和main线程启动,mian线程需要等待输入,这之间至少需要几秒.而在这几秒的过程中while()会执行几百亿次.

关键有两点:

1. 在这多次lode中,读取内存中的值都是一样的,没发生改变.

2. 读取内存比条件跳转的开销大很多

这就会导致在等待输入这几秒中,大量的循环比较,其中去读取内存,读到的值却没有改变.读取内存的开销又特别大.这就会让JVM怀疑这样的操作有必要嘛.它就有可能会将读取内存指令删除只用寄存器中的值. 这就导致main中改变了flag,但t1线程却没看到,这就是内存不可见.

解决方法

内存可见性是高度依赖JVM的代码优化的具体实现,代码改变一点,结果可能就不一样.为了保证绝对性,Java中就引入了volatile关键字.它的作用就是保证内存可见. 它可以强制代码不进行优化,就是强制读取内存.

Java中锁的特性

互斥性

互斥性就是一个线程获取了这把锁,另一个线程再尝试获取就需要等待,这里就是锁竞争造成的堵塞.这个特性就是用来解决线程安全问题的.

可重入性

可重入性就是一个线程再使用一把锁的前提下,在嵌套二次使用这把锁.在这种情况下不会让线程卡死.

举个栗子:

public class TreadDemo12 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread thread = new Thread(() -> {
            synchronized(object) {
                synchronized (object) {
                    //写代码
                    System.out.println("hello word");
                }
            }
        });
        thread.start();
    }
}

在这个代码中,如果不使用可重入锁,就会卡死,进入死锁状态.在C++中就没有可重入锁,就会陷入死锁状态. 这种死锁情况就是: 在一个线程里使用锁的前提下,嵌套第二次再使用这个锁.就会发生第一次这个锁对象已经加锁了,则第二次使用锁对象就需要等待第一次解锁,但第一个解锁在第二次加锁的后面.这就导致线程卡死,进入了死锁状态.

这就是相当于你将钥匙忘在了被锁的房间里.

在Java中就不会发生. 因为Java中的锁是可重入的. 由于是同一个线程,在第二次加锁的时候,就会直接放行,不会造成堵塞. 而Java中的锁可以重入是因为锁里面有两个重要的属性: 加锁线程 和 计数器

加锁线程这个属性会记录加锁的线程是谁. 计数器初始值为0,加锁就会+1,解锁就会-1.

在第一次加锁时,加锁线程就记录这个线程.计数器+1. 第二次就会判断加锁的线程和持有锁线程是不是同一个,是就直接计数器++,不是就堵塞等待. 出第二次加锁的括号,计数器就-1, 出第一次加锁的括号再-1,当计数器为0时,就是解锁成功.

死锁

加锁是对多线程的线程安全问题的解决方式,但是加锁操作不恰当就是会出现死锁问题.

死锁问题的常见三种情况

1. 一个线程一把锁:

一个线程中在持有这把锁的前提下,第二次使用这把锁,这就会导致死锁.但在Java中不会出现.

2. 两个线程两把锁:

一个线程在持有A锁的情况下去尝试获取B锁,同时另一个线程在持有B锁的情况下尝试获取A锁.这就会导致死锁.

栗子:

public class ThreadDemo13 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized(A) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println("在A加锁后,加锁B");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized(B) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized(A) {
                    System.out.println("在B加锁后,加锁A");
                }
            }

        });
        t1.start();
        t2.start();
    }
}

这就相当于车钥匙放到了被锁的房间里,房间钥匙放到了被锁的车里.

N个线程M把锁:

哲学家就餐问题

这里有5个哲学家,5个筷子. 一个哲学家就餐时需要使用两个身边的筷子. 当1滑稽就餐时需要1和5筷子,这是5滑稽和2滑稽想就餐就需要等待了.这样虽然需要等待但是最后还是可以吃上面.但是这里有一个极端的情况就是:所有的滑稽同时拿起左手边的筷子,这时每个人都只有一个筷子,这时需要拿起第二个筷子时发现没有筷子了就需要等待.但是所有人都在等待,就没人吃到面放下筷子.这就是循环等待.

解决方法

在分析解决方法钱我们需要知道发生死锁有4个必要条件:

1. 互斥性: 一个线程使用锁,另一个就需要等待.

2. 不可抢占: 一个线程在使用锁时,另一个线程不能强行抢占,只能等它自动解锁.

3. 请求保持: 一个线程持有一把锁的前提下,尝试获取另一把锁.

4. 循环等待

发生死锁,这4个条件缺一不可.

知道了发生死锁的条件后,我们就可以对症下药.我们只需要破坏其中一个条件就可以解除死锁.

1和2是锁的基本特性,我们不能改变.3我们需要看情况而定,有的情况可以避免,有的情况不可以避免.

4是最容易改变的.我们可以制定规则: 指定获取锁的顺序,为每个锁编号,先获取小的锁,再获取大的锁.这样就不会发生循环等待了.

改变锁的循环等待有多种方式:

1. 增加一把锁

2. 减少一个线程.

3. 引入计数器,限制最多多少个线程同时获取锁

4.制定加锁顺序规则(最常用)

5. 银行家算法

线程的通知等待 - wait和notify

这里是通过引入wait与notify来在应用层面来改变线程执行的先后顺序.

操作系统中线程在内核中的调度是抢占式,随机调度的,这是不可改变的.这里我们就是在应用代码层面来让线程主动放弃CPU的调度,从而影响到线程执行的先后顺序. 也就是让执行条件没达到的线程先放弃CPU的竞争,让其他线程先执行,等到条件达到时再参与竞争.

这里举个栗子:

多个滑稽老哥去ATM上执行一些操作

没有wait和notify时: 1号老哥进去取钱,发现没有钱了,那他就会出来与其他滑稽老哥进行竞争进入ATM的机会,1号老哥可能又会竞争到,再进去取钱发现没有钱,又出来和它们竞争,这样可能会多次1号滑稽进去但又没有进行到有用的操作.

转换成代码:

while(true) {
    synchronized(....) {
        if(ATM有钱) {
            //取钱操作
        }else {
            //什么也不做
        }

    }

}

有wait和notify时: 1号老哥进去取钱,发现没有钱了.那他会出来先不参与和它们老哥竞争进去的机会,而是等待其他老哥把钱存进去后再参与竞争,这样就减少了无效操作.

代码:

while(true) {
    synchronized(....) {
        if(ATM有钱) {
            //取钱操作
        }else {
            wait();
        }

    }

}

画图理解:

对于上面第一种情况还是比较容易发生的, 1号滑稽拿到锁,处于RUNNABLE状态,其他线程处于WAITING 状态. 当1号滑稽解锁后再次竞争时,其他滑稽需要系统先唤醒在竞争,而1号滑稽不用唤醒可以直接竞争. 

wait方法

对于wait方法,它的内部会做三件事情:

1. 解锁.

2. 进入堵塞状态.

3. 等到其他线程执行到notify方法时,解除堵塞,加入到锁竞争中.

wait的使用

1. wait需要在synchronized内部使用,不然会抛出异常.

2. wait的对象需要和synchronized的对象时一致的.

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized(object) {
            System.out.println("执行wait前");
            object.wait();
            System.out.println("执行wait后");
        }
    }
}

 

这时我们可以打开jcons观察:

 

通过这里我们观察到main线程在wait这里进入了堵塞状态.需要解除堵塞我们就需要使用notify方法. 

notify方法

notify方法就是用来解除wait造成的堵.

notify是不用在synchronized中使用的.比如在操作系统中也有wait和notify,notify是不用先加锁再使用的.但在Java中notify需要在synchronized中使用,不然会报错.

public class TreadDemo2 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                System.out.println("wait方法前");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait方法后");
            }

        });

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (object) {
                System.out.println("notify方法前");
                object.notify();
                System.out.println("notify方法后");
            }
        });
        t1.start();
        t2.start();

    }
}

 

代码执行过程:

1) t1和t2启动,并发执行.

2) t1拿到锁会进入阻塞等待

3) t2会先休眠一秒,这会让t1先拿到锁

4) 等到t2休眠结束, t1已经进入wait,将锁解除了,这时t2就可以拿到锁

5) 等到t2执行到notify时,t1的堵塞等待结束重新进入到锁竞争中.

6) 虽然t1等待结束但是t2还没释放锁,再等待t2释放锁后,t1才能拿到锁继续执行. 

注意

1. wait方法有三个可以使用:

第一个是死等,这个对代码非常的不利,一但忘记使用notify,线程就会卡死.我们一般常用的是第二种,有时间限制的等待,超过这个时间就不等了.第三个是微秒级的等待.

2. wait和notify的所对象得是一致的,不然会导致wait的堵塞等待解除不了.

3. notifyAll方法可以解除在多个线程使用同一个锁的wait的堵塞,但是这样不利于代码控制,我们还是比较推荐使用notify.

wait和sleep的区别

相同点:

1. sleep是指定时间的,wait也有指定时间的版本.

2. sleep和wait都可以提前唤醒. sleep是interrupt方法,wait是notify方法.

不同点:

1. sleep是在知道要休眠多久的情况下使用,wait是在不知道要等待多久的情况下使用

2. wait需要在synchronized中使用,sleep不需要.

3. wait是Object方法. sleep是Thread的静态方法.

Java标准库中的线程安全类

线程不安全类

ArrayList

LinkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder

线程安全类

Vector

HashTable

ConcurrentHashMap

StringBuffer

String

这里前四个为线程安全主要是加了synchronizednized关键字不过这几个类jdk都快弃用了.

String为线程安全是因为它不可改变,就不涉及到线程安全问题了.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Rational Arithmetic

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 ☁️有理数运算 实现对两个有理数的…

机器人与3D视觉 Robotics Toolbox Python 一 安装 Robotics Toolbox Python

一 安装python 库 前置条件需要 Python > 3.6&#xff0c;使用pip 安装 pip install roboticstoolbox-python测试安装是否成功 import roboticstoolbox as rtb print(rtb.__version__)输出结果 二 Robotics Toolbox Python样例程序 加载机器人模型 加载由URDF文件定义…

Oracle SQL优化

1、书写顺序和执行顺序 在Oracle SQL中&#xff0c;查询的书写顺序和执行顺序是不同的。 1.1SQL书写顺序如下&#xff1a; SELECTFROMWHEREGROUP BYHAVINGORDER BY 1.2 SQL执行顺序 FROM&#xff1a;数据源被确定&#xff0c;表连接操作也在此步骤完成。 WHERE&#xff1a;对…

样品实验Epiclon萘系环氧树脂HP4032D说明书

样品实验Epiclon萘系环氧树脂HP4032D说明书 50克/袋

4.livox hap(大疆激光雷达)环境搭建

本文是在rk3588设备的ubuntu20.04的系统环境下搭建livox hap的。大概的步骤分为&#xff1a; 一、gcc、g、cmake 的安装 二、ros安装&#xff08;上一章已介绍&#xff09; 三、Livox SDK2的编译 四、livox_ros_driver2的编译 五、hap的点云视频录制、点播点云视频bag、ba…

Docker Swarm总结+CI/CD Devops、gitlab、sonarqube以及harbor的安装集成配置(3/5)

博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码下载地址&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb;…

网站优化SEO文章采集组合方法

为了在激烈的网络竞争中脱颖而出&#xff0c;SEO专业人士不断寻求创新的方法和技术。其中&#xff0c;SEO文章采集后重组是一项备受关注的技术&#xff0c;通过巧妙地整合和重新组织已有的信息&#xff0c;以提升网站在搜索引擎中的排名和曝光度。 SEO文章采集是这一技术的第一…

【MySQL】事务(事务四大特性+四种隔离级别+MVCC)

事务 前言正式开始事务的四大特性为什么会出现事务事务的版本支持事务提交方式事务常见操作方式启动事务回滚演示提交事务事务的异常autocommit 事务的隔离性隔离级别查看隔离级别修改隔离级别验证四种隔离级别读未提交(read uncommitted) —— 缩写为RU读提交(read committed)…

Jmeter接口自动化测试(提取CSV文件遍历数据)

CSV文件是我们参数化时一种最常用的存储数据文件格式&#xff0c;Jmeter也为我们提供了提取CSV文件数据的工具 首先在创建CSV文件之前&#xff0c;我们要保证我们的CSV文件编码格式为ANSI或者UTF-8,我们可以用记事本另存为&#xff0c;将编码改成ANSI或者UTF-8 接着打开Jmeter…

c MJPG(1)

.读取量化表&#xff0c;全局参数&#xff0c;霍夫曼表&#xff0c;恢复表编码&#xff0c;现在只是实现思路。 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sy…

CSS伪类伪元素?:hover,::before,::after使用(举例)

文章目录 什么是CSS伪类&#xff1f;什么是伪元素&#xff1f;怎么用伪元素&#xff1f;可以做些什么&#xff1f;::before&#xff0c;在标签选择器之前添加内容&#xff0c;::after正好与之相反::before&#xff0c;在类选择器之前添加内容&#xff08;:制作一个悬浮提示窗 参…

CAN总线

1、CAN总线简介 CAN总线协议&#xff08;Controller Area Network&#xff09;&#xff0c;控制器局域网总线&#xff0c;是德国BOSCH&#xff08;博世&#xff09;公司研发的一种串行通讯协议总线&#xff0c;它可以使用双绞线来传输信号&#xff0c;是世界上应用最广泛的现场…

Locust单机多核压测,以及主从节点的数据通信处理!

一、背景 这还是2个月前做的一次接口性能测试&#xff0c;关于locust脚本的单机多核运行&#xff0c;以及主从节点之间的数据通信。 先简单交代下背景&#xff0c;在APP上线之前&#xff0c;需要对登录接口进行性能测试。经过评估&#xff0c;我还是优先选择了locust来进行脚…

实现校园网开机自启动部署

❤️博客主页&#xff1a; iknow181&#x1f525;系列专栏&#xff1a; Python、JavaSE、JavaWeb、CCNP&#x1f389;欢迎大家点赞&#x1f44d;收藏⭐评论✍ 目录 一.准备工作 1、IDE安装 2、安装Selenium 1.介绍 2.下载 3、安装pywifi 1.介绍 2.下载 4、下载浏览器驱…

python 制作3d立体隐藏图

生成文件的3d图&#xff0c;例子&#xff1a; 文字&#xff1a; 隐藏图&#xff1a; 使用建议&#xff1a; &#xff11;、建议不用中文&#xff0c;因为中文太复杂&#xff0c;生成立体图效果不好。 &#xff12;、需要指定FONT_PATH&#xff0c;为一个ttf文件&#xff0c;…

NoSQL 数据建模错误会降低性能

数据建模错误是破坏性能的最简单方法之一。当您使用 NoSQL 时&#xff0c;特别容易搞砸&#xff0c;&#xff08;讽刺的是&#xff09;NoSQL 往往用于对性能最敏感的工作负载。NoSQL 数据建模最初可能看起来非常简单&#xff1a;只需对数据进行建模以适应应用程序的访问模式。但…

BatchOutput PDF for Mac(PDF 批量处理软件)

BatchOutput PDF是一款适用于 Mac 的 PDF 批量处理软件。它可以帮助用户将多个 PDF 文件进行异步处理&#xff0c;提高工作效率。 BatchOutput PDF 可以自动化执行许多任务&#xff0c;包括 PDF 文件的打印、转换、分割、压缩、加密、重命名等&#xff0c;而且它还可以将自定义…

基于python 医院预约挂号系统-计算机毕业设计源码24802

摘 要 随着互联网时代的到来&#xff0c;同时计算机网络技术高速发展&#xff0c;网络管理运用也变得越来越广泛。因此&#xff0c;建立一个基于django 医院预约挂号系统 &#xff0c;会使&#xff1b;医院预约挂号系统的管理工作系统化、规范化&#xff0c;也会提高平台形象&a…

汽美汽修店服务预约会员管理系统小程序效果如何

很多家庭中都有一辆或多辆汽车&#xff0c;无论燃油车还是新能源电车等&#xff0c;其市场中的数量及人均拥有量都很大&#xff0c;除了汽车销售业外&#xff0c;汽车美容修理店则生意也很多&#xff0c;可以看到城市中的不少街道中都有大大小小的汽车服务门店。 而在市场中&a…

关于制造业数字化转型,大咖们有这些建议……

随着数字经济与实体经济深度融合&#xff0c;越来越多传统制造业企业走上数字化转型之路&#xff0c;实现生产、研发、管理、组织、商业模式等多层面的创新。为发挥鼎捷41年在制造业数字化转型的行业积淀&#xff0c;赋能更多制造业企业突破发展瓶颈&#xff0c;鼎捷携手来自工…