Netty权威指南——基础篇1(同步阻塞IO-BIO)

1 Linux网络I/O模型简介

1.1 简述

        Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。根据UNIX网络编程对I/O模型的分类,UNIX提供了五种I/O模型。

1.2  五种I/O模型介绍

        阻塞I/O模型:

        最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的。以套接字接口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误时才返回,再次期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此成为阻塞I/O模型模型。如下图所示:

        非阻塞 I/O模型:

        recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞模型进行轮询检查这个状态,看内核是否有数据到来,如下图:

        I/O复用模型:

        Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态状态。select/poll是顺序扫描fd是否就绪,而且支持df数量有限,因此它的使用受到了一些限制。Linux还提供了一个epoll系统调用,epoll使用基于时间驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数,如下图:

        信号驱动I/O模型

        首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通过主循环函数处理数据,如下图:

         异步I/O

        告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已完成,如下图:

1.3 I/O多路复用技术

        在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O 多路复用技术通过把多个I/O 的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型相比,I/O 多路复用技术的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源,I/O 的主要应用场景如下:

        1、服务器需要同时处理多个处于监听状态或者多个连接状态的套接字

        2、服务器需要同时处理多种网络协议的套接字。

        目前支持I/O 多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到很大限制,最终Linux选择了epoll。epoll与select的原理比较类型,为了克服select的缺点,epoll做了很多重大改进。总结如下:

        1、支持一个进程打开的socket描述符不受限制(仅受限于操作系统的最大文件句柄数)。

        select最大缺陷就是单个进程能够打开的fd是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接发大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降,也可以通过选择多进程的方案来解决这个问题,不过虽然在Linux上创建进程的代价比较大,但仍旧是不可忽视的。另外,进程间的数据交换非常麻烦,对于Java来说,由于没有共享内存,需要通过Socket通信或其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。而epoll没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024.例如,在1GB内存的机器上大约有10万个句柄左右,这个值跟系统的内存关系比较大。

        2、I/O 效率不会随着FD数量的增加而线性下降

        传统select/poll的另一个致命弱点,就是当你拥有一个很大的socket集合时,由于网络延时造成链路空闲,任一时刻只有少部分的socket是活跃的,但是select/poll每次调用都会线性扫描全部集合,导致效率呈线性下降。epoll不存在这个问题,它只会对活跃的socket进行操作——这是因为在内核实现中,epoll是根据每个fd上面的callback函数实现的。那么,只有活跃的socket才会去主动调用callback函数,其他idle状态的socket则不会。在这点上,epoll实现了一个伪AIO。针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于一个活跃态——例如一个高速LAN环境,epoll并不比select/poll效果高太多;相反,如果过多使用epoll_ctl,效率相比还有稍微的降低。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/epoll之上了。

        3、使用mmap加速内核与用户空间的消息传递

        无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存来实现。

        4、epoll的API更简单

        包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生、关闭epoll描述符等。

2 传统的BIO编程

        网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端位置信息(绑定的IP地址和监听端口),客户端通过连接操作箱服务端监听的地址发起连接请求,通过的三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

        在基于传统同步阻塞模型并发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

        下面以时间服务器(TimeServer)为例,通过代码分析来回顾和熟悉BIO编程。

2.1 BIO通信模型图

        首先,通过下图所示的通信模型来熟悉BIO的服务端通信模型

        采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。就是代行的一请求一应答通信模型。

        该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

2.2 同步阻塞式I/O创建的TimeServer源码分析

package BIO;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;


public class TimeServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        if(args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);
            }catch (NumberFormatException e){
                e.printStackTrace();
            }
        }
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("时间服务器开始启动,端口号是:"+port);
            Socket socket = null;
            while(true){
                socket = serverSocket.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(serverSocket != null){
                System.out.println("时间服务器关闭");
                serverSocket.close();
                serverSocket = null;
            }
        }
    }
}
TimeServerHandler.java
package com.jay.BIO;

import org.ietf.jgss.Oid;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

public class TimeServerHandler implements Runnable{
    private  Socket socket;

    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run(){
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            in = new BufferedReader(new InputStreamReader((this.socket.getInputStream())));
            out = new PrintWriter(this.socket.getOutputStream(),true);
            String currentTime = null;
            String body = null;
            while(true){
                body = in.readLine();
                if(body == null){
                    break;
                }
                System.out.println("TimeServer收到信息:"+body);
                currentTime = "query".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "bad";
                out.println(currentTime);
            }
        }catch (Exception e){
            if(in != null){
                try {
                    in.close();
                }catch (IOException e1){
                    e1.printStackTrace();
                }
            }
            if(out != null){
                out.close();
                out = null;
            }
            if(this.socket != null){
                try {
                    this.socket.close();
                }catch (IOException e2){
                    e2.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}

       TimeServer.java 分析

        TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080.然后通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。然后通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。启动TimeServer,发现主线程阻塞在accept上。当有新的客户端接入的时候,执行这行代码

new Thread(new TimeServerHandler(socket)).start();

        以Socket为参数构造TimeServerHandler对象,  TimeServerHandler是一个Runnable,使用它它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。

         TimeServerHandler.java 分析

        通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到非空值,则对内容进行判断,如果请求消息为查询时间的指令“query”,则获取当前最新的系统时间,如果PrintWrite的println函数发送给客户端,最后退出循环。最后面为释放输入流、输出流和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。

2.3 同步阻塞式I/O创建的TimeClient源码分析

        客户端通过Socket创建,发送查询时间服务器的“query”指令,然后读取服务端的相应并把结果打印出来,随后关闭连接,释放资源,程序退出执行。

package BIO;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;


public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        if(args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);
            }catch (NumberFormatException e){
                e.printStackTrace();
            }
        }
        Socket socket = null;
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            socket = new Socket("127.0.0.1",port);
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(),true);
            out.println("query");
            System.out.println("成功发送到服务器");
            String resp = in.readLine();
            System.out.println("现在时间是:" + resp);
        }catch (Exception e){
            
        }finally {
            if(out != null){
                out.close();
                out = null;
            }
            if(in != null){
                try {
                    in.close();
                }catch (IOException e1){
                    e1.printStackTrace();
                }
                in = null;
            }
            if(socket != null){
                try {
                    socket.close();
                }catch (IOException e2){
                    e2.printStackTrace();
                }
                socket = null;
            }
        }
    }
}

        客户端通过PrintWrite想服务端发送“query”指令,然后通过BufferedReader的readLine读取响应并打印。分别执行服务端和客户端,执行结果如下。

        服务器运行结果如下:

       客户端执行结果如下:

  

        为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”。下面对伪异步代码进行分析。

3 伪异步I/O编程

        为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化——后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N。通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

3.1 伪异步I/O模型图

        采用线程池和任务队列可以实现一种伪异步的I/O 通信框架,它的模型如下图:

        当有新的客户端接入时,将客户端的Socket封装成一个Task(该任务实现Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

3.2 伪异步I/O创建TimeServer源码分析

TimeServerAsyn.java代码如下:
public class TimeServerAsync {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        if(args != null && args.length > 0){
            try {
                port = Integer.valueOf(args[0]);

            }catch (NumberFormatException e){
                e.printStackTrace();
            }
        }
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("TimeServer 启动,端口号是:"+port);
            Socket socket = null;
            TimeServerHandlerExecutePool singleExecute = new TimeServerHandlerExecutePool(50, 10000);
            while(true){
                socket = server.accept();
                singleExecute.execute(new TimeServerHandler(socket));
            }
        }finally {
            if(server != null){
                System.out.println("关闭TimeServer ");
                server.close();
                server = null;
            }
        }
    }
}

        伪异步I/O的主函数发生了变化,首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接时,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。

TimeServerHandlerExecutePool.java 源码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TimeServerHandlerExecutePool {
    private ExecutorService executorService;

    public TimeServerHandlerExecutePool(int maxPoolSize,int queueSize) {
        executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxPoolSize,120L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(queueSize));

    }
    public void execute(Runnable task){
        executorService.execute(task);
    }
}

        客户端代码没有改变,因此直接运行服务端和客户端,执行结果如下:

        伪异步I/O虽然避免了每个请求都创建一个独立线程造成的线程资源耗尽问题。但由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

3.3 伪异步I/O弊端分析

        要对伪异步I/O的弊端进行深入分析,首先看两个java同步I/O的API说明,随后结合代码进行详细分析,先看InputStream.java的read()方法:

/**
    This method blocks until input data is
     * available, end of file is detected, or an exception is thrown.
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

         请看注释,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三件事情:

        1、有数据可读。2、可用数据已经读取完毕。3、发生异常。

        这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60秒才能将数据发送完成,读取一方的I/O线程也将会被同步阻塞60秒,在此期间,其他接入消息只能在消息队列中排队。

        同样,当调用OutPutStream的write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP Window size不断减少,直到为0,双方处理Keep-Alive状态,消息发送方将不能再想TCP缓冲区写入消息,这是如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP Window size大于0或者发生I/O异常。

        伪异步I/O实际上仅仅是对之前I/O线程模型的一个简单优化,无法从根本上解决同步I/O导致的通信线程阻塞问题。

        

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

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

相关文章

SpringBoot原理篇

文章目录 SpingBoot原理1. 配置优先级2. Bean管理2.1 获取Bean2.2 Bean作用域2.3 第三方Bean 3. SpringBoot原理3.1 起步依赖3.2 自动配置3.2.1 概述3.2.2 常见方案3.2.2.1 概述3.2.2.2 方案一3.2.2.3 方案二 3.2.3 原理分析3.2.3.1 源码跟踪3.2.3.2 Conditional 3.2.4 案例3.2…

深入探究Nginx的使用方法

目录 引言 一、网络状态页 二、Nginx 第三方模块 三、变量 &#xff08;一&#xff09;内置变量 &#xff08;二&#xff09;自定义变量 四、自定义日志 &#xff08;一&#xff09;有关日志的配置信息 &#xff08;二&#xff09;error日志的设置 1.日志的等级 2.自…

怎么样避免被企业裁掉呢?

在当前经济环境下&#xff0c;许多企业纷纷选择裁员以降低成本、提升效益。面对这一现象&#xff0c;员工如何避免成为裁员风波中的牺牲品呢&#xff1f;本文将从多个角度为您提供应对策略。 首先&#xff0c;要了解企业裁员的背景和原因。金融危机、行业变革、市场竞争等外部…

第十篇【传奇开心果系列】Python的文本和语音相互转换库技术点案例示例:Microsoft Azure开发语音翻译应用程序经典案例

传奇开心果博文系列 系列博文目录Python的文本和语音相互转换库技术点案例示例系列 博文目录前言一、雏形示例代码二、扩展思路介绍三、Azure多语种支持示例代码四、Azure实时对话模式示例代码五、Azure自定义翻译模型示例代码六、Azure语音合成示例代码七、Azure用户界面优化示…

接口自动化测试用例如何设计

说到自动化测试&#xff0c;或者说接口自动化测试&#xff0c;多数人的第一反应是该用什么工具&#xff0c;比如&#xff1a;Python Requests、Java HttpClient、Apifox、MeterSphere、自研的自动化平台等。大家似乎更关注的是哪个工具更优秀&#xff0c;甚至出现“ 做平台的 &…

如果发现某个地方太薄了想要加厚怎么办?

Q 做完模型后&#xff0c;发现斧头柄部太薄了想要加厚怎么办&#xff1f; A 使用圆形套索区域&#xff0c;选中点 然后左视图&#xff0c;选择缩放&#xff0c;横向拉宽即可

c# 广度优先搜索(Breadth-First Search,BFS)

在这篇文章中我将讨论用于树和图的两种遍历机制之一。将使用 C# 示例介绍广度优先搜索 (BFS)。图是最具挑战性和最复杂的数据结构之一。 广度优先搜索的工作原理&#xff1a;广度优先搜索 &#xff08;BFS&#xff09;是一种探索树或图的方法。在 BFS 中&#xff0c;您首先探索…

实战 vue3 使用百度编辑器ueditor

前言 在开发项目由于需求vue自带对编辑器不能满足使用&#xff0c;所以改为百度编辑器&#xff0c;但是在网上搜索发现都讲得非常乱&#xff0c;所以写一篇使用流程的文章 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、下载ueditor编辑器 一个“…

好书推荐丨细说Python编程:从入门到科学计算

文章目录 写在前面Python简介推荐图书内容简介编辑推荐作者简介 推荐理由粉丝福利写在最后 写在前面 本期博主给大家推荐一本Python基础入门的全新正版书籍&#xff0c;对Python、机器学习、人工智能感兴趣的小伙伴们快来看看吧~ Python简介 Python 是一种广泛使用的高级、解…

基于插件实现RabbitMQ“延时队列“

1.官网下载 在添加链接描述下载rabbitmq_delayed_message_exchange 插件,本文以v3.10.0为例 1.1.上传安装包 scp /Users/hong/资料/rabbitmq_delayed_message_exchange-3.10.0.ez root10.211.55.4:/usr/local/software1.2.将文件移入RabbitMQ的安装目录下的plugins目录 m…

数学建模【插值与拟合】

一、插值与拟合简介 在数学建模过程中&#xff0c;通常要处理由试验、测量得到的大量数据或一些过于复杂而不便于计算的函数表达式&#xff0c;针对此情况&#xff0c;很自然的想法就是&#xff0c;构造一个简单的函数作为要考察数据或复杂函数的近似。插值和拟合就可以解决这…

【愚公系列】2024年02月 大数据教学课程 017-Hadoop环境配置

&#x1f3c6; 作者简介&#xff0c;愚公搬代码 &#x1f3c6;《头衔》&#xff1a;华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xf…

FPGA 与 数字电路的关系 - 这篇文章 将 持续 更新 :)

先说几个逻辑&#xff1a;&#xff08;强调一下在这篇文章 输入路数 只有 1个或2个&#xff0c;输出只有1个&#xff0c;N个输入M个输出以后再说&#xff09; 看下面的几个图&#xff1a; 图一&#xff08; 忘了 这是 啥门&#xff0c;不是门吧 &#xff1a;&#xff09;也就…

【好书推荐-第五期】《Java开发坑点解析:从根因分析到最佳实践》(异步图书出品)

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公粽号&#xff1a;程序员洲洲。 &#x1f388; 本文专栏&#xff1a;本文…

I/O流(C++)

输入输出操作是程序中必不可少的操作&#xff0c;通过输入输出可以完成程序和外界的交互。 C语言支持两种I/O操作&#xff1a; &#xff08;1&#xff09;从C语言继承来的I/O函数输入输出语句&#xff1a;scanf()、printf()函数 &#xff08;2&#xff09;面向对象的I/O流类…

动画法则与动画曲线解析

先介绍一些和代码关系不大的动画常识 挤压与拉伸(Squeeze and stretch) 当有力作用到物体身上时,物体将会产生一定的形变,比如你在拍球时,球落地后会被挤压,弹起时会产生拉伸,对于具体的挤压与拉伸的强度,与物体的硬度和用力的大小有关。做动画要遵循运动规律让动画更…

一周学会Django5 Python Web开发-Http请求HttpRequest请求类

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计25条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…

JavaWeb 自己给服务器安装SQL Server数据库遇到的坑

之前买的虚拟主机免费送了一个SQL Server数据库&#xff0c;由于服务器提供商今年下架我用的那款虚拟主机产品&#xff0c;所以数据库也被收回了。我买了阿里云云服务器&#xff0c;但是没有数据库&#xff0c;于是自己装了一个SQL Server数据库&#xff0c;总结一下遇到的坑。…

【设计模式】5种创建型模式详解

创建型模式提供创建对象的机制,能够提升已有代码的灵活性和复用性。 常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。不常用的有:原型模式。一、单例模式 1.1 单例模式介绍 1 ) 定义 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,此模…

Temu、亚马逊店铺如何快速得到好评?自养号测评下单的秘籍及必备条件。

Temu、亚马逊店铺如何快速得到好评?在这个竞争激烈的电商平台上&#xff0c;好评是店铺吸引顾客、建立良好声誉的关键。快速积累好评不仅能够提高商品的曝光度&#xff0c;也有助于吸引更多潜在顾客的关注。 然而&#xff0c;亚马逊不同于国内电商&#xff0c;对于操纵评论、…