多线程-线程安全

目录

线程安全问题

加锁(synchronized)

synchronized 使用方法

synchronized的其他使用方法

synchronized 重要特性(可重入的)

死锁的问题

对 2> 提出问题

对 3> 提出问题

 解决死锁

对 2> 进行解答

对4> 进行解答

volatile 关键字

wait 和 notify (重要)

wait使用实例:

notify使用实例:

" 线程饿死 "

notify 和 notifyAll

小结


线程安全问题

线程安全问题: 有些代码在单个线程环境下执行完全正确. 但是如果同样的代码让多个线程同时执行, 此时就可能出现 bug, 这种情况叫做 "线程安全问题" / "线程不安全", 它是多线程中最复杂, 最重要的部分

举个例子: 两个线程, 每个线程count++ 5000次, 正常情况下结果为 10w, 实际结果:如下图:

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        t1.start();
        t2.start();
        //如果没有这俩 join , 肯定不行, 线程还没有自增完毕, 就开始打印了,
        //打印出来的count 可能是 0;
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

改变一下 join 的次序可以让结果输出正确, 如下代码:

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                count++;
                i++;
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

这个代码意味着 t1 执行时 t2并不会启动, 虽然上述代码卸载两个线程中, 但并不是同时执行的, 而第一个代码中 t1 和 t2 同时执行了, 第二个代码结果输出正确, 为 10w, 我们可以猜测是因为两个线程同时执行的原因导致第一个结果出错了.

解释:

count++ 这个操作本质上是分三步进行的 ~~ 站在 cpu 的角度, 是 cpu 通过三个指令实现的

1> load 把数据从内存读到 CPU 寄存器中

2> add 把寄存器中的数据进行 +1

3> sava 把寄存器中的数据保存到内存中

由于多个线程执行上述代码, 由于线程之间的调度顺序是 "随机" 的, 就会导致有些调度顺序下, 上述的逻辑就会出现问题.

如图:

这只是其中的一种情况, 还可能有无数种情况, 这三个步骤的排列顺序有很多种了, 还有可能 t1 连续执行了多次, 然后 t2 再次执行的情况, 有无数种排列顺序.

我们意识到在多线程程序中最困难的一点是: 现成的随机调度, 是两个线程执行逻辑的先后顺序存在很多可能, 我们要做的是保证在每一种情况下都输出正确的结果.

举个例子看一下: 不同的情况怎么输出结果的:

理想情况下:

两次相加后, 最终可以输出2

可能会出现的情况:

这种情况下两次相加得到的结果为 1

因为线程调度是随机的, 很容易出现错误情况, 这样的话最终的结果是一个随机值, 随机值小于 10w.

产生线程安全的原因:

1> 操作系统中, 线程的调度顺序是随机的 (抢占式执行)

2> 两个线程针对同一个变量进行修改

3> 修改操作不是原子的

此处给定的 count++ 就属于是非原子的操作, 先读取, 在修改, 有三个指令

4> 内存可见性问题

5> 指令重排序问题

 如何解决这个问题呢? 从这些原因入手

1> 调度随机性在系统内核里实现的, 最早的操作系统奠定了这个基调, 无能为力.

2> 有些情况可以通过调整代码结构来规避在这个问题, 有些情况规避不了

3> 有办法让 count++ 三步走成为 "原子" 的   ---->  (加锁) 的方法

加锁(synchronized)

synchronized 使用方法

需要搭配一个代码块 {   } 使用, 进入  {  就会加锁, 出去  }  就会解锁

作用

在已经加锁的状态下, 另一个线程尝试同样加这个锁, 就会产生 "锁冲突/锁竞争", 后一个线程就会阻塞等待, 一直等到前一个线程解锁为止.

使用方法举例

用上述代码进行举例:

count++ 加在代码块中, 然后 synchronized()  这个后面的 () 需要表示一个用来加锁的对象, 这个对象是啥不重要, 重要的是通过这个对象来区分两个线程是否在竞争同一个锁, 如果两个线程是针对同一个对象加锁, 就会有锁竞争, 反之不会有锁竞争, 仍然是并发执行.

追妹子: 你想妹子表白, 成功了就相当于加锁了, 另一个小哥准备追同一个妹子, 就得阻塞等待, 等你俩分手了他才有机会, 如果他准备追另一个对象, 那么可以直接表白.

public class Test {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //我们任意定义一把锁
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                //进行加锁
                synchronized (locker) {  
                    count++;
                    i++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            int i = 0;
            while(i < 5000) {
                //进行加锁
                synchronized (locker) {
                    count++;
                    i++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(count);
    }
}

 运行结果正确了.

加过锁之后两个线程相互影响, 在进行 count++ 时会先加锁, t1 线程加过锁了, t1没执行完之前, t2 操作会出现阻塞. 只有当 t1 中 count++ 的操作执行完之后才会让 t2 中的 count++ 进行操作, 这就避免了 t1 中的 load add save 与 t2 中的 load add save 操作 出行穿插, 此时线程安全问题就迎刃而解了.

如果在 两个线程加锁时 使用不同的 锁 那么就不会出现锁竞争, 上述问题就不会解决

其中synchronized 后面 () 中的锁对象到底是哪个对象无所谓, 重要的是俩线程加锁的对象是否是同一个对象.

synchronized的其他使用方法

synchronized 还可以修饰 一个方法

class Counter {
    public int count;
    synchronized public void increase() {
        count++;
    }
    public void increase2() {
        synchronized (this) {
            count++;
        }
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //预期结果是 10w
        System.out.println(counter.count);
    }
}

两种的写法是一样的, 上面是下面的简化版本

synchronized还可以修饰一个 静态方法

两种的写法是一样的, 上面代码是下面代码的简化版本

其中 Counter.class 是类对象

.java => .class => JVA加载到内存中(类对象) 可以看作是 .Java 文件中的二进制码

类对象中包含以下内容: 

1> 类的属性有哪些, 名字, 类型, 权限

2> 类的方法有哪些, 名字, 类型, 权限

3> 类本身继承自哪个类, 实现了哪些接口

在一个 Java 进程中, 类对象是唯一的.

synchronized 用的锁是存在 Java 对象头里的

Java 的一个对象, 对应的内存空间中, 除了你自己定义的一些属性之外, 还有一些自带的属性, 在对象头中, 其中就有属性表示当前的对象是否已经加锁.

synchronized 重要特性(可重入的)

可重入定义: 一个线程连续针对一把锁加锁两次, 不会出现死锁, 满足这个要求就是 "可重入" , 不满足就是 "不可重入" .

 死锁的解释: 有一个线程 t , 锁对象 locker,  t 线程中存在下列代码:

synchronized (locker) {

          synchronized (locker) {

                 ........... 

         }

}

第一次加锁能够加锁成功, 此时 locker 属于 "被锁定" 的状态, 第二次加锁 locker 已经是锁定状态, 第二次加锁操作, 应该要 "阻塞等待" 的, 等到锁被释放之后才能加锁成功

第二次想要加锁成功, 需要第一次加锁释放锁, 释放锁就要第二次加锁成功

这样就出现了死锁现象. 就是一个bug, 可能会出现这种情况

    private static Object locker = new Object();
    public static void func1() {
        synchronized (locker) {
            func2();
        }
    }
    public static void func2() {
        func3();
    }
    public static void func3() {
        func1();
    }
    public static void func4() {
        synchronized (locker) {
        }
    }

 这种bug时常出现而且不容易发现.

问题是: 上述代码中, synchronized 是可重入锁, 没有因为第二次加锁而死锁, 加入上述加锁过程有 N 层, 释放时机该如何判定? 

解答: 此处无论有多少层锁, 都是到在最外层才能释放锁, 提前释放会线程不安全

引用计数: 锁对象中不但要记录谁拿到了锁, 还要记录锁被加了几次, 每加锁一次, 计数器 +1, 每解锁一次, 计数器 -1, 除了最后一个大括号恰好减成 0 , 才真正释放锁.

死锁的问题

关于死锁总结: 

1> 一个线程针对一把锁, 连续加锁两次, 如果是不可重入锁, 就死锁了. (synchronized 不会出现)

2> 两个线程, 两把锁 (此时无论是不是可重入锁, 都会死锁)

 t1  t2 两个线程,  A 和 B 两把锁

t1 获取锁 A, t2 获取锁 B,  t1 尝试获取B, t2 尝试获取 A. 这种情况出现死锁.

3> N 个线程, M 把锁 (相当于 2> 的扩充) 更容易出现死锁的情况了, 经典模型: 哲学家就餐问题.

对 2> 提出问题

public class Test {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // 此处的 sleep 很重要, 要确保 t1 和 t2 都分别拿到一把锁
                // 之后再进行动作
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2) {
                    System.out.println("t1 加锁成功!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1) {
                    System.out.println("t2 加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

此时的代码会出现死锁情况, 什么都没法打印出来

 此时打开 jconsole (java带的一个查看线程情况的工具) 就可以看到这俩线程的状态:

当前线程出现了阻塞状态;

对 3> 提出问题

死锁是属于很严重的 bug (导致线程卡住, 无法执行后续工作)

死锁的成因涉及到四个 必要条件

1> 互斥使用 (锁的基本特性) 当一个线程持有一把锁之后, 另一个线程也想获取到锁, 就要阻塞等待

2> 不可抢占 (锁的基本特性) 当锁已经被线程 1 得到之后, 线程2 只能等线程1 主动释放出来, 不能强行抢过来

3> 请求保持 (代码结构) 一个线程尝试获取多把锁, (先拿到锁1 之后, 在尝试获取锁2 的时候锁1 不会释放, 就是的上面的2> 例子

4> 循环等待/环路等待  (代码结构) 等待的依赖关系形成环了, 上面的3> 哲学家就餐的例子

 解决死锁

如何解决/避免死锁呢?

核心是破坏上述必要条件

1> 和 2> 是锁的特性, 不能改变, 要从 3> 和 4> 着手

3> 来说, 调整代码结构, 避免编写 "锁嵌套" 逻辑,  当然这个方案不一定好使, 有的需求可能就是需要获取多个锁之后再操作

4> 通过约定加锁顺序, 就可以避免循环的等待

对 2> 进行解答

调整代码结构, 避免编写 "锁嵌套" 逻辑

public class Test {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // 此处的 sleep 很重要, 要确保 t1 和 t2 都分别拿到一把锁
                // 之后再进行动作
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker2) {
                System.out.println("t1 加锁成功!");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (locker1) {
                System.out.println("t2 加锁成功!");
            }
        });
        t1.start();
        t2.start();
    }
}

这样就能正常输出结果了.

对4> 进行解答

通过约定加锁顺序, 就可以避免循环的等待

针对锁进行编号, 比如约定 加多把锁的时候先加编号小的锁, 后加编号大的锁.(所有线程都遵守这个规则) 举个例子如下图:

最终死锁问题迎刃而解, 本质上是破除了循环等待

synchronized 使用规则并不复杂, 抓住一个原则, 两个线程针对同一个对象加锁, 就会产生锁竞争.

volatile 关键字

作用: 

1> 保证内存可见性

2> 禁止指令重排序

1>什么是 内存可见性

计算机运行的程序/代码, 经常要访问数据, 这些依赖的数据往往存在在 内存中, (定义一个变量, 变量就是存在内存中), CPU 使用这个变量的时候, 就会把这个内存中的数据先读出来, 放到 CPU 的寄存器中在参与运算 (load)

CPU 读内存 相当于 读硬盘 快几千上万倍, 读寄存器 相比于 读内存 又快了几千上万倍, 为了提高效率, 编译器把代码做出优化, 把一些本来要读内存的操作, 优化成读其寄存器, 减少读内存的次数, 也就可以提高整体程序的效率了

举个例子:

public class Test {
    private static int isQuit = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(isQuit == 0) {

            }
            System.out.println("t1 退出");
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            System.out.println("请输入 isQuit: ");
            Scanner sc = new Scanner(System.in);
            isQuit = sc.nextInt();
        });
        t2.start();
    }
}

当我们输入 isQuti == 1 或者其他不为零的数时程序应该停止运行, 但结果不是. 打开 jconsole 可以看到 t1 线程正在执行, 为 RUNNABLE 状态

之前是两个线程修改用一个变量会引起线程安全问题, 现在是一个线程读, 一个线程修改也有可能会有问题, 就是因为 内存可见性引起的

 

使用方法:

在 isQuit 变量加上 volatile限制就可以了

关于内存可见性还涉及到一个关键概念, JMM(Java Memory Model, Java 内存模型) Java规范文档的叫法.

JMM 把存储空间分为 主内存 和工作内存, t1线程对应 isQuit 变量, 本身是在 主内存中的, 由于此处的优化就会把 isQuit 变量放到工作内存中. 进一步的 t2 修改主内存的 isQuit, 不会影响到 t1 的工作内存. 主内存就是咱们平常说的内存, 工作内存就是 CPU 寄存器.

volatile 可以保证内存可见性, 但是不能保证原子性

wait 和 notify (重要)

wait 和 notify 都是 Object 方法, 随便定义一个对象, 都可以使用 wait notify .它俩需要配合使用

作用: 用来协调多个线程的执行顺序

本身多个线程的执行顺序是随机的 (系统随即调度, 抢占式执行) 很多时候希望通过一定手段, 协调执行顺序. join 使用像线程结束的先后顺序, 相比之下此处是希望线程不结束, 也能够有先后顺序的控制.

wait 等待:  让指定线程进入阻塞状态

notify 通知:  唤醒对应的阻塞状态的线程

wait如何使用

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

此处代码报错, 非法的 监视器 状态 异常 , 其中 synchronized 就是监视器锁. 

wait 操作在执行的时候要做 三件事

1> 释放当前的锁

2> 让线程进入阻塞

3> 线程被唤醒的时候重新获取到锁

通过object 调用wait 释放锁的过程. 释放锁的前提就是 先加锁

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {
            System.out.println("wait 之前");
            //将 wait 放到 synchronized 里面调用, 保证确实是拿到了锁
            object.wait();
            System.out.println("wait 之后");
        }
    }
}

应该把 wait 写到 synchronized 里面加锁, 此时可以运行代码, 但是wait 会持续阻塞等待下去, 直到其他线程调用 notify 唤醒. 

此处的状态就是 waiting 状态

wait 除了默认的无参版本之外, 还有一个带参数的版本, 带参数的版本就是指定一个时间

参数的版本就是指定超时时间, 避免 wait 无休止地等待下去.

notify如何使用

public class Test {
    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) {
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (object) {
                System.out.println("进行通知");
                object.notify();
            }
        });
        t1.start();
        t2.start();
    }
}

 

结果如图.

" 线程饿死 "

针对这种情况:可以使用 wait 和 notify 来解决

让 1 号老铁在发现没钱的时候就进行 wait.(wait 内部本身就会释放锁, 并且进入阻塞)

让 1 号老铁不再进行后续的锁竞争, 把所释放出来让别人获取. 给其他老铁提供机会

运钞车把钱运过来的线程就是调用 notify 唤醒的线程

notify 和 notifyAll

notify 一次唤醒一个线程

notifyAll 一次唤醒全部线程

调用 wait 不一定只有一个线程调用, N 个线程都可以调用 wait, 此时有多个线程调用的时候这些线程都会进入阻塞状态., 唤醒的时候就有两种方式了

nitifyAll: 唤醒的时候 wait 涉及到一个重新获取锁的过程, 需要串行执行(用的更少)

notify: 更可控(用的更多)

小结

保证线程安全需要 : 保证原子性, 可见性, 顺序性:

了解synchronized 和 wait notify , volatile 的语法, 目的

掌握死锁的几种情况, 及如何解决死锁问题.

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

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

相关文章

如何在沉浸式翻译浏览器插件中使用免费的DEEPLX和配置API接口

如何在浏览器插件沉浸式翻译中使用DEEPLX 如何配置免费的DEEPLX翻译功能如何打开PDF翻译功能如何解除翻译额度限制 如何配置免费的DEEPLX翻译功能 假设你已经在浏览器上安装了沉浸式翻译插件&#xff0c;但是不知道如何使用免费的DEEPLX功能 这里以EDGE浏览器为例&#xff0c;…

JVM从1%到99%【精选】-类加载子系统

目录 1.类的生命周期 1.加载 2.连接 3.初始化 2.类的加载器 1.类加载器的分类 2.双亲委派机制 3.面试题&#xff1a;类的双亲委派机制是什么&#xff1f; 4.打破双亲委派机制 1.类的生命周期 类加载过程&#xff1a;加载、链接&#xff08;验证、准备、解析&a…

# 从浅入深 学习 SpringCloud 微服务架构(十七)--Spring Cloud config(1)

从浅入深 学习 SpringCloud 微服务架构&#xff08;十七&#xff09;–Spring Cloud config&#xff08;1&#xff09; 一、配置中心的 概述 1、配置中心概述 对于传统的单体应用而言&#xff0c;常使用配置文件来管理所有配置&#xff0c;比如 SpringBoot 的 application.y…

解决 Content type ‘application/json;charset=UTF-8‘ not supported

文章目录 问题描述原因分析解决方案参考资料 问题描述 我项目前端采用vue-elementUi-admin框架进行开发&#xff0c;后端使用SpringBoot&#xff0c;但在前后端登录接口交互时&#xff0c;前端报了如下错误 完整报错信息如下 前端登录接口JS代码如下 export function login(…

商业数据分析--时间序列图及趋势分析

绘制时间序列图,并指出存在什么样的状态如上两图: 可见状态:从时间序列图可以看出,这些数据存在明显的季节性波动,每年的第4季度值都最高,而第2季度值最低。同时也存在一些下降的趋势。 通过引进虚拟变量,建立多元线性回归模型。答: 通过引入虚拟变量,我们可以建立如下的…

7-Zip:解锁数字世界的压缩艺术

探索数字世界&#xff0c;你需要的不仅是勇气&#xff0c;还有正确的工具。《7-Zip&#xff1a;解锁数字世界的压缩艺术》将带你深入了解7-Zip——这个开源免费的压缩工具&#xff0c;将帮助你在数字世界中更加游刃有余&#xff01; 文章目录 7-Zip 使用介绍1. 引言2. 背景介绍…

Linux系统——VIM编辑工具

vi/vim vi是一个文本编辑器&#xff0c;用于撰写文档&#xff0c;或者开发程序。vim是vi的增强版 功能一致&#xff0c;可视化效果更好一些。去鼠标化 编辑更加方便 可定制化。 vim编辑器是一个模式化文本编辑器 模式以&#xff1a;编辑模式 进入文档后默认的模式 作用&am…

AI虚拟伴侣方案

打造类似Character AI的产品,现成的训练好的模型方案,适合做陪伴型虚拟女友等项目,近期看到的最佳项目: 1、项目背景: (1)项目动机:角色扮演LLM是AI的第二大消费用例,但通常被开源社区忽视。 (2)行业现状:缺乏与https://character.ai/提供的角色扮演LLM相对应的…

QuickBooks 2024 for Mac 激活版:智慧管理,财务无忧

想要轻松掌控财务&#xff0c;实现高效管理吗&#xff1f;QuickBooks 2024 for Mac&#xff0c;您的智慧财务管理专家&#xff0c;为您带来前所未有的便利和体验。无论是账务、工资还是销售和库存&#xff0c;它都能一手搞定。直观易用的界面&#xff0c;让您轻松上手&#xff…

PullTube for Mac:视频下载,一键搞定

还在为找不到想看的视频而烦恼吗&#xff1f;PullTube for Mac&#xff0c;让您的视频下载之旅变得更加轻松&#xff01;支持从多个主流视频网站下载视频&#xff0c;提供多种格式和质量选项&#xff0c;满足您的不同需求。简单易用的界面设计&#xff0c;让您轻松上手&#xf…

MongoDB安装及接入springboot

环境&#xff1a;windows、jdk8、springboot2 1.MongoDB概述 MongoDB是一个开源、高性能、无模式&#xff08;模式自由&#xff09;的文档&#xff08;Bson&#xff09;型数据库&#xff1b;其特点如下&#xff1a; 模式自由 ---- 不需要提前创建表 直接放数据就可以 支持高并…

制作绿色便携式Chrome浏览器

准备环境 chrome离线解压包7zip解压缩软件Chrome Portable便携版启动程序 一、获取Chrome离线解压包 获取官方的离线下载包&#xff0c;使用7zip软件打开压缩包。如果里面看到的事102~表示是离线安装包&#xff0c;如果是chrome.7z表示是离线解压包。 如果是解压包的话&…

picoCTF-Web Exploitation-More SQLi

Description Can you find the flag on this website. Additional details will be available after launching your challenge instance. Hints SQLiLite 先随便输入个账号密码登录一下&#xff0c;得到查询SQL&#xff0c;接下来应该对SQL进行某些攻击来绕过密码登录成功 -- …

分布式与一致性协议之PBFT算法(二)

PBFT算法 如何替换作恶的主节点 虽然PBFT算法可以防止备份节点作恶&#xff0c;因为这个算法是由主节点和备份节点组成的&#xff0c;但是&#xff0c;如果主节点作恶(比如主机点接收到了客户端的请求&#xff0c;但就是默不作声&#xff0c;不执行三阶段协议)&#xff0c;那…

二叉搜索数使用,底层原理及代码实现

1:二叉搜索树的定义 二叉搜索树的底层是一个二叉链表 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树 &#xff0c;或者是具有以下性质的二叉树 : 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所…

js逆向-某投资平台参数分析

声明 本文仅供学习参考&#xff0c;如有侵权可私信本人删除&#xff0c;请勿用于其他途径&#xff0c;违者后果自负&#xff01; 如果觉得文章对你有所帮助&#xff0c;可以给博主点击关注和收藏哦&#xff01; 分析 aHR0cDovLzIyMS4yMTQuOTQuNTE6ODA4MS9pY2l0eS9pcHJvL2hhb…

4.5网安学习第四阶段第五周回顾(个人学习记录使用)

本周重点 ①部署域环境&#xff08;Win2008&#xff09; ②域组策略 ③域内信息收集 ④(重点)哈希传递攻击PTH ⑤MS14-068 提权漏洞 ⑥黄金票据伪造 ⑦白银票据伪造 ⑧ZeroLogon (CVE-2020-1472) 漏洞复现 本周主要内容 ①部署域环境&#xff08;Win2008&#xff09;…

.net core WebApi 部署 IIS

安装 IIS 下载需要的 net 版本安装 前往 .net core WebApi 项目打包 Program.cs var builder WebApplication.CreateBuilder(args);// 输出 builder.Services.AddControllers().AddJsonOptions(options > {options.JsonSerializerOptions.PropertyNamingPolicy null;…

.NET_NLog

步骤 1. 添加依赖 ①Microsoft.Extensions.DependencyInjection ②NLog.Extensions.Logging&#xff08;或Microsoft.Extensions.Logging.___&#xff09; Tutorial NLog/NLog Wiki GitHub 2.添加nlog.config文件(默认名称, 可改为其他名称, 但需要另行配置) 文件的基础…

第13节 第二种shellcode编写实战(2)

在第二种shellcode编写实战(1)的基础上&#xff0c;新增加一个CAPI类&#xff0c;将所有用到的函数都在这个类中做动态调用的处理&#xff0c;这样使得整个shellcode功能结构更加清晰。 1. 新建类CAPI&#xff08;即api.h和api.cpp两个文件&#xff09;&#xff1a; api.h&…