多线程 _ 基础篇 _ 线程安全问题【JavaEE初阶】

news2025/1/18 11:50:39

一、线程安全概述

1.1 什么是线程安全问题

线程安全问题 出现的 "罪魁祸首",正是 调度器的 随机调度 / 抢占式执行 这个过程 

在随机调度之下,多线程程序执行的时候, 有无数种可能的排列方式 

在这些排列顺序中,有的排列方式 逻辑是正确的,但是有的排列方式 可能会引出 bug 

对于多线程并发时,会使程序出现 bug 的代码 称作线程不安全的代码,这就是线程安全问题 

1.2 存在线程安全问题的实例

创建两个线程,让这两个线程 同时并发 对一个变量,自增 5w 次,最终预期能够一共自增 10w 次 

package thread; 
class Counter { 
    public int count;  //用来保存计数的变量
    public void increase() {
        count++;
    }
}

public class Demo9 {
    public static void main(String[] args) {
        //这个实例用来进行累加
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + counter.count);
    }
}

二、线程安全问题及其解决办法

2.1 案例分析

按理来说,上述实例 运行的结果 count 应该等于 10w吖

可是 连续运行多次,就会发现 每一次运行的结果都不一样,但都是小于 10w,这是为什么呢?

这个就是 线程不安全的问题 

其原因主要是:随机调度的顺序不一样,就导致程序运行的结果不一样 

上述的 bug 是怎么形成的呢?

这个得需要站在硬件的角度来理解: 

像 count++ 这一行代码,其实对应的是 三个机器指令:

  1. 首先 需要从内存中读取数据到 CPU,这个指令称为 load指令
  2. 其次 在 CPU 寄存器中,完成加法运算,这个指令称为 add指令
  3. 最后 把寄存器的指令 写回到内存中,这个指令称为 save指令

这里的 load 是把内存中的数据读到 寄存器里,add是在寄存器里面进行加法操作,save是把寄存器里面的值放回到内存 

情况一:

情况二: 

由于是多线程,所以有无数种情况 ~

总之,在无数中的排列顺序情况下,只有 "先执行完第一个线程,再执行完第二个线程" 以及 "先执行完第二个线程",再执行完第一个线程" 的这两种情况,是没有问题的 

剩下的情况,全部都是和正确结果不匹配  


 操作系统的随机调度,其实不是 "真随机",而是 操作系统内核的调度器调度线程,其内部是有一套 逻辑 / 算法,来支持这一调度过程。即 每种出现的排列情况下不是均等的,所以不可以通过排列组合的情况下算出每种情况 出现的概率。

2.2 造成线程不安全的原因

一)操作系统的 随机调度 / 抢占式执行 

这个是 万恶之源、罪魁祸首!! 

这个是 操作系统内核 实现的时候,就是这样设计的,因此 我们改不了 ,对此 我们是无能为力的 

二) 多个线程 修改 同一个变量 

如果只是一个线程修改变量,没有线程安全问题;

如果是多个线程读同一个变量,也没有线程安全问题;

如果是多个线程修改不同的变量,还是没有线程安全问题;

但是,多个线程修改同一个变量,那就有了线程安全问题了;

所以,在写代码的时候,我们可以针对这个要点进行控制(可以通过调整程序的设计,来去规避 多个线程修改同一个变量) 

但是,此时的 "规避方法" 是有适用范围的,不是所有的场景都可以规避掉(这个得要看具体的场景) 

三)有些修改操作,不是 原子的修改,更容易触发 线程安全问题~

 不可拆分的最小单位 就叫做原子 

如:赋值操作来修改(=,只对应一条机器指令),就是视为原子的~

像之前通过 ++操作 来修改(对应三条机器指令),就不是原子的~

 四)内存可见性 引起的线程安全问题~

内存可见性所存在的场景是:一个线程读、一个线程写的场景

package thread; 
import java.util.Scanner; 
public class Demo16 {
    //写一个 内部类,此时这个内部类 就处在 Demo16 的内部,就可以解决 前面已经写过 Counter 的问题
    static class Counter {
        public int flg = 0;
    }
 
    public static void main(String[] args) {
        Counter counter = new Counter();
        
        Thread t1 = new Thread(() -> {
            while (counter.flg == 0) {
                //执行循环,但是此处循环 啥都不做
            }
            System.out.println("t1循环结束");
        });
        t1.start();
        
        Thread t2 = new Thread(() -> {
            //让用户输入一个数字,赋值给 flg
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flg = scanner.nextInt();
        });
        t2.start();
    }
}

预期效果:t2线程 输入一个非零的整数后,此时 t1线程 循环结束,随之进程结束~

实际结果:

 分析:

t1线程的工作:

  1. load 读取内存的数据到 CPU 的寄存器
  2. test 检测 CPU寄存器的值是否和预期的一样
  3. 反复进行,频繁进行 

由于 读内存比读 CPU寄存器 慢上几千倍、上万倍,意味着 t1线程 的主要操作就在 load上,但是 每一次读取到的值又没有啥变化,于是 直接进行了优化,就相当于 只从内存中只读取一次数据,后续就直接从寄存器里面 进行反复 test 就好了 

编译器看到这个线程(t1线程)对变量 flg 也没有做修改,于是就进行了优化操作 

但是,这里出现了一个特殊情况,有其他的线程(t2线程)对这个变量做出了修改 

但是,t1线程 仍然是 采用之前的数据来读寄存器,此时 读到的数据和内存的数据是不一致的,这种情况就叫做 内存可见性问题(即 内存改了,但是线程没有看见;或者说,没有及时读取到内存中的最新数据) 


 内存可见性的解决办法 —— volatile关键字

由于 编译器优化,是属于编译器自带的功能,正常来说,程序员并不好干预 

但是 因为上述的场景,编译器知道自己可能会出现误判,因此就给程序猿提供了一个 干预优化的途径 —— volatile关键字~

这个关键字是写到要修改的变量上,要保证哪个变量的内存可见性 就往哪个变量里面加  

 

 注意 :volatile 可以修饰变量的位置,也是在 public 左右 

 此时,运行结果:

 volatile 操作 相当于是 显示得禁止了编译器进行上述优化,相当于是给这个对应的变量加上了 "内存屏障"(特殊的二进制指令),JVM 再读取这个变量的时候,因为内存屏障的存在,就知道每次都要重新读取这个变量的内容,而不是草率的进行优化了 

虽然频繁的读取内存,使得速度变慢了,但是数据却是算的对了 

关于编译器的优化:

  • 编译器的优化是根据代码的实际情况来运行的,在一开始的代码中,由于循环体是空,所以循环的转速极快,导致了 读内存的操作非常频繁,所以就出发了优化
  • 但是,如果在循环体中加上 sleep,让循环转速一下子就慢了,读取内存的操作 就不是特别频繁了,就不会被触发优化了 。
  • 所以说,编译器到底什么时候会优化,仍然是一个 "玄学"问题,它内部有一个完整的优化体系,但是也不关咱们啥事。由于咱们也不好确定 什么时候优化,什么时候不优化,所以还得要在必要的时候加上 volatile 
package thread;
 
import java.util.Scanner;
 
public class Demo16 {
    //写一个 内部类,此时这个内部类 就处在 Demo16 的内部,就可以解决 前面已经写过 Counter 的问题
    static class Counter {
        public int flg = 0;
    }
 
    public static void main(String[] args) {
        Counter counter = new Counter();
 
        Thread t1 = new Thread(() -> {
            while (counter.flg == 0) {
                //执行循环,此处加上 sleep 操作
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1循环结束");
        });
        t1.start();
 
        Thread t2 = new Thread(() -> {
            //让用户输入一个数字,赋值给 flg
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flg = scanner.nextInt();
        });
        t2.start();
    }
}

注意:

  1. volatile关键字 保证的是 内存可见性 的问题,它不保证原子性的问题~
  2. volatile 解决的是 一个线程读、一个线程写 的问题~
  3. 当然,volatile 也可以解决指令重排序的问题~
  4. synchronized 保证的是 原子性的问题,解决的是 两个线程写 的问题~

 五)指令重排序,也可能引起线程不安全

指令重排序,也是 操作系统 / 编译器 / JVM 的优化操作。它调整的是代码的执行顺序,以此来达到加快速度的效果 

比如说,张三媳妇 要张三去到超市买一些蔬菜,并且给了他一张清单:

西红柿、鸡蛋、茄子、小芹菜

调整顺序后,也是符合张三媳妇 对张三的要求:买到了四样菜,并且效率也是得到了提高~

至于买的过程是什么样子的,张三媳妇并不关心~

这个就叫做 指令重排序!!!

可惜的是 指令重排序也会引发线程不安全~

如:

此处,就容易出现指令重排序引入的问题:

2 和 3 的顺序是可以重排的。在单线程下,调换这两的顺序是没有影响的;但是如果在多线程条件下,那就会出现多线程不安全:


假设 另一个线程,尝试读取 t 的引用,

如果按照 2、3的顺序,第二个线程读到 t 为非null 的时候,此时 t 就一定是一个有效对象;

如果按照 3、2的顺序,第二个线程读到 t 为非null 的时候,仍然可能是一个无效对象

总结:

线程安全问题出现的五种原因(前三种原因 是更普遍的 ):

  1. 系统的随机调度(万恶之源、无能为力)
  2. 多个线程同时修改同一个变量(部分规避)
  3. 修改操作不是原子的(有办法改善的)

后两种原因,是 编译器 / JVM / 操作系统 搞出的幺蛾子(但是 总体上来说还是利大于弊的)

  1. 内存可见性
  2. 指令重排序

编译器 / JVM / 操作系统 误判了,导致把不应该优化的地方给优化了,逻辑就变了,bug 就出现了(当然,后两种原因 也可以用 volatile关键字 来进行解决) 

2.3 线程加锁操作解决 原子性问题

现在先重点来介绍一下 解决线程安全问题出现的第三种原因的方法(原子性),通过 加锁操作,来把一些不是原子的操作打包成一个原子的操作!!!

加锁在 Java 中有很多方式来实现,其中最常用的就是 synchronized 

 2.3.1 使用 synchronized关键字 进行加锁

 synchronized 从字面意思上翻译叫做 "同步",其实 实际上它所起的是 互斥的效果~

在一开始的时候,列举了一个典型的线程不安全的例子:创建两个线程,让这两个线程同时并发 对一个变量,自增 5w 次,最终预期能够一共自增 10w 次~

package thread; 
class Counter { 
    public int count; //用来保存计数的变量
     public void increase() {
        count++;
    }
} 
public class Demo14 {
    public static void main(String[] args) {
        //这个实例用来进行累加
        Counter counter = new Counter(); 
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5_0000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start(); 
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + counter.count);
    }
}

那么,怎么使用 synchronized关键字 来解决这个线程不安全的问题呢?

—— 很简单,我们在上面的 increase() 方法 前面加上 synchronized关键字即可(写在 void 之前都可以):

 此时,我们再执行程序,发现无论再运行多少次,发现运行结果是正确的了:


那么,为什么加锁之后,就可以来实现 线程安全的保障呢?

LOCK 这个指令是互斥的,当 线程t1 进行 LOCK 之后,t2 也尝试 LOCK,那么 t2 的 LOCK 就不会直接成功!!!


 2.3.2 synchronized 使用示例

(一)synchronized 直接修饰普通方法

public synchronized void methond() { 
    //......
}

(二)synchronized 修饰静态方法

 public synchronized static void method() {
         //.....
   }

(三)修饰代码块

public void method() {
        synchronized (this) {
            //.....
       }
   }

() 里面的 this 指的是:是针对哪个对象进行加锁 。加锁操作,是针对一个对象来进行的 。

我们要重点理解,synchronized 锁的是什么:两个线程竞争的是同一把锁,才会产生阻塞操作( 两个线程尝试使用两把不同的锁,是不会产生阻塞的)

注意:

  1. 在Java里,任何一个对象,都可以用来做锁的对象,都可以放在  synchronized() 的括号中;其它的主流语言都是专门搞了一类特殊的对象,用来作为锁的对象(大部分的正常对象不能用来加锁)!
  2. 每个对象,内存空间中都会有一个特殊的区域 —— 对象头(JVM自带的,对象的一些特殊的信息) 
  3. synchronized 写到普通方法上相当于是对 this(可创建出多个实例) 进行加锁,synchronized 写到静态方法上 相当于是对类对象(整个 JVM 里只有一个) 进行加锁,synchronized (类名.class)~


三、Java标准库里面的线程安全类

在Java标准库里面,很多线程都是不安全的,如:例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder~

当然,还是有一些是线程安全的,如:Vector (不推荐使用),HashTable (不推荐使用),ConcurrentHashMap (推荐),StringBuffer,String~

需要注意的是,加锁也是有代价的,它会牺牲很大的运行速度(毕竟,加锁涉及到了一些线程的阻塞等待,以及 线程的调度),所以可以视为,一旦使用了锁,我们的代码基本上就和 "高性能" 说再见了 

四、wait 和 notify 关键字

前面已经介绍到,线程它是随机调度的,这个随机性很讨厌,我们希望可以控制线程的执行顺序 

我们已经可以用 join关键字来控制线程结束的顺序, 但是,我们仍希望让两个线程按照既定的顺序配合执行。wait 和 notify 关键字就可以做到这个效果,相比于 jion,它们可以更好的控制线程之间的执行顺序。

  • wait 叫做 "等待",调用 wait 的线程,就会进入线程阻塞等待的状态(即 Waiting状态) 
  • notify 叫做 "通知 / 唤醒",调用 notify 的线程,就可以把对应的 wait 线程给唤醒(即 从阻塞状态恢复回就绪状态) 

wait 和 notify 都是 Object 的成员方法(随便哪个对象都可以调用) 

比如说:如果有 o1.wait();  那么 o1.notify()就可以唤醒调用 o1.wait() 的线程,而 o2.notify() 是不能够唤醒调用 o1.wait() 的线程的 

4.1 wait() 方法

wait() 内部的执行过程:

  1. 释放锁
  2. 等待通知
  3. 当通知到达后,就会被唤醒,并且尝试重新获取锁

wait() 一上来就要释放锁,这就说明 在调用 wait 之前,就需要先拿到锁;

换句话说,wait 必须要放到 synchronized 中使用,并且 synchronized 加锁的对象 和 调用 wait 方法的对象 是同一个对象~


4.2 notify() 方法

notify() 内部执行的过程:进行通知~ 

package thread; 
import java.util.Scanner; 
//创建两个线程,一个线程调用 wait,一个线程调用 notify
public class Demo18 {
    //这个对象用来作为锁对象
    public static Object locker = new Object();
 
    public static void main(String[] args) {
        Thread waitTask = new Thread(() -> {
            synchronized (locker) {
                System.out.println("wait 开始");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 结束");
            }
        });
        waitTask.start();
 
        //创建一个用来 通知/唤醒 的线程
        Thread notifyTask = new Thread(() -> {
            //让用户来控制,用户输入内容后,再执行通知~
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容,开始通知:");
            //next 会阻塞,直到用户真正输入内容以后
            scanner.next();
 
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        notifyTask.start();
    }
}

运行结果:

当然,wait 和 notify 机制,还能够有效避免 "线程饿死"的问题

线程饿死:有些情况下,调度器可能分配的不均匀,导致 有些线程反复占用 CPU,导致有些线程始终捞不着 CPU...... 

        线程在拿到锁之后,判定当下的任务是否可以进行;如果可以进行,那么就干活;如果不可以进行,那么就 wait;等到合适的时候(条件满足的时候)就再继续执行(notify) / 再继续参与竞争锁


注意:

  • notify 在调用的时候,会尝试唤醒进行通知,如果当前对象没有在其他线程里 wait,也不会有副作用 
  • 如果 wait 是一个对象,notify 是另一个对象,则没啥用,无法被唤醒 

4.3 notifyAll() 方法

当然,在 Java 中,还有一个唤醒线程的方法 —— notifyAll() 方法~

当有多个线程等待的时候,notify 是从若干个线程里面随机挑选一个唤醒,是一次唤醒一个;而 notifyAll 则是直接唤醒所有线程,再有这些线程去竞争锁

举个例子理解 notify 和 notifyAll 的区别:

notify 只是唤醒等待队列中的一个线程,其他的线程还是 需要乖乖的等着,如:

而 notifyAll 则是一下子将这些线程全部唤醒,这些进程则需要重新竞争锁,如:

 

由于 最终的结果 notifyAll 还是只能进去一个线程,并且 其他的线程还可能出现 "线程饿死" 的情况,所以说 一般的还是 notifyAll 用的比较少~ 

4.4 wait 和 sleep 的对比

wait 和 sleep都会让线程进入阻塞状态。

但是,阻塞的原因和目的不同,进入的状态也不同,被唤醒的条件也不同。

  1. wait 是用来控制线程之间的执行先后顺序,而 sleep 在实际开发中实际很少会用到(等待的时间太固定了,如果有突发情况 想提前唤醒并不是那么容易) 
  2. wait 进入的是 Waiting 状态,sleep 进入的是 Time Waiting 状态 
  3. wait 是主动被唤醒,而 sleep 是时间到了就会自动被唤醒 
  4. wait 其实是涵盖了 sleep 的功能,即可以死等,也可以等待最大时间,所以一般在实际开发中用的多的是 sleep 

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

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

相关文章

Kotlin高仿微信-第1篇-注册

Kotlin高仿微信-项目实践58篇详细讲解了各个功能点&#xff0c;包括&#xff1a;注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。 Kotlin高仿…

C语言——指针(入门详解)

文章目录1.什么是指针&#xff1f;1.1.理解指针的两个要点&#xff1a;1.2.指针变量&#xff1a;1.3.内存是如何编址&#xff1f;2.指针和指针类型2.1指针的创建与初始化2.2.指针类型3.野指针3.1.什么视野指针&#xff1f;3.2.野指针成因3.3.规避野指针4.指针运算4.1.指针-整数…

【App自动化测试】(十四)Android WebView测试方法

目录1. webview 架构与分析方法1.1 webview与Hybrid开发1.1.1 移动APP三种开发模式1.1.2 native原生开发1.1.2.1 native原生开发说明1.1.2.2 native原生开发组件1.1.3 Hybrid混合开发webview展示1.1.3.1 Hybrid混合开发中的app webview组件1.1.3.2 webview开发代码展示1.1.3.3 …

Reids实战——优惠券秒杀(全局唯一ID生成策略)

1 全局唯一ID生成策略 每个店铺都可以发布优惠券&#xff1a; 当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就存在一些问题&#xff1a; 1. id的规律性太明显 2. 会受单表数据量的限制 全局ID生成器&a…

Android APP全局黑白化实现方案

在清明节时各大APP都会进行黑白化处理&#xff0c;当时在接到这个需求的时候感觉好麻烦&#xff0c;是不是又要搞一套皮肤&#xff1f; 然而在一系列搜索之后&#xff0c;找到了两位大神&#xff08;鸿洋、U2tzJTNE&#xff09;的实现方案&#xff0c;其实相当的简单&#xff…

NoSQL数据库之MongoDB

一、NoSQL数据库背景 传统关系型数据库遇到的问题 2008 年左右&#xff0c;网站 、 论坛、社交网络开始高速发展&#xff0c;传统的关系型数据库在存储及处理数据的时候受到了很大的挑战 &#xff0c;其中主要体现在以下几点&#xff1a; 难以应付每秒上万次的高并发数据写入…

hiveSql 相互关注问题

hiveSql 相互关注问题说明需求分析优化实现最后说明 普遍社交软件上会有关注功能&#xff0c;如何知道自己的关注是否也是关注了自己呢&#xff1f; 需求 求关注结果数据中&#xff0c;相互关注的用户对。 数据如下&#xff1a; follow表&#xff1b;from_user&#xff1a;关…

JMeter 扩展开发:扩展 TCP 取样器

前言 对基于 TCP/IP 协议的套接字应用进行性能测试是非常常见的测试场景。JMeter 提供的“TCP 取样器”大部分情况下可以满足测试的需求&#xff0c;但是也有它的局限性。如果希望实现更灵活的 TCP 套接字测试方式&#xff0c;可以通过对 JMeter 内置的 TCP 取样器进行扩展开发…

在数据工厂中刷新PowerBI数据集

一开始因为部门使用的是坚果云来同步资料&#xff0c;而坚果云同步SSIS工程总是会报错&#xff0c;一气之下就把所有的SSIS迁移到了Azure云上&#xff0c;本来部门使用的就是Azure SQL&#xff0c;所以迁到Data Factory&#xff08;数据工厂&#xff09;也不需要过多的配置。 …

传输线理论基础01——相关定义、信号速率、分布参数与电报方程

前言一直以来都对高频信号、信号完整性、传输线、分布参数这些概念似懂非懂&#xff0c;上学时没学过相关课程&#xff0c;这导致我对高频电路和PCB理解较差&#xff0c;这里新开一个专栏&#xff0c;补齐这方面知识。 一. 传输线相关定义1.1 传输线定义 传输线指的是传输信号…

PyTorch学习笔记-神经网络模型搭建小实战

1. torch.nn.Sequential torch.nn.Sequential 是一个Sequential 容器&#xff0c;能够在容器中嵌套各种实现神经网络中具体功能相关的类&#xff0c;来完成对神经网络模型的搭建。模块的加入一般有两种方式&#xff0c;一种是直接嵌套&#xff0c;另一种是以 OrderedDict 有序…

LabVIEW创建类 1

LabVIEW创建类 1 通过创建LabVIEW类&#xff0c;可在LabVIEW中创建用户定义的数据类型。LabVIEW类定义了对象相关的数据和可对数据执行的操作&#xff08;即方法&#xff09;。通过封装和继承可创建模块化的代码&#xff0c;使代码更易修改而不影响应用程序中的其它代码。 在…

Terraform 华为云最佳实践

目录划分如下&#xff1a;首先是环境&#xff0c;分为网络和service。global是全局的配置&#xff0c;也就是backend的配置&#xff0c;这次使用s3的存储作为backend的存储。最后就是模块做了一些封装。 在global里面的backend里面的main.tf去创建s3的存储。华为云支持s3存储&a…

[附源码]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;…

RK3588平台开发系列讲解(USB篇)USB 外设 CONFIG

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、 Mass Storage Class CONFIG二、USB Serial Converter CONFIG三、USB HID CONFIG四、USB Net CONFIG五、USB Camera CONFIG六、USB Audio CONFIG七、 USB HUB CONFIG沉淀、分享、成长,让自己和他人都能有所收获!…

PG::Seppuku

nmap -Pn -p- -T4 --min-rate1000 192.168.81.90 nmap -Pn -p 21,22,80,139,445,7080,7601,8088 -sCV 192.168.81.90 查看7601端口的页面 对路径进行爆破 在/secret路径下得到了用户名和一个密码字典 尝试ssh爆破 得到密码 eeyoree ssh登录 这里使用sudo -l&#xff0…

FineReport表格软件- 计算操作符说明

1. 概述 FineReport 中使用函数需要用到很多的操作符。 操作符不仅包含很多运算符&#xff0c;还包括一些报表特有的操作符。 FineReport 11.0 优化了公式 2. 运算符类型 运算符用于指定要对公式中的元素执行的计算类型。有默认计算顺序&#xff0c;但可以使用括号更改此顺序…

企业表格软件-FineReport 数组函数概述

1. ADD2ARRAY ADD2ARRAY(array, insertArray, start)&#xff1a;在数组 array 的第 start 个位置插入 insertArray 中的所有元素&#xff0c;再返回该数组。 示例&#xff1a; ADD2ARRAY([3, 4, 1, 5, 7], [23, 43, 22], 3)返回[3, 4, 23, 43, 22, 1, 5, 7]。 ADD2ARRAY([…

将 AWS IAM Identity Center (SSO) SAML 与 Amazon OpenSearch Dashboard集成

Amazon OpenSearch Amazon OpenSearch Service 是一项 AWS 托管服务&#xff0c;可以让您运行和扩展 OpenSearch 集群&#xff0c;而不必担心管理、监控和维护您的基础设施&#xff0c;或者不必在操作 OpenSearch 集群方面积累深入的专业知识。 基于 SAML 的 OpenSearch Dash…

Json用法总结

1、忽略json JsonIgnoreProperties(value{“addressId”}) JSONField(serializefalse) JsonIgnore 2、 JsonFiled JsonProperty XStreamAlias Builder.Default 网上可以查询下相关资料 3、 JSON.parseObject(response, ***Response.class) JSONObject.parseObject(response, **…