当前位置: 首页 > news >正文

Netty应用(四) 之 Reactor模型 零拷贝

目录

6.Reactor模型

6.1 单线程Reactor

6.2 主从多线程Reactor (主--->Boss | 从--->Worker | 一主多从机制)

7.扩展与补充

8.Reactor模型的实现

8.1 多线程Reactor模型的实现(一个Boss线程,一个Worker线程)

8.2 多线程Reactor模型的实现(一个Boss线程,多个Worker线程)

9.零拷贝

10.硬件驱动程序与软件驱动程序(数据库驱动程序)的关系

11.linux内核与centos,redhat等这些产品有啥区别和关系?

6.Reactor模型

6.1 单线程Reactor

6.2 主从多线程Reactor (主--->Boss | 从--->Worker | 一主多从机制)

但是单线程版的Reactor,一定存在性能瓶颈的!你想想一个线程做了所有的连接,读,写的操作,肯定存在极大的性能瓶颈。

所以后续引入了主从多线程Reactor模式:

1.Boss主线程负责监听并且处理客户端的连接事件。它具有一个独立的Selector监控器,所有的客户端都会注册存储到该Selector上,这样才能完成监听连接事件的操作。

2.当Boss主线程监听并处理完客户端的连接事件后,它会把这个客户端交给Worker从线程进行处理,从线程肯定会先把该客户端像主线程一样先注册存储到当前从线程所对应的Selector上。从线程会进行监听与处理该客户端后续可能会进行的读或写的事件。从线程可能会有多个,每一个从线程都具有一个独立的Selector。

补充:

1.Boss主线程与Worker从线程们之间一定是建立了联系的,这样主线程才能把客户端交给从线程。每一个线程都具有一个独立的Selector

2.分配多少个线程给Boss或Worker?分配一个线程给Boss,剩余(Cpu核数-1)个线程分配给Worker从线程。

先说为什么一共分配CPU核数的线程?为了避免频繁的线程上下文切换,为了实现真正的并行。后续肯定还可以引入线程池的优化

为什么Worker线程分配的多?因为客户端连接就一次,后续Boss把客户端甩手给Worker后,客户端的读写事件就不再让Boss去处理了,读写事件可是远比连接事件多的!

如图所示:

7.扩展与补充

扩展:

主从架构与主备架构的区别:

主从架构:

主节点也干活,从节点也干活。只是二者干的活不一样,主节点一般做写操作(主要的工作),从节点一般做次要的工作 如 读操作。缺点:主节点一旦宕机,就废掉了。

主备架构:

主节点会同步数据到另外一共备用主节点,当主节点宕机时,会切换到另外一台时刻同步数据的备用主节点。解决了主从架构的缺点。

主备架构:MySQL的双主架构,Redis的哨兵集群

如何实现主备架构中,主节点同步数据到另外一台备用主节点呢?

使用zookeeper集群这一CP原理架构,遵循CP的是强一致性的架构,即:当zookeeper集群节点出现宕机时,需要重新选举节点作为主节点时,拒绝对外提供服务,此时就保证了强一致性。但是降低了性能,这就是CAP原则,不可以同时满足C A P。

当然,zookeeper可以替换为consul,etcd

反向代理与正向代理:

反向代理时针对于服务器端:

1.做负载均衡

2.URL重写 :面对写入的URL重写

3.动静分离

新的问题:当客户端连接这个服务器集群时,正在连接的服务器宕机了,你怎么做到客户端无感的?客户端socket连接服务器时,需要通过服务器的ip连接,如果当前这台服务器节点宕机,你不是要更新此时连接的ip?这怎么可能做到无感啊,但通过以下技术就是可以做到无感。。。

方法1:VIP虚拟IP

其实都不需要更新ip,在过去传统的开发中,使用的是Virtual IP 虚拟IP。客户端访问的是虚拟IP对应的服务,虚拟IP这个服务再找到具体的节点服务器资源,虚拟IP服务器肯定会涉及到分发,保活,注册服务器节点等,但是这一切对于客户端来说,是无感知的,它是不知道服务器节点的切换的。

这些都可以通过早期的一些技术完成:如lvs,keepavlid。

方法2:IP飘逸

当发现节点服务器坏了,那么偷偷的把这台服务器节点ip给复制给另外一台服务器节点,这就是ip飘逸,这样也做到了客户端无感知!

解决技术:HAproxy

方法3:云原生OPS自动化运维

这些技术实际上都是运维的技术,现如今微服务为什么说是云原生的一个分支,因为云原生主张OPS,自动化运维,就是要取代运维!

抛开上述三种运维方式,仅仅是站在java层面去开发一个程序去实现这一客户端无感:

其实思路很简单,就是做一个redis缓存,缓存的是ip-主机节点的ip映射集合。

然后在client与server端分别开一个agent,agent之间做网络通信,然后agent再与client或server端内部做交互传递数据。

8.Reactor模型的实现

8.1 多线程Reactor模型的实现(一个Boss线程,一个Worker线程)

Reactor模型:多个客户端连接同一服务端。在服务端做了Reactor模型的编程逻辑。

通过一个Boss线程(可以有多个,看具体业务),处理多个NIO的接收连接的操作,也就是ServerSocketChannel的ACCEPT事件

通过一个worker线程(可以有多个,看具体业务),处理读写的操作,也就是SocketChannel的READ或WRITE事件。

重点:你处理的read或write事件一定是前面对应这次连接的boss线程的连接得来的,你不能前后没联系。是一种传递的关系。但是每一个线程,无论是boss还是worker,都具有独立的Selector监管器

  • 第一版本代码
package com.messi.netty_basic_01.Reactor_prac;import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class ReactorBossServer2 {public static void main(String[] args) throws Exception{ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8000));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector,SelectionKey.OP_READ);//TODO 调用Worker线程去处理读写事件~~~~}}}}
}

对于TODO位置,我们要进行调用worker线程去进行处理读写事件。但是具体要开启多少个worker线程呢?是过来一个客户端连接就需要开启一个worker线程去处理该连接对应的读写事件吗?

当然不是,在之前,我们使用一个线程处理所有注册在复用器上的channel对象对应的所有事件,这里我们使用Boss线程处理连接事件。worker线程处理读写事件,但是也没必要一个线程对应一个客户端连接的后续读写事件啊,如果线程过大,内存占用过大,线程之间切换也会更加频繁,性能很低的。

下面暂且把worker线程设为开启一个,那么我们需要把创建Worker的代码设为公共的,而不能每过来一个客户端连接后,就进行创建一份新的Worker。具体见下面代码

  • 第二版代码
package com.messi.netty_basic_01.Reactor_prac;import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class ReactorBossServer2 {public static void main(String[] args) throws Exception{ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8000));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//放在外面,做一个共享变量Worker2 worker2 = new Worker2();while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector,SelectionKey.OP_READ);//TODO 调用Worker线程去处理读写事件~~~~worker2.register(socketChannel);}}}}
}

  • 第三版代码
package com.messi.netty_basic_01.Reactor_prac;import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class ReactorBossServer2 {public static void main(String[] args) throws Exception{ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8000));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//放在外面,做一个共享变量Worker2 worker2 = new Worker2("worker-01");while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector,SelectionKey.OP_READ);//TODO 调用Worker线程去处理读写事件~~~~worker2.register(socketChannel);}}}}
}
package com.messi.netty_basic_01.Reactor_prac;import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;public class Worker2 implements Runnable{private String name;private Thread thread ;private Selector selector;private volatile boolean isCreated;public Worker2(String name) {this.name = name;}public void register(SocketChannel socketChannel) {if (!isCreated) {this.thread = new Thread(this,name);thread.start();selector = Selector.open();isCreated = true;}}@Overridepublic void run() {}
}

为什么Worker2线程类构造方法就一个参数name?

对于其他属性,thread,selector的创建,完全是可以在Worker2内部完成的。woker2线程本身就是Boss线程调用的,Boss线程只初始化worker线程的name,这样解耦合,降低代码的侵入性。你想想,并发场景下,在Boss线程创建多个Thread或Selector,这样是不是Boss线程端代码太难看,造成代码的极大污染。

为什么把thread,selector的初始化创建放在register方法中?

其实也是可以放在构造方法中的。

为什么要使用isCreated标记位?

保证thread线程创建和启动只进行一次!使用标志位isCreated的方式在register中进行初始化。使用标记位isCreated一定要记住:isCreated在完成一次初始化后,要置为true!如果不使用isCreated,boss线程在触发连接事件后调用worker的register方法。但此时还是main线程在执行register方法。那么boss线程触发一次客户端连接事件,就会对应创建一个新线程来处理该客户端连接对应后续的读写事件。

补充:

在多线程并发共享isCreated这个变量时,要加上volatile关键字,volatile可以保证当其中一个线程修改isCreated的值的时候,会及时通知告知其他线程,"isCreated的最新值为xxx"。保证了isCreated的可见性,因为标记位通常做if判断,对可见性要求高。

但此时boss为单线程,并且由于是迭代器一个个遍历触发的事件的,所以同一时刻只有一个boss线程调用register方法,所以isCreated并不存在多线程共享的并发安全问题。养成良好习惯,说不准后续boss变为多线程了。。。

worker2.register(socketChannel)接着调用:

为什么出现空指针异常?

  • 第四版代码
package com.messi.netty_basic_01.Reactor_prac;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class ReactorBossServer2 {private static final Logger log = LoggerFactory.getLogger(ReactorBossServer2.class);public static void main(String[] args) throws Exception{log.debug("boss线程开启 >>>");ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8000));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//放在外面,做一个共享变量Worker2 worker2 = new Worker2("worker-01");while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector,SelectionKey.OP_READ);//TODO 调用Worker线程去处理读写事件~~~~log.debug("boss线程即将要调用Worker2类的register方法 >>>");worker2.register(socketChannel);log.debug("boss线程完成调用Worker2类的register方法 >>>");}}}}
}
package com.messi.netty_basic_01.Reactor_prac;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;public class Worker2 implements Runnable{private static final Logger log = LoggerFactory.getLogger(Worker2.class);private String name;private Thread thread ;private Selector selector;private volatile boolean isCreated;public Worker2(String name) {this.name = name;}public void register(SocketChannel socketChannel) throws Exception{if (!isCreated) {selector = Selector.open();this.thread = new Thread(this,name);thread.start();isCreated = true;}log.debug("boss线程将要执行socketChannel#register方法 >>>");socketChannel.register(selector, SelectionKey.OP_READ);}@Overridepublic void run() {log.debug("开启一个新线程worker-01 >>>");while (true) {try {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(30);int read = channel.read(buffer);if (read == -1) {log.debug("客户端断开 >>>");key.cancel();} else {buffer.flip();CharBuffer decode = Charset.forName("UTF-8").decode(buffer);log.info("worker-01线程读取的结果为:{}",decode.toString());}}}} catch (IOException e) {e.printStackTrace();}}}
}
package com.messi.netty_basic_01.Reactor_prac;import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;public class MyClient2 {public static void main(String[] args) throws Exception{SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress(8000));socketChannel.write(Charset.forName("UTF-8").encode("梅西"));System.out.println("MyClient2.main");}}

测试:

  • 但是代码在多线程场景下,存在bug:因为多线程环境下,一切皆有可能,如果新开启的线程执行的run方法(selector.select())在register方法之前执行。由于selector阻塞等待,导致register注册不上。最终就一直阻塞等待着,就死掉了。。。。。下面详细分析一下:

补充一点:

为什么创建一个线程后thread.start()不会立即执行?

因为真正创建出一个线程并且切换都是需要一定时间的,最重要的是当前main线程的时间片可能还没使用完,所以CPU是不会轻易切换线程的,但是在多线程环境下一切是皆有可能,可能start后就切换到该新线程了。但是当main线程时间片耗尽后,就一定会切换到新创建的线程的,即调用run方法中的业务逻辑。通常main线程时间片耗尽的原因是因为执行的业务线复杂导致时间片耗尽,这里我们可以模拟一下,Thread.sleep(2000)休眠2s,此时main线程时间片耗尽,然后就完成了线程切换,线程切换到新创建的线程,并且执行run方法!

但就是因为先执行了run方法,后执行了register方法,代码就出现了bug!

出现bug的原因症结就是因为:run方法(selector.select())在register方法之前执行。由于selector阻塞等待,导致register注册不上。但是在多线程环境下,一切皆有可能,所以你无法保证run方法(selector.select())一定在register方法之后执行!说白了,就是因为register方法和run方法(select()方法调用)二者不是在同一线程中,你无法百分百做到控制方法的执行顺序。

演示一下这个bug:

睡眠模拟bug》》》》

断点设为Thread级别,当前线程挂起时只挂自己。如果设为ALL,那么当前线程挂起时,会挂起所有的线程

debug测试:

切换到main线程,往下执行,发现也阻塞了,阻塞在register这句代码:

分析如下:

解决方法1:

搞一个同步锁,把selector.wakeup()和register方法的调用搞一块:

为什么要搞一个同步锁,为何多此一举呢?

搞同步锁为的就是保证wakeup唤醒和register注册之间没有其他情况的发生,不然不可能保证有意外的发生,如下:

但是同步锁依旧存在很大问题,其实问题就是在于:性能低下。所以后续我们模仿Netty做一个队列,把要执行的代码传递给新开启的线程中,让selector.select()和register方法在同一个线程中执行调用!!并且加一个wakeup做兜底操作,就完美的解决了这个问题。其实就是为了保证在selector.select阻塞selector之前完成register注册READ事件的操作,如果selector因为多线程环境下的因素导致在register方法之前阻塞(一切皆有可能),所以使用wakeup方法做兜底,wakeup像做了一个标记,无论在wakeup前还是后做阻塞了,都可以唤醒一次!

解决方法2:

所以最终解决方案:模仿Netty底层做一个并发队列,把调用register的代码当作一个任务存储到该并发队列中,然后在run方法对应的线程中,再把该队列中存储的register调用代码取出来,这样就保证了register方法和selector.select()在同一个线程中调用。那么你就可以控制先后顺序了。最后再补充一个selector.wakeup()的唤醒作为兜底操作。当selector.select()再次阻塞时,直接唤醒,然后就可以接着在同一线程中执行register方法啦。

具体如下:

ConcurrentLinkedQueue队列可以在两个线程之间进行传递一些代码,功能,知道这些就够了。

测试:

多线程环境下,一切皆有可能,因为你也不知道走到哪一句代码CPU就调度执行其他线程了,所以要考虑更多的情况,然后保证并发安全。

  • 第五版代码(最终代码)
package com.messi.netty_basic_01.Reactor_prac;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class ReactorBossServer2 {private static final Logger log = LoggerFactory.getLogger(ReactorBossServer2.class);public static void main(String[] args) throws Exception{log.debug("boss线程开启 >>>");ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8000));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//放在外面,做一个共享变量Worker2 worker2 = new Worker2("worker-01");while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {ServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector,SelectionKey.OP_READ);//TODO 调用Worker线程去处理读写事件~~~~log.debug("boss线程即将要调用Worker2类的register方法 >>>");worker2.register(socketChannel);log.debug("boss线程完成调用Worker2类的register方法 >>>");}}}}
}
package com.messi.netty_basic_01.Reactor_prac;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;public class Worker2 implements Runnable{private static final Logger log = LoggerFactory.getLogger(Worker2.class);private String name;private Thread thread ;private Selector selector;private volatile boolean isCreated;public Worker2(String name) {this.name = name;}private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();public void register(SocketChannel socketChannel) throws Exception{if (!isCreated) {selector = Selector.open();this.thread = new Thread(this,name);thread.start();isCreated = true;}log.debug("boss线程将要执行socketChannel#register方法 >>>");//使用睡眠的方式模拟出:在register方法执行前执行了繁重的业务代码逻辑Thread.sleep(2000);log.debug("main线程睡眠 >>>");
//        synchronized (this) {
//            selector.wakeup();
//            socketChannel.register(selector, SelectionKey.OP_READ);
//        }queue.add(()->{try {socketChannel.register(selector,SelectionKey.OP_READ);} catch (ClosedChannelException e) {throw new RuntimeException(e);}});selector.wakeup();}@Overridepublic void run() {while (true) {log.debug("开启一个新线程worker-01 >>>");try {selector.select();Runnable poll = queue.poll();if (poll != null) {poll.run();}Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(30);int read = channel.read(buffer);if (read == -1) {log.debug("客户端断开 >>>");key.cancel();} else {buffer.flip();CharBuffer decode = Charset.forName("UTF-8").decode(buffer);log.info("worker-01线程读取的结果为:{}",decode.toString());}}}} catch (IOException e) {e.printStackTrace();}}}
}
package com.messi.netty_basic_01.Reactor_prac;import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;public class MyClient2 {public static void main(String[] args) throws Exception{SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress(8000));socketChannel.write(Charset.forName("UTF-8").encode("梅西"));System.out.println("MyClient2.main");}}

8.2 多线程Reactor模型的实现(一个Boss线程,多个Worker线程)

  • 相比之前的代码,我们要做的就是增加Worker线程的数量 然后随机选举出一个Worker线程进行register

  • 代码
package com.messi.netty_basic_01.reactor;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.net.InetSocketAddress;
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.concurrent.atomic.AtomicInteger;public class ReactorBossServer {private static final Logger log = LoggerFactory.getLogger(ReactorBossServer.class);public static void main(String[] args) throws Exception{log.debug("boss线程开启 >>>");//建立ServerSocketChannelServerSocketChannel ssc = ServerSocketChannel.open();//设置为非阻塞ssc.configureBlocking(false);//监听端口8000ssc.bind(new InetSocketAddress(8000));//创建Selector监管者Selector selector = Selector.open();//把ServerSocketChannel注册到Selector监管者上的keys这一HashSet中,对应注册事件为ACCEPTssc.register(selector, SelectionKey.OP_ACCEPT);//        Worker worker = new Worker("worker-01");//模拟多线程的情况,一个Boss线程,多个Worker线程。在实际开发中,要使用线程池Worker[] workers = new Worker[2];//模拟出多个Worker线程for (int i = 0; i < workers.length; i++) {workers[i] = new Worker("worker - " + i);}//原子操作的变量AtomicInteger index = new AtomicInteger();while (true) {//轮询selector.select();//得到Selector上注册的Channel对应的所有事件 ---》Boss(main)线程对应注册的只有ACCEPT事件,Worker线程处理READ,WRITE事件Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//遍历每个事件while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {//拿到的肯定是ACCEPT事件对应的ServerSocketChannelServerSocketChannel channel = (ServerSocketChannel) key.channel();//accept方法接收得到SocketChannel 这个是建立连接后,服务端与客户端之间进行READ或WRITE的通道SocketChannel socketChannel = channel.accept();//同样,设为非阻塞socketChannel.configureBlocking(false);log.debug("boss线程准备调用worker线程的register方法 >>>");//在Boss单线程的情况下,由于是while迭代器逐个遍历,所以这句代码同一时刻只有一个线程访问,不存在并发安全问题
//                    worker.register(socketChannel);//index.getAndIncrement(): 获取到index的值并且+1,线程安全的//index.getAndIncrement() % workers.length 得到的结果为:[0,workers.length-1]//注意哈:一次拿到一个事件去处理,而不是一次拿一堆,由于Boss是单线程, 所以同一时刻register方法只有一个线程去访问,无并发安全问题。workers[index.getAndIncrement() % workers.length].register(socketChannel) ;log.debug("boss线程调用结束worker线程的register方法 >>>");}}}}}
package com.messi.netty_basic_01.reactor;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;public class Worker implements Runnable{private static final Logger log = LoggerFactory.getLogger(Worker.class);//worker的名称private String name;//worker是一个子线程的任务,所以应该有一个线程,其实就是实际干活的private Thread thread;//每一个worker子线程都会有一个Selector轮询器。一定注意,每一个线程都具有独立的Selector复用器,和主线程不是同一个Selectorprivate Selector selector;//加上一个volatile来解决多线程环境下isCreated值的可见性问题//但是在我们这个程序中,并不存在多线程安全问题,因为Boss线程为单线程,并且迭代器遍历客户端事件时是逐个遍历请求register//所以不存在多线程并发安全问题。只不过这里养成良好习惯给标记位加上volatile。为了保证后续多个Boss线程同时访问的环境下的可用性private volatile boolean isCreated ; //标记位//并发队列,负责传递[代码 或 功能]到另外一个线程private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>() ;public Worker(String name) {this.name = name;}public void register(SocketChannel socketChannel) throws Exception{log.debug("worker#register invoke");//使用标记位方式,保证下列初始化只执行一次!// 分析一下:为什么只让执行一次?// Boss(main)线程调用register方法中的下列这些代码。其实就是Boss线程在与处理完客户端建立连接的事件后的调用// 如果每一次处理完一个客户端连接就开启一个新Thread去处理该客户端后续的READ或Write事件,那么是不是倒退了?// 所以要控制,每一个Worker对象才会异步开启一个线程去执行处理很多客户端的读写事件,后续压力大了,可以搞一个Worker线程池,多个Worker线程去处理所有客户端的读写事件!// 绝对不是一个Worker线程处理一个客户端的读写事件,那样性能耗费太大了,而且没必要!// 但是还有一个细节需要思考:// 对于同一Worker对象而言,异步线程Thread只创建启动一个。但是对于不同的Worker对象,异步线程Thread会重新创建启动一个// 为什么会这样呢?因为isCreated标记位,对于一个新Worker对象,isCreated初始值为false, 那么自然会执行下列这段代码!// 下列这个if包围的代码还可以放在哪?// 放在Worker的构造方法中,只在对象创建初始化执行一次if (!isCreated) {//创建线程this.thread = new Thread(this,name);//先对Selector进行初始化,避免后续NUll Exceptionselector = Selector.open();//异步起一个线程this.thread.start();//标志位设为true 保证初始化代码只执行一次isCreated = true;System.out.println("我是梅西");}log.debug("socketChannel#register >>>");//使用睡眠模拟繁杂的业务逻辑Thread.sleep(2000);
//        synchronized (this) {
//            //其实就是打了一个标记。即调用一次wakeup后,无论此时selector.select()阻塞还是之后才阻塞,都会减少阻塞一次
//            selector.wakeup();
//            socketChannel.register(selector, SelectionKey.OP_READ);
//        }//把SocketChannel注册到Selector并且设置READ事件的代码放到并发队列中 传递给后续新开启的线程queue.add(()->{try {socketChannel.register(selector, SelectionKey.OP_READ);} catch (ClosedChannelException e) {e.printStackTrace();}});//兜底操作:唤醒一次Selector。无论Selector在wakeup方法执行前阻塞,还是在wakeup执行后阻塞,wakeup都可以唤醒一次Selector阻塞,像是一个标记计数selector.wakeup();System.out.println("Worker.register");}@Overridepublic void run() {while (true) {log.debug("worker线程的run方法开始执行 >>>");try {//应用层等待。内核层轮询监听事件的发生selector.select();//从并发队列中获取任务Runnable poll = queue.poll();//任务不为空,则执行该任务if (poll != null) {poll.run();}//一旦Selector监听到keys这一HashSet中注册的Channel对应事件的发生,就会把这些触发的事件对应的Channel的引用复制copy到SelectionKeys这一HashSet中//这一copy迁移Channel引用的工作是selector.select()做的Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//遍历Selector监听到的事件集合迭代器while (iterator.hasNext()) {//一个一个的来SelectionKey key = iterator.next();//使用完后就删除掉 避免空指针异常iterator.remove();//Worker线程监听Read事件if (key.isReadable()) {//获取到SocketChannel 该SocketChannel为客户端与服务端之间进行读写操作的管道SocketChannel socketChannel = (SocketChannel) key.channel();//创建Buffer缓冲区 默认为写模式ByteBuffer buffer = ByteBuffer.allocate(30);//把SocketChannel中的数据读取到buffer缓冲区socketChannel.read(buffer);//切换到读模式buffer.flip();//解码CharBuffer result = Charset.forName("UTF-8").decode(buffer);//输出读取到的结果数据log.info("worker线程读取的结果result为 : {}",result);}}} catch (IOException e) {e.printStackTrace();}}}
}
package com.messi.netty_basic_01.reactor;import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;public class MyClient {public static void main(String[] args) throws Exception{//客户端创建SocketChannelSocketChannel socketChannel = SocketChannel.open();//客户端与服务端之间通过SocketChannel进行连接socketChannel.connect(new InetSocketAddress(8000));//客户端写数据给服务端socketChannel.write(Charset.forName("UTF-8").encode("leomessi")) ;//.....System.out.println("MyClient.main");}}
  • 测试

debug的方式进行开启多个客户端与服务端建立通信

Server端:每次都会选一个Worker线程执行register方法

  • 讨论交流

worker线程搞多少个比较好 ? ? ?

CPU核数减1。现代计算机一般都是单CPU多个核,也就是俗称的多核CPU。假设目前为八核,那么开启8-1=7个worker线程,为什么开7个worker线程,一个线程给boss线程,一共8个线程,更好的利用这八个核,实现真正意义上的并行。截止到目前有好几个问题:(1)为什么worker线程数要多于boss线程?(2)为什么正好开启核数个线程效率最佳?(即核数=boss线程数+worker线程数)(3)能不能开启线程个数超过最大核数,最大开启线程数为多少?

依次回答:(1)对于一个客户端而言,连接只进行一次,连接之后的读写可能回触发多次!而我们又知道,boss线程是处理连接的,worker线程处理连接后的读写事件的,答案显而易见了。(2)为了实现真正意义上的并行,因为当线程数超过CPU核数时,CPU需要不断的频繁调度切换。并且线程创建是占用内存的,线程越多,内存消耗越大。(3)当然可以,最大开启线程数取决于具体硬件配置,你要看硬件的并发能力,你要CPU切换能力强,内存大,线程开多点其实也没毛病的。

9.零拷贝

一定要看这篇文章:

一文彻底弄懂零拷贝原理 - 掘金

  • 基本知识

无论是磁盘IO还是网络IO,实际上最终都需要操作具体的硬件,磁盘IO--->磁盘文件,网络IO--->网络网卡

但是呢,我们编写的JVM应用程序是无法直接操控硬件的,中间需要通过操作系统做中间者,具体原因这里简单说一下,因为对于磁盘,网络资源,我们不能让用户程序直接操控,一方面是硬件版本问题,一方面是安全问题(怎么可能让用户直接操控内存呢,这多不安全呀!)

具体的调用过程为:JVM用户程序--->操作系统--->硬件驱动程序--->具体的硬件

总结一下:

1.JVM中的java应用程序不能直接操作硬件,需要操作系统的介入

2.内存分为两种,也可以理解成内存空间分为两部分:

一部分为用户地址空间,这部分的内存空间通常给应用程序(如:JVM进行使用),应用程序通常是进程级别(我们常说的线程,线程实际上是共享进程的内存资源的一种抽象细化,线程是利用进程空间的)。应用程序其实就是我们平常编写的各种代码动态运行起来的一种概念统称。

另外一部分为内核地址空间,这部分的内存空间通常给操作系统,操作系统也是占用内存的一种特殊的程序,操作系统通常会提供一系列公共的功能API。啥是公共的功能API?比如:应用程序需要申请内存空间,应用程序就需要先调用操作系统提供的公共功能API。

3.为什么socket网络连接,io流资源,线程资源都是珍贵的资源,使用完毕后要及时关闭?

对于这些资源,是对安全性要求极高的资源,你仅仅通过用户程序是无法完成这些资源的操控的,必须陷入操作系统内核让OS去真正调用申请。所以这些资源是珍贵的, 是位于OS内存的公共资源。

并且申请的这些公共资源都是占用着操作系统所对应的内核地址空间内存,我们应用程序是陷入内核使用这部分内核内存空间的。

假如说当前用户程序使用完毕后不断开对这部分操作系统内存的引用(即不断开socket网络连接,IO流),会影响这台计算机其他应用程序对操作系统这部分公共资源内存的调用和使用的,因为不可能一台计算机只有一个进程应用程序!甚至导致其他应用程序无法申请到这些珍贵的资源。所以使用完后,应用程序要及时断开对珍贵资源的连接。

并且要减少应用程序与这部分资源的交互,因为需要陷入内核,切换的时候性能耗费一定是很大的

  • 传统方式进行拷贝,不进行零拷贝优化:

下面分析涉及到的代码程序

总结:

参看文章:一文彻底弄懂零拷贝原理 - 掘金

分析:

注释:创建直接内存的API就是:ByteBuffer buffer = ByteBuffer.allocateDirect(20);

  • 零拷贝优化1:mmap+write

涉及到的代码:

通过NIO的API创建直接内存空间,所谓直接内存就是应用程序不再开辟用户地址内存空间(堆内存)了,而是直接使用操作系统的内存。用户程序可以编写代码直接操控这一块直接内存,可以读写,清除等操作。

这也叫做内存映射,好像是把操作系统的内存映射到用户地址内存空间上,减少了数据的拷贝。

但应用程序一味的使用直接内存也存在很大弊端,因为OS不具备JVM的GC机制,所以如果应用程序不及时释放操作系统的直接内存,那么可能会存在很大隐患。

eg:调用NIO-API:ByteBuffer.allocateDirect(10),这句代码创建的就是10字节大小的操作系统内存:高速页缓存这种。然后应用程序获取到这块直接内存后,在应用程序中就可以使用java代码直接操作这块直接内存了。

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

补充:

但是注意,内存映射/开辟OS直接内存是针对于文件的读写操作。对于网络相关的数据读写操作我们不可以使用直接内存映射,为什么?因为你无法把这一台JVM的相应内容数据给另外一台JVM。

  • 零拷贝优化2:linux内核2.1版本的零拷贝 - sendfile方式

涉及到的代码:

所有的零拷贝都是文件相关的,socket不涉及零拷贝。零拷贝是指不进行JVM虚拟机层面的拷贝,只进行系统级别的拷贝.

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

  • 零拷贝优化3:linux内核2.4版本的零拷贝- 带有 scatter/gather 的 sendfile方式

对Linux2.4内核进行sendFile方法的优化的一些理解:这次优化直接让保存到高速页缓存的文件读取数据直接发送到网卡对应的驱动程序,再给到网卡。而无需拷贝到socket缓存。

为什么可以这样?因为无论是文件对应的高速页缓存还是网卡对应的socket缓存,其实内部结构都是一样的,只不过可能标记位不同,既然差不多一样,把页缓存的内存地址,偏移量记录传输到socket缓存,然后直接把高速页缓存的数据给到网卡驱动程序,然后再给网卡,这有问题吗?

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

  • 零拷贝优化4 - splice 方式

splice 调用和sendfile 非常相似,用户应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说,一直都是使用sendfile系统调用,而splice一直以来就只是一种机制,它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。

在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 机制的实现已经没有了,但是其 API 及相应的功能还在,只不过 API 及相应的功能是利用了 splice 机制来实现的。

总结:

见文章:一文彻底弄懂零拷贝原理 - 掘金

  • 总结

无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA copy是都少不了的。因为两次 DMA 都是依赖硬件完成的。所以,所谓的零拷贝,都是为了减少 CPU copy 及减少了上下文的切换。

sendfile局限于文件与socket进行数据传输。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。

下图展示了各种零拷贝技术的对比图:

  • 补充

我们java平常做的优化都是在JVM层面进行优化的。对于OS操作系统层面的优化,需要OS开发维护者去优化的,比如说linux2.4内核完成了sendFile方法的重构,epoll模型的完善。对于我们java程序来说,只要你部署在linux操作系统上,你就可以享用OS优化带来的好处,因为你java程序对于一些系统级别的操作,如文件读取,其实是调用OS的sendFile,那么OS对sendFile做了优化,是不是等价于你java程序性能也进一步优化了?对吧,答案显而易见。

这就是为什么后面我们的服务一般都部署在linux操作系统服务器上,它在IO处理方面是要比市面上其他操作系统服务器要强的多的,但是异步处理层面上要比win操作系统差一些的。。。所以现在都研究linux内核

10.硬件驱动程序与软件驱动程序(数据库驱动程序)的关系

11.linux内核与centos,redhat等这些产品有啥区别和关系?

linux内核是开源的,是linux最核心的代码。后续centos,redhat这些厂商基于linux内核进行开发,进行套壳,增加了稳定性,可靠性,或一些美丽的图形化界面。但是这些厂商也会把产品分为很多发行版,比如说这一个发行版做的不好或差一些,那么就开源不收费。如果这一个发行版做的好,那么就收费闭源。。。

这就好比:安卓操作系统与小米的MIUI,华为的一些发行版产品等,一样的关系。

相关文章:

Netty应用(四) 之 Reactor模型 零拷贝

目录 6.Reactor模型 6.1 单线程Reactor 6.2 主从多线程Reactor (主--->Boss | 从--->Worker | 一主多从机制) 7.扩展与补充 8.Reactor模型的实现 8.1 多线程Reactor模型的实现&#xff08;一个Boss线程&#xff0c;一个Worker线程&#xff09; 8.2 多线程Reactor模…...

Huggingface上传模型

Huggingface上传自己的模型 参考 https://juejin.cn/post/7081452948550746148https://huggingface.co/blog/password-git-deprecationAdding your model to the Hugging Face Hub&#xff0c; huggingface.co/docs/hub/ad…Welcome&#xff0c;huggingface.co/welcome三句指…...

kyuubi 接入starrocks | doris

kyuubi 接入starrocks 一、环境 Hadoop集群 组件版本Hadoop3.1.1spark3.Xzookeeper3.XHive3.X kyuubi 版本 1.7.1 starrocks 2.X   已将kyuubi部署到yarn上&#xff0c;并且接入了spark3引擎&#xff0c;并通过Ambari进行kyuubi组件的管理&#xff0c;下面步骤为新增对sta…...

notepad++成功安装后默认显示英文怎么设置中文界面?

前几天使用电脑华为管家清理电脑后&#xff0c;发现一直使用的notepad软件变回了英文界面&#xff0c;跟刚成功安装的时候一样&#xff0c;那么应该怎么设置为中文界面呢&#xff1f;具体操作如下&#xff1a; 1、打开notepad软件&#xff0c;点击菜单栏“Settings – Prefere…...

HiveSQL——连续增长问题

注&#xff1a;参考文章&#xff1a; SQL连续增长问题--HQL面试题35_sql判断一个列是否连续增长-CSDN博客文章浏览阅读2.6k次&#xff0c;点赞6次&#xff0c;收藏30次。目录0 需求分析1 数据准备3 小结0 需求分析假设我们有一张订单表shop_order shop_id,order_id,order_time…...

使用cocos2d-console初始化一个项目

先下载好cocos2d-x的源码包 地址 https://www.cocos.com/cocos2dx-download 这里使用的版本是 自己的电脑要先装好python27 用python安装cocos2d-console 看到项目中有个setup.py的一个文件 python setup.py 用上面的命令执行一下。 如果执行正常的话回出现上面的图 然后…...

VitePress-13- 配置-title的作用详解

作用描述 1、title 是当前站点的标题&#xff1b;2、默认值是 &#xff1a;VitePress&#xff1b;3、当使用默认主题时&#xff0c;会直接展示在 页面的【导航条】中&#xff1b;4、一个特殊的作用 &#xff1a; 会作为单个页面的默认标题后缀&#xff01;除非又指定了【title…...

Rust-AI todo list 开发体验

之前用AI协助开发了一个Vue模块&#xff0c;感觉意犹未尽&#xff0c;所以决定再让AI 来协助我做一个todo list。 todo list对我来说真是一个刚需&#xff0c;从我决定做一件事情&#xff0c;到这件事情做完&#xff0c;我的todo list不但不会减少&#xff0c;反而会增加。 回…...

2024-02-07(Sqoop,Flume)

1.Sqoop的增量导入 实际工作中&#xff0c;数据的导入很多时候只需要导入增量的数据&#xff0c;并不需要将表中的数据每次都全部导入到hive或者hdfs中&#xff0c;因为这样会造成数据重复问题。 增量导入就是仅导入新添加到表中的行的技术。 sqoop支持两种模式的增量导入&a…...

LDAR管理系统解决方案

1、密封点数量不准确 工业企业LDAR项目多委托第三方进行检测&#xff0c;由于前几年由于检测费较高&#xff0c;为减少开支&#xff0c;很多企业只安排检测公司检测了部分密封点&#xff0c;造成密封点遗漏。也有少数企业为了从中谋私利&#xff0c;虚增密封点。 2、密封点台账…...

[vscode]ssh报错: Resolver error: Error: XHR failedscode错误

场景问题&#xff1a;通过vscode ssh连接远程服务器失败&#xff0c;报错&#xff1a;Resolver error: Error: XHR failedscode&#xff1a; 问题原因&#xff1a;~/.vscode-server/bin/一串数字下的vscode-server-linux-x64.tar.gz由于某种原因无法正常下载 解决方式&#x…...

【Maven】依赖、构建管理 继承与聚合 快速学习(3.6.3 )

文章目录 Maven是什么&#xff1f;一、Maven安装和配置本地配置文件设置idea配置本地maven 二、基于IDEA的Maven工程创建2.1 Maven工程GAVP属性2.2 Idea构建Maven JavaEE工程 三、Maven工程项目结构说明四、Maven核心功能依赖和构建管理4.1 依赖管理和配置4.2 依赖传递和冲突4.…...

Flume安装部署

安装部署 安装包连接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1m0d5O3Q2eH14BpWsGGfbLw?pwd6666 &#xff08;1&#xff09;将apache-flume-1.10.1-bin.tar.gz上传到linux的/opt/software目录下 &#xff08;2&#xff09;解压apache-flume-1.10.1-bin.tar.gz…...

点云从入门到精通技术详解100篇-非结构化道路下无人平台路径规划与运动控制

目录 前言 路径规划方法研究现状 传统规划算法 智能规划算法 规划方法比较...

生成树技术华为ICT网络赛道

9.生成树 目录 9.生成树 9.1.生成树技术概述 9.2.STP的基本概念及工作原理 9.3.STP的基础配置 9.4.RSTP对STP的改进 9.5.生成树技术进阶 9.1.生成树技术概述 技术背景&#xff1a;二层交换机网络的冗余性与环路 典型问题1&#xff1a;广播风暴 典型问题2&#xff1a;MA…...

[HTTP协议]应用层的HTTP 协议介绍

目录 1.前言 2.使用fiddler抓包来观察HTTP协议格式 3.HTTP协议的基本格式 2.1请求 2,1.1首行 2.1.2请求头 2.1.3空行 2.2响应 2.2.1首行 2.2.2响应头 键值对 ​编辑2.2.3空行 2.2.4载荷(响应正文) 3.认识URL 3.1关于URL encode 1.前言 我们在前面的博客中,简单的…...

Linux 命令基础

Shell概述 Linux操作系统的Shell作为操作系统的外壳&#xff0c;为用户提供使用操作系统的接口。它是命令语言、命令解释程序及程序设计语言的统称。 Shell是用户和Linux内核之间的接口程序&#xff0c;如果把硬件想象成一个球体的中心&#xff0c;内核围绕在硬件的外层管理着…...

【开源】JAVA+Vue+SpringBoot实现实验室耗材管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 耗材档案模块2.2 耗材入库模块2.3 耗材出库模块2.4 耗材申请模块2.5 耗材审核模块 三、系统展示四、核心代码4.1 查询耗材品类4.2 查询资产出库清单4.3 资产出库4.4 查询入库单4.5 资产入库 五、免责说明 一、摘要 1.1…...

集成开发环境 IntelliJ IDEA的基本使用

集成开发环境 IntelliJ IDEA 是由 JetBrains 开发的一个强大的 Java IDE&#xff0c;它也被广泛用于其他编程语言的开发&#xff0c;如 Kotlin、Scala 和 Groovy 等。IntelliJ IDEA 以其智能的代码补全、代码分析、重构工具和强大的调试功能而闻名。以下是 IntelliJ IDEA 的基本…...

【Flink入门修炼】1-2 Mac 搭建 Flink 源码阅读环境

在后面学习 Flink 相关知识时&#xff0c;会深入源码探究其实现机制。因此&#xff0c;需要现在本地配置好源码阅读环境。 本文搭建环境&#xff1a; Mac M1&#xff08;Apple Silicon&#xff09;Java 8IDEAFlink 官方源码 一、 下载 Flink 源码 github 地址&#xff1a;h…...

Spring IoC容器详解

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 基本概念 Spring IoC容器是Spring框架的核心组件&#xff0c;它实现了控制反转&#xff08;Inversion of Control&#xff0c;IoC&#xff09;的设计原则。IoC是一种编程思…...

06 MP之自动填充+SQL执行的语句和速度分析

1. 自动填充 在项目中有一些属性&#xff0c;比如常见的创建时间和更新时间可以设置为自动填充。 1.1 实例 需求: 将创建时间和更新时间设置为自动填充, 这样每次插入数据时可以不用理会这两个字段 1.1.1 在数据库增加字段 默认开启驼峰映射 createTime --> create_time…...

3 scala集合-Set

与 Java 的 Set 一样&#xff0c;scala 的 set 中&#xff0c;元素都是唯一的&#xff0c;而且遍历 set 中集合的顺序&#xff0c;跟元素插入的顺序是不一样的。 同样&#xff0c;Set 也包含可变和不可变两种。要实现可变 Set 集合&#xff0c;需要使用类 scala.collection.mu…...

Android应用图标微技巧,8.0系统中应用图标的适配

大家好,2018年的第一篇文章到的稍微有点迟,也是因为在上一个Glide系列结束之后一直还没想到什么好的新题材。 现在已经进入了2018年,Android 8.0系统也逐渐开始普及起来了。三星今年推出的最新旗舰机Galaxy S9已经搭载了Android 8.0系统,紧接着小米、华为、OV等国产手机厂…...

java学习(多态)

一、多态 含义&#xff1a;方法或对象具有多种形态。是面向对象的第三大特征&#xff0c;多态是建立在封装和继承基础上的。 多态的具体体现&#xff1a; 1&#xff09;方法的多态 &#xff08;例如重写和重载&#xff09; 2&#xff09;对象的多态 多态注意事项&#xff1…...

[UI5 常用控件] 07.SplitApp,SplitContainer

文章目录 前言1. SplitApp1.1 组件结构1.2 Demo1.3 mode属性 2. SplitContainer 前言 本章节记录常用控件SplitApp&#xff0c;SplitContainer。主要功能是在左侧显示Master页面&#xff0c;右侧显示Detail页面。 Master页面和Detail页面可以由多个Page组成&#xff0c;并支持…...

MyBatisPlus之分页查询及Service接口运用

一、分页查询 1.1 基本分页查询 配置分页查询拦截器 package com.fox.mp.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springfra…...

对象存储minio

参考Linux搭建免费开源对象存储 创建一个data目录 --address和--console-address是MinIO服务器启动命令中的两个参数&#xff0c;它们具有以下区别&#xff1a; --address参数&#xff1a;用于指定MinIO服务器监听的S3 API访问地址。S3 API是用于与MinIO进行对象存储操作的…...

大模型学习笔记二:prompt工程

文章目录 一、经典AI女友Prompt二、prompt怎么做&#xff1f;1&#xff09;注重格式&#xff1a;2&#xff09;prompt经典构成3&#xff09;简单prompt的python询问代码4&#xff09;python实现订阅手机流量套餐的NLU5&#xff09;优化一&#xff1a;加入垂直领域推荐6&#xf…...

MATLAB实现LSTM时间序列预测

LSTM模型可以在一定程度上学习和预测非平稳的时间序列&#xff0c;其具有强大的记忆和非线性建模能力&#xff0c;可以捕捉到时间序列中的复杂模式和趋势[4]。在这种情况下&#xff0c;LSTM模型可能会自动学习到时间序列的非平稳性&#xff0c;并在预测中进行适当的调整。其作为…...

Kubernetes CNI Calico:Route Reflector 模式(RR) calico IPIP切换RR网络模式

1. 概述 Calico 路由反射模式是一种 BGP 互联方案,用于解决大规模网络中路由信息的分发和同步问题。在 Calico 的路由反射模式中,路由反射器(Route Reflectors)被用来集中管理路由信息,以减少网络中的路由信息数量和减小路由信息的分发规模。 在 Calico 的路由反射模式中…...

探索Gin框架:Golang Gin框架请求参数的获取

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站https://www.captainbed.cn/kitie。 前言 我们在专栏的前面几篇文章内讲解了Gin框架的路由配置&#xff0c;服务启动等内容。 专栏地址&…...

极值图论基础

目录 一&#xff0c;普通子图禁图 二&#xff0c;Turan问题 三&#xff0c;Turan定理、Turan图 1&#xff0c;Turan定理 2&#xff0c;Turan图 四&#xff0c;以完全二部图为禁图的Turan问题 1&#xff0c;最大边数的上界 2&#xff0c;最大边数的下界 五&#xff0c;…...

word导出链接

java 使用 POI 操作 XWPFDocumen 创建和读取 Office Word 文档基础篇 https://www.cnblogs.com/mh-study/p/9747945.html word标签解析文档 http://www.datypic.com/sc/ooxml/e-w_tbl-1.html...

(delphi11最新学习资料) Object Pascal 学习笔记---第4章第2.5节(重载和模糊调用)

4.2.5 重载和模糊调用 ​ 当调用一个重载的函数时&#xff0c;编译器通常会找到匹配的版本并正确工作&#xff0c;或者如果没有任何重载版本具有正确匹配的参数&#xff08;正如我们刚刚看到的&#xff09;&#xff0c;则会报出错误。 ​ 但还有第三种情况&#xff1a;假设编…...

ElementUI Data:Table 表格

ElementUI安装与使用指南 Table 表格 点击下载learnelementuispringboot项目源码 效果图 el-table.vue&#xff08;Table表格&#xff09;页面效果图 项目里el-table.vue代码 <script> export default {name: el_table,data() {return {tableData: …...

11.2 OpenGL可编程顶点处理:细分着色器

细分 Tessellation Tessellation&#xff08;细分&#xff09;是计算机图形学中的一种技术&#xff0c;用于在渲染过程中提高模型表面的几何细节。它通过在原始图元&#xff08;如三角形、四边形或补丁&#xff09;之间插入新的顶点和边&#xff0c;对图元进行细化分割&#x…...

微软正在偷走你的浏览记录,Edge浏览器偷疯了

虽然现在 Edge 浏览器相当强大&#xff0c;甚至在某种程度上更符合中国用户的使用体验&#xff1b;但最近新的Edge浏览器推出后一直在使用的用户应该有感受到&#xff0c;原本的冰清玉洁的转校生慢慢小鸡脚藏不住了&#xff0c;广告越来越多&#xff0c;越来越流氓了。 电脑之前…...

什么是数据库软删除,什么场景下要用软删除?(go GORM硬删除)

文章目录 什么是数据库软删除&#xff0c;什么场景下要用软删除&#xff1f;go GORM硬删除什么是数据库软删除什么场景下要用软删除 什么是数据库软删除&#xff0c;什么场景下要用软删除&#xff1f; go GORM硬删除 使用的是 GORM&#xff0c;默认启用了软删除功能&#xff…...

计算机设计大赛 深度学习+python+opencv实现动物识别 - 图像识别

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 inception_v3网络5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; *…...

我主编的电子技术实验手册(02)——仪表与电源

本专栏是笔者主编教材&#xff08;图0所示&#xff09;的电子版&#xff0c;依托简易的元器件和仪表安排了30多个实验&#xff0c;主要面向经费不太充足的中高职院校。每个实验都安排了必不可少的【预习知识】&#xff0c;精心设计的【实验步骤】&#xff0c;全面丰富的【思考习…...

C语言----内存函数

内存函数主要用于动态分配和管理内存&#xff0c;它直接从指针的方位上进行操作&#xff0c;可以实现字节单位的操作。 其包含的头文件都是&#xff1a;string.h memcpy copy block of memory的缩写----拷贝内存块 格式&#xff1a; void *memcpy(void *dest, const void …...

【力扣】快乐数,哈希集合 + 快慢指针 + 数学

快乐数原题地址 方法一&#xff1a;哈希集合 定义函数 getNext(n) &#xff0c;返回 n 的所有位的平方和。一直执行 ngetNext(n) &#xff0c;最终只有 2 种可能&#xff1a; n 停留在 1 。无限循环且不为 1 。 证明&#xff1a;情况 1 是存在的&#xff0c;如力扣的示例一…...

c实现顺序表

目录 c语言实现顺序表 完整代码实现 c语言实现顺序表 顺序表的结构定义&#xff1a; typedef struct vector {int size; // 顺序表的容量int count; // 顺序表现在存储了多少个数据int *data; // 指针指向连续的整型存储空间 } vector;顺序表的结构操作&#xff1a; 1、初始…...

微软为新闻编辑行业推出 AI 辅助项目,记者参加免费课程

2 月 6 日消息&#xff0c;微软当地时间 5 日发布新闻稿宣布与多家新闻机构展开多项基于生成式 AI 的合作。微软表示&#xff0c;其使命是确保新闻编辑室在今年和未来拥有创新。 目前建议企业通过微软官方合作伙伴获取服务&#xff0c;可以合规、稳定地提供企业用户使用ChatGP…...

openssl3.2 - exp - buffer to BIO

文章目录 openssl3.2 - exp - buffer to BIO概述笔记END openssl3.2 - exp - buffer to BIO 概述 openssl的资料看的差不多了, 准备将工程中用到的知识点整理一下. openssl中很多API是以操作文件作为输入的, 也有很多API是以BIO作为输入的. 不管文件是不是受保护的, 如果有可…...

Android 13.0 系统framework修改低电量关机值为3%

1、讲在最前面 系统rom定制开发中&#xff0c;其中在低电量时&#xff0c;系统会自动关机&#xff0c;这个和不同的平台和底层驱动和硬件都有关系&#xff0c;需要结合这些来实际调整这个值&#xff0c;我们可以通过分析源码中电池服务的代码&#xff0c;然后进行修改如何实现…...

【EAI 013】BC-Z: Zero-Shot Task Generalization with Robotic Imitation Learning

论文标题&#xff1a;BC-Z: Zero-Shot Task Generalization with Robotic Imitation Learning 论文作者&#xff1a;Eric Jang, Alex Irpan, Mohi Khansari, Daniel Kappler, Frederik Ebert, Corey Lynch, Sergey Levine, Chelsea Finn 论文原文&#xff1a;https://arxiv.org…...

一文讲透ast.literal_eval() eval() json.loads()

文章目录 一文讲透ast.literal_eval() eval() json.loads()1. ast.literal_eval()2. eval()3. json.loads()4. 总结 一文讲透ast.literal_eval() eval() json.loads() 在Python库中&#xff0c;我们经常会遇到需要将字符串转换为相应对象或数据结构的情况。在这种情况下&#…...

微软.NET6开发的C#特性——类、结构体和联合体

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;下面我就重点讲讲微软.NET6开发人员需要知道的C#特性&#xff0c;然后比较其他各种语言进行认识。 C#经历了多年发展…...