Java NIO学习与记录(八): Reactor两种多线程模型的实现
注:本篇文章例子基于上一篇进行:Java NIO学习与记录(七): Reactor单线程模型的实现
前言:单线程Reactor模型的缺点
紧接着上篇Reactor单线程模型的例子来,假设Handler的read那里的处理方式延迟5s,当做是业务性能瓶颈,改变下原来的Handler,让其read方法在处理时延迟5s:
private void read() throws IOException {
if (selectionKey.isValid()) {
System.out.println("服务端读取数据前");
readBuffer.clear();
int count = socketChannel.read(readBuffer);
if (count > 0) {
try {
Thread.sleep(5000L); //读取信息后睡眠5s当做业务处理瓶颈
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("收到来自 %s 的消息: %s",
socketChannel.getRemoteAddress(),
new String(readBuffer.array())));
status = SEND;
selectionKey.interestOps(SelectionKey.OP_WRITE);
} else {
selectionKey.cancel();
socketChannel.close();
System.out.println("read时-------连接关闭");
}
}
}
现在同样开启两个客户端同时连接到该服务端,然后请求-->收到响应-->再次请求的流程走10次,会发现,客户端每收到一次响应需要10s,同样的如果开启3个客户端,则需要15s,因为单线程的Reactor模型是串行的,业务处理的瓶颈可以影响到全局的事件分发,这种模型下,如果存在类似例子中的瓶颈点是致命的(例子的5s是夸张处理),因为新进来的连接也会排队,整个select都会被Handler的处理给阻塞掉,举个实际点的例子,redis在使用时大部分时候会避免使用类似keys这种重操作,为什么呢?就是因为redis是单线程,这里说的单线程其实并不是说redis服务端就一个线程,而是说redis采用的NIO Reactor模型就是单线程的Reactor模型,跟上面代码里做的改动一样,5s可以理解成重操作,影响整个模型的正常运作,redis之所以采用单线程模式,是因为redis大部分操作实在是太快了,快到使用这种模式也可以提供近十万/秒的并发能力,单线程模型实现起来简单且可控性强,所以redis很自然的选择了这种模式。回到问题本身,我们自己的业务可能并没有redis那样高的处理能力,搞不好几个网络请求就可以造成性能瓶颈,拖慢甚至拖垮整个处理模型,所以大部分RPC框架和web容器并不会采用单线程的Reactor模型实现,那么有没有什么方法可以优化这种模型呢?比如,把这个瓶颈点利用独立线程异步出去处理,这样可以保证不影响select的执行,也就很好的避免了上面的问题了,下面介绍两种多线程异步的Reactor模型。
一、单Reactor多线程模型
模型图:
图1
上图与单线程Reactor模型对比可以看出,读入数据后,对数据的业务处理部分被线程池做了异步处理,也就是说,上述5s的那段瓶颈被放到了子线程去处理,select的执行不会受到任何影响,因此对新的连接处理、多个客户端的响应速度都应该可以得到保障。
现在来改写下前篇文章里的单线程处理模式的Handler,更名为AsyncHandler:
public class AsyncHandler implements Runnable {
private final Selector selector;
private final SelectionKey selectionKey;
private final SocketChannel socketChannel;
private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
private ByteBuffer sendBuffer = ByteBuffer.allocate(2048);
private final static int READ = 0; //读取就绪
private final static int SEND = 1; //响应就绪
private final static int PROCESSING = 2; //处理中
private int status = READ; //所有连接完成后都是从一个读取动作开始的
//开启线程数为5的异步处理线程池
private static final ExecutorService workers = Executors.newFixedThreadPool(5);
AsyncHandler(SocketChannel socketChannel, Selector selector) throws IOException {
this.socketChannel = socketChannel;
this.socketChannel.configureBlocking(false);
selectionKey = socketChannel.register(selector, 0);
selectionKey.attach(this);
selectionKey.interestOps(SelectionKey.OP_READ);
this.selector = selector;
this.selector.wakeup();
}
@Override
public void run() { //如果一个任务正在异步处理,那么这个run是直接不触发任何处理的,read和send只负责简单的数据读取和响应,业务处理完全不阻塞这里的处理
switch (status) {
case READ:
read();
break;
case SEND:
send();
break;
default:
}
}
private void read() {
if (selectionKey.isValid()) {
try {
readBuffer.clear();
int count = socketChannel.read(readBuffer);
if (count > 0) {
status = PROCESSING; //置为处理中,处理完成后该状态为响应,表示读入处理完成,接下来可以响应客户端了
workers.execute(this::readWorker); //异步处理
} else {
selectionKey.cancel();
socketChannel.close();
System.out.println("read时-------连接关闭");
}
} catch (IOException e) {
System.err.println("处理read业务时发生异常!异常信息:" + e.getMessage());
selectionKey.cancel();
try {
socketChannel.close();
} catch (IOException e1) {
System.err.println("处理read业务关闭通道时发生异常!异常信息:" + e.getMessage());
}
}
}
}
void send() {
if (selectionKey.isValid()) {
status = PROCESSING; //置为执行中
workers.execute(this::sendWorker); //异步处理
selectionKey.interestOps(SelectionKey.OP_READ); //重新设置为读
}
}
//读入信息后的业务处理
private void readWorker() {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("收到来自客户端的消息: %s",
new String(readBuffer.array())));
status = SEND;
selectionKey.interestOps(SelectionKey.OP_WRITE); //注册写事件
this.selector.wakeup(); //唤醒阻塞在select的线程,因为该interestOps写事件是放到子线程的,select在该channel还是对read事件感兴趣时又被调用,因此如果不主动唤醒,select可能并不会立刻select该读就绪事件(在该例中,可能永远不会被select到)
}
private void sendWorker() {
try {
sendBuffer.clear();
sendBuffer.put(String.format("我收到来自%s的信息辣:%s, 200ok;",
socketChannel.getRemoteAddress(),
new String(readBuffer.array())).getBytes());
sendBuffer.flip();
int count = socketChannel.write(sendBuffer);
if (count < 0) {
selectionKey.cancel();
socketChannel.close();
System.out.println("send时-------连接关闭");
} else {
//再次切换到读
status = READ;
}
} catch (IOException e) {
System.err.println("异步处理send业务时发生异常!异常信息:" + e.getMessage());
selectionKey.cancel();
try {
socketChannel.close();
} catch (IOException e1) {
System.err.println("异步处理send业务关闭通道时发生异常!异常信息:" + e.getMessage());
}
}
}
}
可以看到,read里、send里的逻辑处理被异步出去执行,新增了中间状态“执行中”,主要用来防止事件重复触发,重复执行异步逻辑,当异步逻辑处理完毕才会更改状态值,这时候可以继续处理接下来的事件(读或写)。
把Accptor类里的实现换成AsyncHandler,运行服务端和客户端会发现,两个客户端的响应均为5s,也不会阻塞新增的连接,新增至三个或者更多的客户端基本可以保持客户端响应均为5s(说明:这里5s是夸张比喻,正常瓶颈没这么夸张,若开了n多客户端,每个都阻塞5s,那么线程池也会发生排队,因为子线程个数有限,处理不过来,最后还是阻塞,一定会远超过5s)。
通过多线程Reactor模型,降低了业务代码瓶颈导致影响整个Reactor执行链路的风险,但是即便如此,read、send操作仍然和接收请求(accept)处于同一个线程,这就意味着read、send的处理可能会影响到对客户端连接的接收能力,那么有没有一种办法,可以把读写流程彻底异步出去,负责连接的线程就只负责接收连接?于是多Reactor多线程模型就产生了,这种模型也叫主从Reactor模型,该模型下可以分为一个主Reactor专门处理连接事件,而多个从Reactor负责读写、业务处理等,这样服务端可以接收并处理更多的请求,提升服务端的吞吐能力(该模型或者说所有基于NIO的Reactor模型,都是以提升服务端处理能力为基础的,NIO在某些情况下不一定会比BIO处理速度快,但一定比BIO稳,就像NIO可以利用很少的线程处理大量的客户端请求,而BIO在大量客户端请求过来的情况下,由于各种操作均会阻塞线程,会处理不过来)。
二、主从Reactor模型
还是把之前文章的图拿来展示下这种模型的流程,可以与上面图1进行对比,看看发生了哪些变化:
图2
上图就是主从Reactor模型的一个流程,看下与图1的不同之处,多了SubReactor这样一个角色,这个角色就是用来处理读写操作的Reactor,现在仍然基于之前的例子,进行改写,明确需要改写的点:
①新增SubReactor
②Acceptor那里进行初始化一批SubReactor,进行分发处理
③为了区分客户端分别是被哪个SubReactor处理的读写操作,还需要改写下AsyncHandler,在里面加上SubReactor的序号,打印信息时进行区分。
ok,总结完改动点,现在基于上面的代码(代码初代目版本:Reactor单线程模型的实现)改写一下这几个类:
step1.首先新增SubReactor类
public class SubReactor implements Runnable {
private final Selector selector;
private boolean register = false; //注册开关表示,为什么要加这么个东西,可以参考Acceptor设置这个值那里的描述
private int num; //序号,也就是Acceptor初始化SubReactor时的下标
SubReactor(Selector selector, int num) {
this.selector = selector;
this.num = num;
}
@Override
public void run() {
while (!Thread.interrupted()) {
System.out.println(String.format("%d号SubReactor等待注册中...", num));
while (!Thread.interrupted() && !register) {
try {
if (selector.select() == 0) {
continue;
}
} catch (IOException e) {
e.printStackTrace();
}
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
dispatch(it.next());
it.remove();
}
}
}
}
private void dispatch(SelectionKey key) {
Runnable r = (Runnable) (key.attachment());
if (r != null) {
r.run();
}
}
void registering(boolean register) {
this.register = register;
}
}
这个类负责Acceptor交给自己的事件select(例子中实际上就是read、send)。
step2.Acceptor类的更改
public class Acceptor implements Runnable {
private final ServerSocketChannel serverSocketChannel;
private final int coreNum = Runtime.getRuntime().availableProcessors(); // 获取CPU核心数
private final Selector[] selectors = new Selector[coreNum]; // 创建selector给SubReactor使用,个数为CPU核心数(如果不需要那么多可以自定义,毕竟这里会吞掉一个线程)
private int next = 0; // 轮询使用subReactor的下标索引
private SubReactor[] reactors = new SubReactor[coreNum]; // subReactor
private Thread[] threads = new Thread[coreNum]; // subReactor的处理线程
Acceptor(ServerSocketChannel serverSocketChannel) throws IOException {
this.serverSocketChannel = serverSocketChannel;
// 初始化
for (int i = 0; i < coreNum; i++) {
selectors[i] = Selector.open();
reactors[i] = new SubReactor(selectors[i], i); //初始化sub reactor
threads[i] = new Thread(reactors[i]); //初始化运行sub reactor的线程
threads[i].start(); //启动(启动后的执行参考SubReactor里的run方法)
}
}
@Override
public void run() {
SocketChannel socketChannel;
try {
socketChannel = serverSocketChannel.accept(); // 连接
if (socketChannel != null) {
System.out.println(String.format("收到来自 %s 的连接",
socketChannel.getRemoteAddress()));
socketChannel.configureBlocking(false); //
reactors[next].registering(true); // 注意一个selector在select时是无法注册新事件的,因此这里要先暂停下select方法触发的程序段,下面的weakup和这里的setRestart都是做这个事情的,具体参考SubReactor里的run方法
selectors[next].wakeup(); // 使一個阻塞住的selector操作立即返回
SelectionKey selectionKey = socketChannel.register(selectors[next],
SelectionKey.OP_READ); // 当前客户端通道SocketChannel向selector[next]注册一个读事件,返回key
selectors[next].wakeup(); // 使一個阻塞住的selector操作立即返回
reactors[next].registering(false); // 本次事件注册完成后,需要再次触发select的执行,因此这里Restart要在设置回false(具体参考SubReactor里的run方法)
selectionKey.attach(new AsyncHandler(socketChannel, selectors[next], next)); // 绑定Handler
if (++next == selectors.length) {
next = 0; //越界后重新分配
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
可以跟以前的Acceptor做个对比,做了如下改动:
①接受到连接后不再直接触发handler了
②初始化一堆SubReactor(从反应堆),每个交给一个线程处理,注册读事件后顺序分配给不同的SubReactor去处理自己的selector监听。
以上,就可以把读写处理+业务处理与接受连接的Reactor彻底分开了,接受连接的事件不再受任何读写、业务相关的影响,只负责接收,目前即便是业务线程池用光线程发生排队,也不会影响到连接的接收,很大程度上降低了服务端的接收能力遭遇瓶颈的风险。
step3.改写AsyncHandler的打印
这里就不po代码了,具体就是把SubReactor的序号传给handler,标记触发Handler的Reactor是哪个。
同样的,启动下服务端,再开启两个客户端(跟之前一样,每个客户端发10条消息终止连接),运行结果如下:
服务端:
1号SubReactor等待注册中...
3号SubReactor等待注册中...
0号SubReactor等待注册中...
2号SubReactor等待注册中...
收到来自 /127.0.0.1:60407 的连接
0号SubReactor等待注册中...
收到来自 /127.0.0.1:60410 的连接
1号SubReactor等待注册中...
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第1条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第1条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第2条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第2条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第3条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第3条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第4条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第4条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第5条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第5条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第6条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第6条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第7条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第7条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第8条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第8条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第9条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第9条消息
0号SubReactor触发:收到来自客户端/127.0.0.1:60407的消息: 客户端发送的第10条消息
1号SubReactor触发:收到来自客户端/127.0.0.1:60410的消息: 客户端发送的第10条消息
0号SubReactor触发:read时-------连接关闭
1号SubReactor触发:read时-------连接关闭
客户端:
已完成 /127.0.0.1:2333 的连接
已完成 /127.0.0.1:2333 的连接
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第1条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第1条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第2条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第2条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第3条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第3条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第4条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第4条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第5条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第5条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第6条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第6条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第7条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第7条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第8条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第8条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第9条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第9条消息, 200ok;
收到来自服务端的消息: 0号SubReactor触发:我收到来自/127.0.0.1:60407的信息辣:客户端发送的第10条消息, 200ok;
收到来自服务端的消息: 1号SubReactor触发:我收到来自/127.0.0.1:60410的信息辣:客户端发送的第10条消息, 200ok;
到这里,主从Reactor模型就被改写完成了,上面的例子只是简单演示了下这个模型,所有的例子都是从单线程Reactor模型一点点改写来的,客户端没变过,为的是更好的测试服务端在不同模型下的表现。主从Reactor模型应用的比较多,比如著名NIO框架Netty底层模型也是基于主从Reactor模型来实现的。
到这里java nio的东西已经差不多记录完了,后续会开始netty的学习记录,当然上述例子弱化了buffer的使用,而且例子中不存在粘包拆包的问题(因为都是请求+应答的方式进行),如果把上面的例子改成客户端在未收到响应时就连续发送几条信息,服务端这时再次由写模式切换到读模式,就会从Channel里连续拿到这几条消息,这就导致了粘包问题,那么如何解决类似的问题呢?通常是定义一种协议,来区分消息头和尾,中间的消息体是我们真正需要的数据,这种协议也就是我们常说的应用层协议,比如HTTP、FTP等,这里不做赘述,之后会通过一个例子来完成这部分的补充说明。
代码地址
单线程Reactor模型:https://github.com/exceting/DemoAll/tree/master/jdk/src/main/java/demo/jdk/reactor/simple
多线程Reactor模型:同上,Acceptor里的Handler改成AsyncHandler即可
主从多线程Reactor模型:https://github.com/exceting/DemoAll/tree/master/jdk/src/main/java/demo/jdk/reactor/mainsub
Java NIO学习与记录(八): Reactor两种多线程模型的实现的更多相关文章
- Java NIO学习与记录(六): NIO线程模型
NIO线程模型 上一篇说的是基于操作系统的IO处理模型,那么这一篇来介绍下服务器端基于IO模型和自身线程的处理方式. 一.传统阻塞IO模型下的线程处理模式 这种处理模型是基于阻塞IO进行的,上一篇讲过 ...
- Java NIO学习与记录(五): 操作系统的I/O模型
操作系统的I/O模型 在开始介绍NIO Reactor模式之前,先来介绍下操作系统的五种I/O模型,了解了这些模型,对理解java nio会有不小的帮助. 先来看下一个服务端处理一次网络请求的流程图: ...
- Java NIO学习与记录(二):FileChannel与Buffer用法与说明
FileChannel与Buffer用法与说明 上一篇简单介绍了NIO,这一篇将介绍FileChannel结合Buffer的用法,主要介绍Buffer FileChannel的简单使用&Buf ...
- Java NIO学习与记录(一):初识NIO
初识 工作中有些地方用到了netty,netty是一个NIO框架,对于NIO却不是那么熟悉,这个系列的文章是我在学习NIO时的一个记录,也期待自己可以更好的掌握NIO. 一.NIO是什么? 非阻塞式I ...
- Java NIO学习与记录(七): Reactor单线程模型的实现
Reactor单线程模型的实现 一.Selector&Channel 写这个模型需要提前了解Selector以及Channel,之前记录过FileChannel,除此之外还有以下几种Chann ...
- Java NIO学习与记录(四): SocketChannel与BIO服务器
SocketChannel与BIO服务器 SocketChannel可以创建连接TCP服务的客户端,用于为服务发送数据,SocketChannel的写操作和连接操作在非阻塞模式下不会发生阻塞,这篇文章 ...
- Java NIO学习与记录(三): Scatter&Gather介绍及使用
Scatter&Gather介绍及使用 上一篇知道了Buffer的工作机制,以及FileChannel的简单用法,这一篇介绍下 Scatter&Gather 1.Scatter(分散 ...
- Java NIO系列教程(八)JDK AIO编程
目录: Reactor(反应堆)和Proactor(前摄器) <I/O模型之三:两种高性能 I/O 设计模式 Reactor 和 Proactor> <[转]第8章 前摄器(Proa ...
- Java NIO学习笔记
Java NIO学习笔记 一 基本概念 IO 是主存和外部设备 ( 硬盘.终端和网络等 ) 拷贝数据的过程. IO 是操作系统的底层功能实现,底层通过 I/O 指令进行完成. 所有语言运行时系统提供执 ...
随机推荐
- 用MapReduce读HBase写MongoDB样例
1.版本信息: Hadoop版本:2.7.1 HBase版本:1.2.1 MongDB版本:3.4.14 2.HBase表名及数据: 3.Maven依赖: <dependency> < ...
- 8.7 正确使用索引(no)
一 索引未命中 并不是说我们创建了索引就一定会加快查询速度,若想利用索引达到预想的提高查询速度的效果,我们在添加索引时,必须遵循以下问题 1 范围问题,或者说条件不明确,条件中出现这些符号或关键字:& ...
- csdn的blog可以直接导入内含图片的word文档吗?
目前大部分的博客作者在用Word写博客这件事情上都会遇到以下3个痛点: 1.所有博客平台关闭了文档发布接口,用户无法使用Word,Windows Live Writer等工具来发布博客.使用Word写 ...
- 从原理上理解Base64编码
开发者对Base64编码肯定很熟悉,是否对它有很清晰的认识就不一定了.实际 上Base64已经简单到不能再简单了,如果对它的理解还是模棱两可实在不应该.大概介绍一下Base64的相关内容,花几分钟时间 ...
- swift学习之-- UIAlertVIewController - uiactionsheet
// // ViewController.swift // actionsheet // // Created by su on 15/12/7. // Copyright © 2015年 t ...
- NLog日志框架简写用法
本文转载:http://www.blogjava.net/qiyadeng/archive/2013/02/27/395799.html 在.net中也有非常多的日志工具,今天介绍下NLog.NLog ...
- Oracle E-Business Suite并发请求的优先级(Concurrent Request Priority)
不少用户抱怨自己的Oracle E-Business Suite并发请求(Concurrent Request)提交了好久,但还是一直在排队,等了好久还没有执行.用户希望对于一些重要性程度高.响应要求 ...
- gvim 全屏 插件
1.这里下载插件zip解压后,将fullscreen.dll放到gvim.exe同目录下 2.执行 :call libcallnr()//切换全屏模式 3.上面的命令非常麻烦,可以在_vimrc中ma ...
- linux下切换用户出现This account is currently not available
今天在一台新服务器下切换用户的时候出现“This account is currently not available”错误上网检索了一 下发现是用户的shell禁止登录的问题 解决办法:比如我是 s ...
- R语言中Fisher判别的使用方法
最近编写了Fisher判别的相关代码时,需要与已有软件比照结果以确定自己代码的正确性,于是找到了安装方便且免费的R.这里把R中进行Fisher判别的方法记录下来. 1. 判别分析与Fisher判别 不 ...