Java JMM

news2024/11/28 18:36:03

JMM 全称: Java Memory Model (Java 内存模式)。
它是一种虚拟机规范, 用于屏蔽掉各种硬件和操作系统的内存访问差异, 以实现 Java 程序在各种平台下都能达到一致的并发效果。
主要规定了以下两点

  1. 一个线程如何以及何时可以看到其他线程修改过后的共享变量的值, 即线程之间共享变量的可见性
  2. 如何在需要的时候对共享变量进行同步

了解 JMM 大体的概念, 可以帮忙我们了解 Java 并发的一些设计。

1 线程通信和线程同步

1.1 线程通信

通信是指线程之间以何种机制来交换信息。
在命令式的编程中, 线程之间的通信机制有两种: 共享内存消息传递

共享内存并发的模型里, 线程之间共享程序的公共状态, 线程之间通过读 - 写内存中的公共状态来隐式进行通信。
消息传递的并发模型里, 线程之间没有公共状态, 线程之间必须通过明确的发送消息来显示进行通信, 在 Java 中典型的消息传递方式就是 wait() 和 notify()。

Java 的并发采用的就是 共享内存模型, Java 线程之间的通信总是隐式进行的, 整个通信过程对程序员是完全透明的。
这里提到的共享内存模型指的就是 Java 内存模型 (简称 JMM )。

1.2 线程同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

共享内存并发的模型里, 同步是显式进行的。程序必须显式指定某个方法或某段代码需要在线程之间互斥执行。
消息传递的并发模型里, 由于消息的发送必须在消息的接收之前, 因此同步是隐式进行的。

2 Java 对 JMM 的实现

2.1 JMM 在 Java 实现中的抽象结构模型

JMM 定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存 (Main Memory) 中, 每个线程都有一个私有的本地内存 (Local Memory), 本地内存中存储了该线程已读/写共享变量的副本。
本地内存是 JMM 的一个抽象概念, 并不真实存在。它涵盖了缓存, 写缓冲区, 寄存器以及其他的硬件和编译器优化。

Alt 'JMM 抽象模型'

从上图来看, 线程 A 与线程 B 之间如要通信的话, 必须要经历下面 2 个步骤

  1. 线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去
  2. 线程 B 到主内存中去读取线程 A 更新的共享变量

2.2 JMM 在 Java 的具体实现

通过 JMM 抽象结构模型, 可以知道 Java 就是变量的传递, 达到了隐式通信的效果, 而这个过程需要借助 2 个重要的数据存储才能实现

  1. 本地内存
  2. 主内存

而 JVM 是如何实现这 2 个地方的呢?

Alt 'JVM 内存模型'

JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干不同的数据区域, 这些区域都有各自的用途以及创建和销毁的时间。

主要包含 2 类:

1. 线程共享区域

方法区 (Method Area): 方法区是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
堆 (Heap): 用来保存程序运行中所创建的所有对象、数组元素等

2. 线程私有区域

虚拟机栈 (VM Stack): 运行在 Java 虚拟机上的线程都拥有自己的线程栈, 主要用于存储线程执行方法时的各种状态数据等信息
本地方法栈 (Native Method Stack): 本地方法栈与虚拟机栈的作用相似, 不同之处在于虚拟机栈为虚拟机执行的 Java 方法服务, 而本地方法栈则为虚拟机使用到的 Native 方法服务
程序计数器 (PC Register) : 程序计数器保存着每一条线程下一次执行指令位置

3 JMM 在并发编程中需要解决的一些问题

3.1 主内存和本地内存设计带来的问题

备注:
我们知道操作系统之间, CPU 的运算速度很快, 而 IO 的效率和他比起来慢了很多, 即便是直接读取内存, 所以 CPU 和内存之间存在着多层高速缓存, 以保证 CPU 可以保持自己运算能力, 不受 IO 的影响。
同理, 线程在主内存之间, 维护了一套自己的本地内存, 也是为了保持自己的执行效率。
而这样的设计, 就和操作系统类似, 带来了一些其他问题。

3.1.1 可见性问题

Alt 'JVM 可见性问题'

如上图, 3 个共享变量 count, 2 个为副本。
启动 2 个线程分别对共享变量操作, 假设原本共享变量 count 为 0。

  1. 线程 A 从主内存将这个共享变量 count 加载到自己的本地内存, 值为 0
  2. 线程 B 执行同样的加载操作, 值为 0
  3. 线程 A 对这个共享变量 count + 1, count 的值变为 1, 没有将这个值同步到主内存
  4. 线程 B 这时候同样需要对这个共享变量 count + 1, 但是这时候 B 中的 count 还是 0, 没有感知到 A 对其做的修改

在多线程的环境下, 如果某个线程首次读取共享变量, 则首先到主内存中获取该变量, 然后存入工作内存中, 以后只需要在工作内存中读取该变量即可。
同样如果对该变量执行了修改的操作, 则先将新值写入工作内存中, 然后再刷新至主内存中, 这个刷新时间虽然很短但并不确定。

3.1.2 竞争问题

Alt 'JVM 竞争问题'

如上图:
如果这两个加 1 操作是串行的, 最终主内存中的 count 的值应该是 3。
然而图中两个加 1 操作是并行的, 当它们值更新到工作内存的副本后, 会争相刷新主内存。在这里, 不管是线程 1 还是线程 2 先刷新计算结果到主内存, 最终主内存中的值只能是 2。

3.1.3 主内存和本地内存

为了解决上面提到的问题, JVM 提供了很多的工具类和关键字, 达到加锁串行操作, 本地内存失效, 数据强制刷新主内存等效果, 达到解决问题

  1. synchronized 让线程之间串行的执行
  2. volatile 让变量变更立即刷入主内存, 其他线程相关的缓存失效
  3. Lock 加锁, 串行执行

这些不是本篇的重点, 就不展开了

3.2 重排序的影响

在上面, 我们讨论了主内存和本地内存在多线程的情况带来的影响: 可见性 + 竞争
这 2 个都是由本地内存和主内存带来的。

而在并发过程中, 除了这 2 个地方会引起问题外, 在编译器中还存在重排序问题: 在执行程序时, 为了提高性能, 编译器和处理器常常会对指令 (我们写的代码, 最终会被转为 1 到多条指令, 然后逐个执行) 做重排序。
这些系统内部的优化大部分都是有效的, 但是有时在并发编程中, 则会带来某些问题。

3.2.1 重排序的类型

一个好的内存模型实际上会放松对处理器和编译器规则的束缚, 也就是说软件技术和硬件技术都为同一个目标而进行奋斗: 在不改变程序执行结果的前提下, 尽可能提高并行度。
JMM 对底层约束比较少, 使其能够发挥自身优势。因此, 在执行程序时, 为了提高性能, 编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:

Alt 'JVM 重排序类型'

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下, 可以重新安排语句的执行顺序
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性, 处理器可以改变语句对应机器指令的执行顺序
  3. 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区, 这使得加载和存储操作看上去可能是在乱序执行的。

注: 指令并行重排序和内存系统重排序统称为处理器排序

3.2.2 重排序的影响

我们知道重排序是编译器或者系统的优化。
但是如果有些指令存在依赖性的话, 进行重排序会导致错误。

数据依赖性
如果两个操作访问同一个变量, 且这两个操作中有一个为写操作, 此时这两个操作之间就存在数据依赖性。
数据依赖分为下列 3 种类型, 这 3 种情况, 只要重排序两个操作的执行顺序, 程序的执行结果就会被改变。

名称说明示例
写后读写一个变量, 再读这个变量a = 1; b = a;
写后写写一个变量, 再写这个变量a = 1; a = 2;
读后写读一个变量, 再写这个变量a = b; b = 1;

这三种操作都存在数据依赖性, 如果重排序最终会导致结果受到影响。

控制依赖性

public int method(int a) {
    int num = 2;
    if (flag) {
        int num = a * a;
        return num;
    }
    return num;
}

在上面的代码中, 变量 num 的值依赖于 if (flag) 的判断值, 这里就叫控制依赖。

控制依赖在单线程的情况下, 对存在控制依赖的操作重排序, 不会改变执行结果。
但是在多线程的情况下, 就有可能出现问题。

比如下面的例子:

public class Demo {
    int a = 0;
    boolean flag = false;
    
    public void init() {
        // 步骤 1
        a = 1;
        // 步骤 2
        flag = true;
    }
    
    public void use() {
        // 步骤 3
        if (flag) {
            // 步骤 4
            int i = a * a;
        }
    }
}

在程序中, 当代码中存在控制依赖性时, 会影响指令序列执行的并行度。为此, 编译器和处理器会采用猜测 (Speculation) 执行来克服控制相关性对并行度的影响。

以处理器的猜测执行为例:
假设现在有 2 个线程 A, B, A 执行到了 init 方法, 线程 B 执行到了 use 方法。
执行线程 B 的处理器可以提前读取并计算 a*a, 然后把计算结果临时保存到一个名为重排序缓冲 (Reorder Buffer, ROB) 的硬件缓存中。
当步骤 3 的条件判断为真时, 就把该计算结果写入变量 i 中。
猜测执行实质上对操作 3 和 4 做了重排序, 问题在于这时候, a 的值可能还没被线程 A 赋值。

当步骤 1 和步骤 2 重排序, 步骤 3 和步骤 4 重排序时, 可能会产生什么效果?
步骤 1 和步骤 2 做了重排序。程序执行时, 线程 A 首先写标记变量 flag, 随后线程 B 读这个变量。由于条件判断为真, 线程 B 将读取变量 a。
此时, 变量 a 还没有被线程 A 写入, 这时就会发生错误!

所以在多线程程序中, 对存在控制依赖的操作重排序, 可能会改变程序的执行结果。

3.2.3 禁止重排序

通过上面的分析, 重排序可能导致线程安全的问题, 不做任何的限制, 会导致一些意向不到的事情。

所以对于重排序, JMM 对 Java 编译器做了一些限制要求

  1. 针对编译器重排序, Java 编译器按照规则禁止一些特定类型的编译器重排序
  2. 针对处理器重排序, Java 编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序

当然也不是全面禁止掉重排序, 在数据没有任何依赖性时, 重排序还是允许的。

举个例子:

// 步骤 1
double pai = 3.14;
// 步骤 2
double r = 1;
// 步骤 3
double area = pai * r * r;

上面是计算圆面积的代码, 步骤 1, 2 之间没有任何的关联, 所以 2 者之间可以重排序, 也可以说步骤 1 和 2 之间没有数据依赖性 (如果两个操作访问同一个变量, 且这两个操作有一个为写操作, 此时这两个操作就存在数据依赖)。
这里可以分为 3 种情况: 读后写 / 写后读 / 写后写, 这 3 种情况, 无论哪一种发生了重排序, 最终执行结果会存在影响。

最终的结论 Java 编译器通过禁止特定的编译器和内存屏障指令, 让编译器和处理器在重排序时, 会遵守数据依赖性, 不会改变存在数据依赖性关系的两个操作的执行顺序

4 Happens-Before - JMM 对操作系统的屏蔽

Happens-Before, 中文译名应该, 先行发生, 大体要表达的意思就是: A 操作会先于 B 操作执行

在上面的分析中, 主要是分析了线程的双内存重排序, 在并发中带来的问题, 涉及了大量的底层知识。
如果在编程中需要考虑这么多底层的知识, 那么对于编写程序的人的负担是很大的。

因此, JMM 为程序员在提供了一套 Happens-Before 的规则, 保证程序员编写的代码在满足对应的条件, 代码的效果都是都是遵循里面的规则。
这样程序员完全可以根据这套规则去和编写并发程序和解决并发的的各种问题, 而不必去思考操作系统底层具体操作, 如内存同步, 重排序等。

JMM 这么做的原因是: 程序员对于这两个操作的底层实现并不关心, 程序员关心的是程序执行时的语义不能被改变 (即执行结果不能被改变)。

大体的效果如下:

Alt 'JMM HappensBefore 效果'

4.1 定义

  1. 如果一个操作 Happens-Before 另一个操作, 那么第一个操作的执行结果将对第二个操作可见, 而且第一个操作的执行顺序排在第二个操作之前
  2. 两个操作之间存在 Happens-Before 关系, 并不意味着 Java 平台的具体实现必须要按照 Happens-Before 关系指定的顺序来执行。
    如果重排序之后的执行结果, 与按 Happens-Before 关系来执行的结果一致, 那么这种重排序并不非法 (也就是说, JMM 允许这种重排序)

第一条是 JMM 对程序员的保证。
如果 A Happens-Before B, 那么 JMM 将向程序员保证 —— A 操作的结果将对 B 可见, 且 A 的执行顺序排在 B 之前。

第二条是 JMM 对编译器和处理器重排序的约束原则。
JMM 允许两个操作之间存在 Happens-Before 关系, 不要求 Java 平台的具体实现必须要按照 Happens-Before 关系指定的顺序来执行。
如果重排序之后的执行结果, 与按 Happens-Before 关系来执行的结果一致, 那么这种重排序是允许的。

4.2 具体的规则

4.2.1 程序顺序规则 (Program Order Rule)

在一个线程中,按照代码的顺序,前面的操作 Happens-Before 于后面的任意操作。
简单理解: 同一个线程 A 中前面的所有写操作对后面的操作可见。

例子

int a = 1;
int b = a + 1;

在同一个线程, 对 a 的赋值操作的结果 (a = 1), 对于后面的操作 (int b = a + 1), 都是可以明确知道的, 即后面操作中, a 一定是 1, 不会是其他值。

备注:
这个结果的保证, 作为程序员的我们无需去关心底层是如何实现的, 内存如何同步, 是否重排序等。我们只需要知道 JVM 遵循了 Happens-Before 原则, 一定是这样的就行。

4.2.2 监视器锁规则 (Monitor Lock Rule)

同一个锁的 unlock 操作 Happens-Before 此锁的 lock 操作。
简单理解: 线程 A 获取锁成功, 做了一些数据的变更, 然后线程 A 释放锁, 线程 B 获取同一个锁成功了, 那么线程 A 释放锁前做的所有数据变更, B 线程都是可见的。

4.2.3 volatile 变量规则 (volatile Variable Rule)

对一个 volatile 变量的写操作 Happens-Before 后续每一个对该变量的读操作。
简单理解: 线程 A 对 volatile 修饰的变量 v 进行操作, 后面其他的线程对变量 v 的读取, 结果都是线程 A 对变量 v 的操作后的结果, 对其他线程 (包括自己) 是可见的。

4.2.4 线程启动规则 (Thread Start Rule)

Thread 的 start 方法 Happens-Before 调用 start 方法的线程前的每一个操作。
简单理解: 线程 T1 做了很多操作, 然后调用线程 T2 的 start 方法启动一个新的线程, 这时 T1 在调用 T2 的 start 方法前做到所有变更, 对线程 T2 都是可见的。

4.2.5 线程终止规则 (Thread Termination Rule)

线程中任何操作都 Happens-Before 其它线程检测到该线程已经结束。
简单理解: 线程 T1 做了很多操作, 然后线程 T2 感知到线程 T1 已经终止了, 那么线程 T1 做到变更, 对线程 T2 都是可见的。

例子

int num = 1;
Thread theadT1 = new Thread(() -> num = 2);
theadT1.start();

// 等待 T1 执行完成
theadT1.join();
// 这时当前线程 T2 读取 num 值一定是 2
4.2.6 线程中断规则 (Thread Interruption Rule)

对线程 interrupt 方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生。
简单理解: 线程 T1 做了很多操作, 然后调用线程 T2 的中断方法 interrupt, 这时线程 T1 做的操作对于线程 T2 都是可见的。

int num = 1;

Thead threadT2 = new Thread(() -> {

  // 线程 T2 没有被中断, 就一直循环
  while(!Thread.currentThread().isInterrupted()){
    // 线程 T2 被中断了, 此时线程 T2 读取 num 值一定是 2
    System.out.println(x);
  }
})
threadT2.start();

// 线程 T1 修改 num 值
num = 2;

// 线程 T1 调用线程 T2 的中断方法
threadT2.interrupt();
4.2.7 对象终结规则 (Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)Happens-Before 它的 finalize 方法的开始。
简单理解一个对象初始化一定在其 finalize 之前 (间接的表明了构造函数其间做的操作在 finalize 方法时都是可见的)。

public class A {

  private int num = 1;

  public A() {
    System.out.println("对象创建: " + num);
    this.num = 2;
  }

  @Override
  protected void finalize() throws Throwable {
    System.out.println("对象销毁: " + num);
  }
}

// 结果
//对象创建: 1
//对象销毁: 2
// 对象创建的日志打印一定在对象销毁之前
4.2.8 传递性 (Transitivity)

如果操作 A Happens-Before B, B Happens-Before C, 那么可以得出操作 A Happens-Before C。
简单理解就是 A 操作的结果对 B 操作可见, B 操作对 C 操作可见, 那么 A 操作对 C 操作同样可见。

4.3 Happens-Before 规则的真正意义

我们已经知道, 导致多线程间可见性问题的两个原因: CPU 缓存和重排序。
一种极端的解决多线程可见性问题的方式就是: 禁止所有 CPU 缓存和重排序。可行, 但是会极端影响处理器的性能。

为了解决多线程的可见性问题, 但是尽可能少的影响处理器性能, 可以选择一种折中的方法:
通过分割线将整个程序划分为若干个程序块

  1. 程序块内指令可以重排序, 但是程序块之间只能不能重排序
  2. 在程序块内, CPU 不需要和主内存交互, 直接使用自己的本地内存即可, 但是到了分割线处, 必须将执行结果同步到主内存, 同时从主内存读取最新的变量到本地内存

而在这个方法中, Happens-Before 就是定义了这些程序块的分割线。

Alt 'JMM HappensBefore 例子'

如图所示, 这里的 unlock M 和 lock M 就是划分程序的分割线。
在这里, 红色区域和绿色区域的代码内部是可以进行重排序的, 但是 unlock 和 lock 操作是不能与它们进行重排序的。
即第一个图中的红色部分必须要在 unlock M 指令之前全部执行完, 第二个图中的绿色部分必须全部在 lock M 指令之后执行。
并且在 unlock M 指令处, 红色部分的执行结果要全部刷新到主存中, 在 lock M 指令处, 绿色部分用到的变量都要从主存中重新读取。

在程序中加入分割线将其划分成多个程序块, 虽然在程序块内部代码仍然可能被重排序, 但是保证了程序代码在宏观上是有序的。并且可以确保在分割线处, CPU 一定会和主内存进行交互。
Happens-Before 原则就是定义了程序中什么样的代码可以作为分隔线。并且无论是哪条 Happens-Before 原则, 它们所产生分割线的作用都是相同的。

5 总结

JMM, Java Memory Model, 其本身只是一个虚拟机规范, 制定了 Java 虚拟机的内存抽象模式: 本地内存 + 主内存。
双内存模式和系统本身的指令重排序, 在并发编程中都会导致可见性问题, 所以 JMM 对实现者提供了一些规则进行确保程序的准确性。

同时为了减轻使用者对并发编程的要求, 提供了一套 Happens-Before 规则帮助使用者屏蔽底层的实现, 只需要使用者按照 Happens-Before 规则进行编程, 就能保证程序的可见性。

所以, 在整个 JMM 规范中 2 个比较重要的组成

  1. 内存模型的抽象
  2. Happens-Before 的保障

6 参考

《Java并发编程的艺术》
《Java 多线程编程实践实战指南 (核心篇) 》
JMM的介绍
再有人问你Java内存模型是什么, 就把这篇文章发给他。
Java 内存模型详解
java内存模型以及happens-bofore原则
JMM和底层实现原理
大厂很可能会问到的JMM底层实现原理
从Java多线程可见性谈Happens-Before原则

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

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

相关文章

最新Redis7持久化(权威出版)

首先我们要知道什么是持久化:持久化是指将数据保存到磁盘上,以确保在Redis服务器重启时数据不会丢失。 Redis支持两种主要的持久化方式:RDB持久化和AOF持久化 下面让我依次给你介绍一下: RDB持久化 作用 这是将Redis数据保存…

AttributeError: ‘bool‘ object has no attribute ‘sum‘

AttributeError: ‘bool’ object has no attribute ‘sum’ AttributeError: ‘bool’ object has no attribute ‘sum’ 解决方法 将torch.max()改为torch.argmax()查看output和targets的数据类型是否都为tensor 以上就是全部内容&#…

Java基础50题:14. 使用方法求最大值(2种方法)

概述 使用方法求最大值。 创建方法求两个数的最大值max2,随后再写一个求3个数的最大值函数max3。 要求: 在max3这个方法中,调用max2函数,来实现3个数的最大值计算。 方法一 【代码】 public class P14 {public static int max…

【数据结构 — 排序 — 选择排序】

数据结构 — 排序 — 选择排序 一.选择排序1.基本思想2.直接选择排序2.1算法讲解2.2.代码实现2.2.1.函数定义2.2.2.算法接口实现2.2.3.测试代码实现2.2.4.测试展示 3.堆排序3.1.算法讲解3.2.代码实现3.2.1.函数定义3.2.2.算法接口实现3.2.3.测试代码实现3.2.4.测试展示 一.选择…

基于SSM的教师上课系统

末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 目录…

史上最全MySQL各种锁详解

锁详解 锁是计算机协调多个进程或线程并发访问某一资源的机制。 MySQL锁可以按模式分类为:乐观锁与悲观锁。按粒度分可以分为全局锁、表级锁、页级锁、行级锁。按属性可以分为:共享锁、排它锁。按状态分为:意向共享锁、意向排它锁。按算法分…

20道计算机网络面试题

网络分层 1、说说OSI 七层、TCP/IP 四层的关系和区别? OSI 七层从下往上依次是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。一张图给你整明白: TCP/IP 四层从下往上依次是:网络接口层、网络层、传输层、应用…

【工具】JS|浏览器脚本6分钟极速入门 · 开发一个限制自己刷b站的脚本

这张图花里胡哨的是让AI生成的,我觉得怪可爱的,就直接作为封面了。 这篇文章中会开发一个JS脚本,这是一个用来限制b站网页版功能的脚本,避免刷b站的时间过长。功能如下: 除了搜索、视频页、私信页之外的任何页都会被重…

最长连续序列(leetcode 128)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路方法一:排序方法二:哈希表 5.实现示例参考文献 1.问题描述 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 请你…

事务--03---分布式系统唯一ID

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 分布式ID一、什么是分布式系统唯一ID2. 二、分布式系统唯一ID的特点 分布式ID-----实现方案1、使用UUID生成分布式ID2、基于数据库自增ID3、Redis生成ID4、号段模式…

电机:无刷直流电机的原理

一、什么是无刷直流电机 无刷直流电机,英文名称 Brushless DC Motor,简称BLDC; 无刷直流电机的定子是线圈组,而转子是磁铁组,所以不需要用刷子把电流引到定子上,这就是无刷的来历。 电机运动的原理都是依…

[山东大学操作系统课程设计]实验四+实验五

0.写在前面: 为什么这次把两个实验放在一起写了,因为实验五的要求就是在实验四的基础上完成实现的。但是我得实现说明,我的实验四虽然完成了要求,但是无法在我自己的实验四的基础上完成实验五,这是一个很大的问题&…

智能优化算法应用:基于被囊群算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用:基于被囊群算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于被囊群算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.被囊群算法4.实验参数设定5.算法结果6.参考文…

linux权限管理以及shell

1.shell 1.1什么是shell? shell即外壳,是运行在linux系统上的一个脚本语言,包裹在linux内核的外面。我们常说的linux操作系统实际上是linux内核。我们使用的所有指令都是一个个程序,而shell指令就是一个将我们用户的操作翻译给linux内核的程…

layui日历插件

layui日历插件: 在已开源的layui日历插件的基础上的改版(原版插件地址:https://gitee.com/smalldragen/lay-calender-mark)https://gitee.com/tangmaozizi/layui-calendar-plugin.gitjava后台代码并没有把项目完整结构上传上去,因…

小黑子——springBoot基础

springBoot简单学习 一、SpringBoot简介1.1 springBoot快速入门1.1.1 开发步骤1.1.2 对比1.1.3 官网构建工程1.1.3 SpringBoot工程快速启动 1.2 springBoot概述1.2.1 起步依赖I. 探索父工程II. 探索依赖III. 小结 1.2.2 程序启动1.2.3 切换web服务器-jetty 二、配置文件2.1 配置…

Redis权限管理体系(一):客户端名及用户名

在Redis6之前的版本中,因安全认证的主要方式是使用Redis实例的密码进行基础控制,而无法按照不同的应用来源配置不同账号以及更细粒度的操作权限控制来管理。本文先从client list中的信息入手,逐步了解Redis的客户端名设置、用户设置及权限控制…

simulink MATLABFunction模块中实时函数调用函数的使用

样例 function Predyy matlabceshi(input, Time_s) input1 input; Time_s1 Time_s; Predyy ee(input1) mm(Time_s1); end 上面是主要部分,下面是被调用部分 function A ee(input1) A input1 * 100; end function B mm(Time_s1) B Time_s1 * 100; end 模型…

每日一练【盛最多水的容器】

一、题目描述 11. 盛最多水的容器 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明…

Threejs发光闪烁提示特效

一、导语 发光闪烁特效应该在我们的项目中是经常需要去封装的一个特效吧,一般用于点击选择,选中物体,或者一些特效加持于中心物体,物体碰撞检测后的发光特效等等 二、分析 我们可以合理的使用后处理特效,上步骤&am…