Java的NIO工作机制

文章目录

    • 1. 问题引入
    • 2. NIO的工作方式
    • 3. Buffer的工作方式
    • 4. NIO数据访问方式

1. 问题引入

在网络通信中,当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream,并通过这两个对象来交换数据。同时我们也知道网络I/O都是以字节流传输的,当创建Socket对象时,操作系统会为InputStream和OutputStream分别分配一定大小的缓存区,数据的写入都是通过这个缓存区完成的。写入端将数据写到SendQ队列中,当队列填满时,数据将被转移到另一端的InputStream的RecvQ队列中,如果这时RecvQ已经满了,那么OutputStream的write方法将会阻塞直到RecvQ队列有足够的空间容纳SendQ发送的数据。值的注意的是,这个缓存区的大小及写入端的速度和读取端的速度非常影响这个连接的数据传输效率,由于可能发生阻塞,所以网络I/O与磁盘I/O不同的是数据的写入和读取还要有一个协调的过程,如果两边同时传送数据可能会产生死锁。使用NIO可以解决该问题。

死锁产生的根本原因是 客户端和服务端都需要对方释放资源,例如在缓存队列中写入数据。当缓存队列满时,写操作会被阻塞,但同时双方都可能需要对方的缓存队列空间,这样就形成了资源争用的情况

2. NIO的工作方式

  • BIO带来的挑战

BIO即阻塞I/O,不管是磁盘I/O还是网络I/O,数据载写入OutputStream或者从InputStream读取时都可能被阻塞,一旦被阻塞,线程将会失去CPU的使用权,这在当前大规模访问量和有性能要求的情况下时不能被接受的。虽然当前的网络I/O有一些解决方案,如一个客户端一个处理线程,出现阻塞时只能是一个线程阻塞而不会影响其他线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本。单如果当前需要大量的HTTP长连接的情况,线程池可能无法创建那么多线程来保持连接。所以此时我们需要一种新的I/O操作方式。

磁盘IO场景
在磁盘 I/O 中,阻塞通常发生在数据的读取和写入过程中。假设一个应用程序需要从磁盘读取大量数据:

  1. 读取阻塞: 当应用程序发起读取请求时,系统会将请求发送给磁盘驱动器,然后等待磁盘驱动器将数据加载到内存中。在这个过程中,应用程序的线程会被阻塞,直到读取操作完成。如果读取的数据量较大,阻塞时间可能会显著增加。
  2. 写入阻塞: 类似地,当应用程序发起写入请求时,系统会将数据传输到磁盘,然后等待写入操作完成。写入操作的阻塞时间取决于数据的大小和磁盘的性能。如果写入的数据量很大,应用程序可能会长时间地被阻塞。

网络IO场景
在网络 I/O 中,BIO 模型同样存在阻塞问题。考虑一个基于阻塞 I/O 的服务器应用程序,它接受客户端连接并处理数据:

  1. 接受连接阻塞: 当服务器调用accept函数等待客户端连接时,如果没有客户端连接进来,该调用会一直阻塞。在这段时间内,服务器的线程无法执行其他任务,造成资源浪费。
  2. 读取数据阻塞: 在已经建立连接的情况下,当服务器调用read函数等待接收客户端发送的数据时,如果没有数据到达,该调用会一直阻塞。服务器线程被迫等待,可能会导致性能下降。
  3. 写入数据阻塞: 类似地,当服务器调用write函数将数据发送到客户端时,如果客户端接收缓冲区已满,写入操作也会阻塞。这可能使得服务器线程长时间处于等待状态。
  • NIO工作机制
    Java IO和NIO的主要区别在于两者的处理方式不同。Java IO是面向流(Stream)的,它将输入输出数据直接传输到目标设备或文件中,以流的形式进行读写;而NIO则是面向缓冲区(Buffer)的,它将会使用缓存去管理数据,使得读写操作更加快速和灵活。
    在这里插入图片描述

Buffer类

在 Java NIO 中,Buffer 类是一个抽象类,表示一个数据缓冲区,用于在通道(Channel)和原始数据之间进行数据传输。主要的 Buffer 子类包括 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer,它们分别对应不同的基本数据类型。

public abstract class Buffer { //mark 属性和 reset() 方法一起使用,用于在缓冲区中设置一个标记(mark)位置,并在之后通过 reset() 方法将当前位置重置为这个标记位置。mark 属性的作用是用于记录一个位置,以便后续能够回到该位置,方便重新处理或采取其他措施。private int mark = -1;//当前位置的索引private int position = 0;//限制位置的索引,即缓冲区中数据的有效长度private int limit;//缓存区的容量private int capacity;//内存段的代理对象final MemorySegmentProxy segment;//该方法用于设置新的索引位置public Buffer position(int newPosition) {//如果新的索引位置大于数据的有效长度,或小于0,抛出异常if (newPosition > limit | newPosition < 0)throw createPositionException(newPosition);//如果新的索引位置在mark标记之后,那么标记置为-1if (mark > newPosition) mark = -1;//然后将当前索引位置设置为新的索引位置position = newPosition;return this;}//设置新的有效数据长度public Buffer limit(int newLimit) {//如果新的有效数据长度大于缓冲区最大容量或小于0,抛出异常if (newLimit > capacity | newLimit < 0)throw createLimitException(newLimit);//将有效数据长度设置为新的有效数据长度limit = newLimit;//如果当前位置所以大于新的有效数据长度,那么当前位置索引同样设置为有效数据长度if (position > newLimit) position = newLimit;//mark如果大于新的有效数据长度,则直接设置为无效if (mark > newLimit) mark = -1;return this;}//用于标记当前索引位置public Buffer mark() {mark = position;return this;}//将当前索引位置重置为mark标记所在的位置public Buffer reset() {int m = mark;if (m < 0)throw new InvalidMarkException();position = m;return this;}//清空缓存区数据,实际上就是重置了mark、position和limitpublic Buffer clear() {position = 0;limit = capacity;mark = -1;return this;}//flip() 方法可以将缓冲区从写模式切换到读模式。调用 flip() 后,位置被设置为0,限制被设置为之前的位置,用于准备读取缓冲区中的数据。public Buffer flip() {//limit有效数据长度为当前索引所在位置limit = position;//postion设置为0,表示从0开始读取position = 0;mark = -1;return this;}//将缓冲区的位置设置为 0,限制保持不变,用于重新读取缓冲区中的数据,类似于 flip() 但不改变限制。public Buffer rewind() {position = 0;mark = -1;return this;}//获取buffer中剩余的有效容量public final int remaining() {int rem = limit - position;return rem > 0 ? rem : 0;}}

Channel类
Java NIO 中,Channel 是一个接口,它提供了用于读取和写入数据的统一的 API。Channel 接口是 NIO 用于与 I/O 设备(如文件、套接字、选择器等)交互的核心部分。Channel 接口的实现类可以包括文件通道、套接字通道等。

public interface Channel extends Closeable {
//判断通道是否打开public boolean isOpen();//关闭通道public void close() throws IOException;
}

Seletionkey类
SelectionKey 类是 Java NIO 中的关键类,它用于表示注册到 Selector 上的通道和对应的事件。SelectionKey 对象是 Selector 与通道之间的桥梁,它包含了与通道相关的一些信息,以及通道所感兴趣的事件,以便在选择器上进行有效的事件选择。

选择键则是一种将通道和选择器进行关联的机制。

public abstract class SelectionKey {
//SelectableChannel 是 Java NIO 中表示支持非阻塞模式的通道的抽象类,该返回返回一个通道public abstract SelectableChannel channel();//该方法返回一个选择器public abstract Selector selector();//获取通道所感兴趣的操作集合(事件集合),返回一个位掩码,用于表示关注的事件public abstract int interestOps();//读操作的位掩码,用于表示通道已经准备好进行读操作。public static final int OP_READ = 1 << 0;//写操作的位掩码,用于表示通道已经准备好进行写操作。public static final int OP_WRITE = 1 << 2;//连接操作的位掩码, 用于表示连接已经建立public static final int OP_CONNECT = 1 << 3;//accept操作的位掩码, 用于表示通道已经准备好接受新的连接。public static final int OP_ACCEPT = 1 << 4;//返回通道现在是否可读public final boolean isReadable() {return (readyOps() & OP_READ) != 0;}//通道现在是否可写public final boolean isWritable() {return (readyOps() & OP_WRITE) != 0;}//通道是否已经连接就绪public final boolean isConnectable() {return (readyOps() & OP_CONNECT) != 0;}//通道是否可以接受新的连接public final boolean isAcceptable() {return (readyOps() & OP_ACCEPT) != 0;}//将指定的对象附加到此键public final Object attach(Object ob) {return ATTACHMENT.getAndSet(this, ob);}}

Selector类

Selector 是 Java NIO 中的一个关键类,用于实现非阻塞 I/O 操作的多路复用。通过 Selector,可以在单个线程上同时监控多个通道的事件,从而实现高效的事件驱动编程模型。

public abstract class Selector implements Closeable {//返回新创建的选择器实例public static Selector open() throws IOException {return SelectorProvider.provider().openSelector();}//判断selector是否已经打开public abstract boolean isOpen();//SelectorProvider 是一个抽象类,用于提供 Selector 和 Channel 的创建。每个 Selector 都与一个 SelectorProvider 实例关联,而SelectorProvider 实例的具体实现是由具体的操作系统提供的。public abstract SelectorProvider provider();//获取选择器上所有的键集public abstract Set<SelectionKey> keys();//关闭选择器public abstract void close() throws IOException;// 阻塞,直到至少有一个通道在选择器上准备好进行 I/O 操作,或者调用线程被中断。返回已经准备就绪的通道的数量。public abstract int select() throws IOException;//非阻塞地检查是否有通道准备好进行 I/O 操作。返回已经准备就绪的通道的数量。public abstract int selectNow() throws IOException;//唤醒因为调用 select 或 selectNow 方法而处于阻塞状态的线程。返回调用 wakeup 方法的选择器。public abstract Selector wakeup();
}

关键类就是Channel和Selector,它们是NIO中的两个核心概念。Channel 通过 register 方法可以注册到 Selector 中,以实现非阻塞 I/O 操作。Channel 和 Buffer 之间通过 read 和 write 方法进行数据的传输。数据首先被写入到缓冲区,然后从缓冲区读取到通道或从通道读取到缓冲区。Channel 可以通过 register 方法注册到 Selector 上,注册时需要指定感兴趣的事件,例如读、写等。SelectionKey 对象表示了一个通道在一个选择器上的注册信息,它与通道和选择器之间建立了关联。SelectionKey 包含了通道、选择器、感兴趣的操作集合(事件集合)、附件等信息。
在这里插入图片描述
下面看看NIO是如何工作的:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class NioServer {public static void main(String[] args) {try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open() //创建一个通道) {//将通道绑定到一个指定的端口,ServerSocketChannel使用选择器(Selector)来管理多个通道,可以在一个线程中处理多个通道的连接请求。serverSocketChannel.bind(new InetSocketAddress(8080));
//将通道设置为非阻塞模式serverSocketChannel.configureBlocking(false);//创建一个选择器Selector selector = Selector.open();//将通道注册到选择器中,状态是等待接受客户端连接serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("NIO Server started on port 8080");while (true) {//自旋获取准备好的通道int readyChannels = selector.select();if (readyChannels == 0) {continue;}//获取所有的键集Set<SelectionKey> selectedKeys = selector.selectedKeys();//迭代键集Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();//处理相应事件if (key.isAcceptable()) {handleAcceptEvent(key, selector);} else if (key.isReadable()) {handleReadEvent(key);}keyIterator.remove();}}} catch (IOException e) {e.printStackTrace();}}private static void handleAcceptEvent(SelectionKey key, Selector selector) throws IOException {//获取键所关联的通道ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();//准备接受连接SocketChannel clientChannel = serverSocketChannel.accept();//设置通道模式为非阻塞clientChannel.configureBlocking(false);//通道注册到选择器上,状态为可以读取数据clientChannel.register(selector, SelectionKey.OP_READ);System.out.println("Accepted connection from " + clientChannel);}private static void handleReadEvent(SelectionKey key) throws IOException {SocketChannel clientChannel = (SocketChannel) key.channel();//分配缓存区ByteBuffer buffer = ByteBuffer.allocate(1024);//通道读取int bytesRead = clientChannel.read(buffer);if (bytesRead == -1) {System.out.println("Connection closed by client: " + clientChannel);clientChannel.close();} else if (bytesRead > 0) {buffer.flip();clientChannel.write(buffer);}}
}

上面代码用NIO实现了一个简单的服务器

调用Selector的静态工厂方法创建一个选择器(这个静态工厂是操作系统底层实现的)。然后创建一个服务端Channel,绑定到一个Socket对象,并把这个通道注册到选择器上,把这个通道设置为非阻塞模式,然后就可以调用Selector的selectedKeys方法检查已经注册在这个选择器上的所有通信信道是否有需要事件发生,从而可以读取通信的数据,而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲区。在上面这段程序中,将server的监听连接请求和事件吹了放在一个线程中,但是在事件应用中,我们通常会放在两个线程中,一个线程专门监听客户端的连接请求,而且是以阻塞的方式执行的;另一个线程专门负责处理请求,这个专门处理请求的线程才会真正采用NIO的方式。

下图展示量NIO的工作方式,Selector可以监听一组通信信道上的I/O状态,前提是这个Selector已经注册到这些通信通道中,选择器Selector可以调用select()方法检查已经注册的通信信道上I/O是否已经准备好,如果没有至少一个信道I/O状态发生变化,那么select方法会阻塞等待或在超时时间后返回0。如果有多个信道有数据,那么将会把这些数据分配到对应的数据Buffer中。所以关键的地方是,有一个线程来处理所有连接的数据交互,每个链接的数据交互不是阻塞方式的,所以可以同时处理大量的连接请求。
在这里插入图片描述

  • 总结

NIO模式下客户端和服务端通信流程对比如下:

1.服务端初始化:

服务端创建一个 ServerSocketChannel,并绑定到一个特定的端口。将 ServerSocketChannel 设置为非阻塞模式,并注册到一个 Selector 上,以监听连接事件。

2.客户端初始化

客户端创建一个 SocketChannel,连接到服务端的地址。将 SocketChannel 设置为非阻塞模式,并注册到一个 Selector 上,以监听连接事件。

3.事件循环

服务端和客户端都进入一个事件循环,不断地检查发生的事件。在服务端,可能会检查 OP_ACCEPT 事件,表示有新的连接请求。在客户端,可能会检查 OP_CONNECT 事件,表示连接已建立。(这里如果一直没有客户端访问程序陷入空转并不会阻塞,这样也就避免了内核切换)

4.处理连接事件

在服务端,当有新的连接请求到达时,通过 ServerSocketChannel.accept() 接受连接,并将新的 SocketChannel 注册到 Selector 上监听读事件。

5.客户端处理连接事件

在客户端,当连接建立完成时,通过 SocketChannel.finishConnect() 完成连接,然后注册到 Selector 上监听读事件

6.处理读事件

在服务端和客户端,当通道可读时,从通道中读取数据

这样,通过 NIO,服务端和客户端可以通过非阻塞的方式处理多个连接,并在一个事件循环中实现数据的读取和写入。在实际应用中,可能需要结合多线程、线程池等机制,以更好地处理多个连接的并发处理。

3. Buffer的工作方式

通过前面分析源码,我们也大致知道了Buffer的工作方式,Buffer可以简单理解为一组基本数据类型的元素列表,它通过介个变量来保持这个数据当前的位置状态,也就是有四个索引:

在这里插入图片描述

我们通过ByteBuffer.allocate(11)方法创建了一个11个byte的数组缓冲区,初始状态如下所示,position为0,capacity和limit都是数组默认长度。
在这里插入图片描述
当我们写入五个数据后位置如下所示。

在这里插入图片描述
调用flip方法,数组切换状态为读状态:
在这里插入图片描述
此时底层操作系统可以从缓存区中正确读取这5个字节数据并发送出去了,在下一次写数据之前我们再调用一下clear方法,缓冲区又回回到默认位置。mark标记就是记录当前position的前一个位置,我们调用reset时,position会恢复到mark位置。

通过Channel获取的I/O数据首先要经过操作系统的Socket缓冲区缓冲区再讲数据复制到Buffer中,从操作系统缓冲区到用户缓冲区比较消耗性能,Buffer提供了另外一种直接操作操作系统缓冲区的方法,即ByteBuffer.allocateDirector(size);,该方法返回与底层存储空间关联的缓冲区,它通过Native代码操作非JVM堆的内存空间,每次创建和释放的时候都会调用一次System.gc()。

4. NIO数据访问方式

我们知道当我们通过IO读取磁盘数据时,需要使用操作系统的系统调用方法,而这会涉及用户态到内核态的切换,操作系统需要把数据读取到内核态空间,然后将数据送到用户态,这是十分重的一个操作,NIO提供了两个优化方法:

  • FIleChannel.transferXXX

减少数据从内核到用户空间中的复制,数据直接在哪和空间中移动,下图首先是传统数据访问方式,然后是tansferXXX方式:

在这里插入图片描述
在这里插入图片描述

  • FileChannel.map

它将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时,直接操作这个文件数据,这种方式就直接跳过了数据从内核空间向用户空间复制。

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

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

相关文章

企业IT安全:内部威胁检测和缓解

什么是内部威胁 内部威胁是指由组织内部的某个人造成的威胁&#xff0c;他们可能会造成损害或窃取数据以谋取自己的经济利益&#xff0c;造成这种威胁的主要原因是心怀不满的员工。 任何内部人员&#xff0c;无论是员工、前雇员、承包商、第三方供应商还是业务合作伙伴&#…

SSL证书HTTPS保护服务

SSL证书属于数字证书的其中一种&#xff0c;广泛用于https协议&#xff0c;从而可以让数据传输在加密前提下完成&#xff0c;确保HTTPS网络安全是申请SSL证书必要工作。 SSL证书是主要用于https是一种加密协议&#xff0c;仔细观察网站地址会发现目前主流的网址前面都会有http…

【玩转TableAgent数据智能分析】利用TableAgent进行教育数据分析

文章目录 前言九章云极&#xff08;DataCanvas&#xff09;介绍前期准备样例数据集体验1. 样例数据集-Airbnb民宿价格&评价 体验1.1 体验一1.2 体验二 教育数据的分析&#xff08;TableAgent&ChatGLM对比&#xff09;1. 上传文件2. 数据分析与对比2.1 分析一2.1.1 Tabl…

web服务器之——建立两个基于ip地址访问的网站

目录 准备工作&#xff1a;web服务器搭建 第一步&#xff1a;挂载 第二步&#xff1a;编辑配置文件 第三步&#xff1a;安装软件包 第四步&#xff1a;启动httpd 查看配置文件&#xff1a; 第五步&#xff1a;设置防火墙状态&#xff1a; 重启服务: 查看状态&#xff1…

Leetcode—2961.双模幂运算【中等】

2023每日刷题&#xff08;五十六&#xff09; Leetcode—2961.双模幂运算 实现代码 class Solution { public:int func(int a, int b) {int ans 1;for(int i 0; i < b; i) {ans * a;ans % 10;}return ans;}int func2(int a, int b, int m) {int ans 1;for(int i 0; i …

评论送书:以企业架构为中心的SABOE数字化转型五环法

01 传统企业数字化转型面临诸多挑战 即将过去的2023年&#xff0c;chatGPT大模型、数据资产入表等事件的发生&#xff0c;标志着数字经济正在加速发展。数字经济是人类社会继农业经济、工业经济之后的第三种经济形态&#xff0c;将推动生产方式、生活方式和治理方式深刻变革&a…

2-Spring

2-Spring 文章目录 2-Spring项目源码地址Spring概述Spring特点&#xff08;优点&#xff09;Spring相关学习网站基于Maven的Spring框架导入Spring的组成及拓展 Spring-IOC--原型理解IOC-原型--示例开发示例-常规开发示例-Set函数&#xff08;IOC原型&#xff09;开发示例-对比思…

C++STL的list(超详解)

文章目录 前言构造函数capacitylist的访问insertswapsort 前言 看一下list, 在任意位置可以进行O(1)插入删除的操作。 它怎么实现这个东西&#xff1f;它其实就是一个带头双向循环链表。 #成员函数 构造函数 这里面的构造函数学完string和vector之后已经相当熟悉了。 capaci…

如何将用户有过行为的item用list形式记录下来,另外如何计算list里的个数

导语&#xff1a; 最近做项目&#xff0c;发现有些语法想一想是知道&#xff0c;但实际操作起来跟想的情况不一样哈哈。不是遇见bug就是输出的结果不是自己想要的&#xff0c;CSDN跟知乎找了很多没怎么解决&#xff0c;后面多摸索多实操终于解决&#xff01; test_data[item_…

数据结构-05-跳表SkipList

1-什么是跳表 跳表SkipList是一种随机化的数据结构&#xff0c;基于并联的链表&#xff0c;实现简单&#xff0c;插入、删除、查找的复杂度均为 O(logN)&#xff08;大多数情况下&#xff0c;因为是实现上是概率问题&#xff09;&#xff0c;因为其性能匹敌红黑树且实现较为简单…

msvcr110.dll丢失的解决方法有哪些-常见方法教程

我们在日常使用电脑中经常遇到各种问题&#xff0c;比如系统文件丢失是最常见的&#xff0c;其中msvcr110.dll丢失也是非常常见的问题&#xff0c;那么msvcr110.dll文件为什么会丢失&#xff0c;丢失对电脑有什么影响呢&#xff0c;丢失了有什么解决方法&#xff1f;今天小编就…

Synchronized 优化

目录 前言 重点 一、 轻量级锁 二、锁膨胀 三、重量锁 四、偏向锁 五、其他优化 我的其他博客 前言 Java synchronized 是一种机制&#xff0c;可以保证多个线程在访问共享资源时的同步性。synchronized 关键字可以用于方法或代码块上&#xff0c;当一个线程获取了这个对…

【动态规划】03斐波那契数列模型_最小花费爬楼梯_C++(easy1)

题目链接&#xff1a;leetcode使用最小花费爬楼梯 目录 题目解析&#xff1a; 算法原理 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 编写代码 题目解析&#xff1a; 题目让我们求达到楼梯顶部的最低花费. 由题可得&#xff1a; cost[i] 是从楼梯第 i 个…

第6章:知识建模:概述、方法、实例

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

java--LinkedHashSet集合的底层原理和TreeSet集合

1.LinkedHashSet底层原理 ①依然是基于哈希表(数组、链表、红黑树)实现的 ②但是&#xff0c;它的每个元素都额外的多了一个双链表的机制记录它前后元素的位置。 2.TreeSet ①特点&#xff1a;不重复、无索引、可排序(默认升序排序&#xff0c;按照元素的大小&#xff0c;由…

Nacos热更新(动态获取配置)

写在前面&#xff1a;各位看到此博客的小伙伴&#xff0c;如有不对的地方请及时通过私信我或者评论此博客的方式指出&#xff0c;以免误人子弟。多谢&#xff01;如果我的博客对你有帮助&#xff0c;欢迎进行评论✏️✏️、点赞&#x1f44d;&#x1f44d;、收藏⭐️⭐️&#…

el-date-picker 限制选择范围最大为一年,设置快捷选项,设置默认时间

el-date-picker 限制选择范围最大为一年&#xff1a; 主要代码为&#xff1a;:picker-options"pickerOptions" 以及 blur"pickerBlur" <el-date-pickerv-model"transactionTime"type"daterange"style"width: 200px"size…

盒马补贴量价-2021KDD

概述&#xff1a; 电商商品定价三个关键问题&#xff1a; 在只有观测数据的时候&#xff0c;怎么构建价格弹性&#xff0c;现在来看这就是一个反事实推断的问题&#xff0c;不仅是如何做的问题&#xff0c;还有如何评估的问题。长周期的规划决策问题怎么建模 & 求解&#…

从零开始学UniApp微信小程序开发:头部适配技巧让你事半功倍!

在 UniApp 中&#xff0c;在微信小程序开发中&#xff0c;头部适配可以通过修改 pages.json 中的 navigationStyle 配置项来实现&#xff0c;具体操作步骤如下&#xff1a; 1.进入 pages.json 文件 在 UniApp 项目的根目录中找到 pages.json 文件&#xff0c;打开该文件。 2…

Win11专业版,eNSP启动失败,错误代码40 解决方法

微软Win11系统默认开启的 Virtualization-based Security &#xff08;VBS&#xff09;“基于虚拟化的安全性”会导致游戏、跑分性能下降。VBS 基于虚拟化的安全性&#xff0c;通常称为内核隔离。使用硬件虚拟化在内存中创建安全区域&#xff0c;为其他安全功能提供了一个安全平…