JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是
通过JNI本地调用的方式委托操作系统的线程管理API完成的。当Java
线程的Thread实例的start()方法被调用后,操作系统中的对应线程进
入的并不是运行状态,而是就绪状态,而Java线程并没有这个就绪状
态。

Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表
Java线程的6种状态,具体如下:

public static enum State {
 NEW, //新建
 RUNNABLE, //可执行:包含操作系统的就绪、运
行两种状态
 BLOCKED, //阻塞
 WAITING, //等待
 TIMED_WAITING, //限时等待
 TERMINATED; //终止
 }

在这里插入图片描述

(1)yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
(2)yield不能保证使得当前正在运行的线程迅速转换到就绪状
态。
(3)即使完成了迅速切换,系统通过线程调度机制从所有就绪线
程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不
被选中,其调度的过程受到其他因素(如优先级)的影响。

在JVM退出时,守护线程daemonThread还远远没有结束,还在死循环的执行中。但是JVM不管这些,强行终止了所有守护线程的执行。

从是否为守护线程的角度,对Java线程进行分类,分为用户线程和守护线程。守护线程和用户线程的本质区别是:二者与JVM虚拟机进程终止的方向不同。用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止;守护线程和JVM进程是被动关
系,如果JVM进程终止,所有的守护线程也随之终止,
但是在终止维度上,守护线程和JVM进程没有主动关系。也就是说,哪怕是守护线程全部被终止,JVM虚拟机也不一定终止。

(1)如果线程为守护线程,就必须在线程实例的start()方
法调用之前调用线程实例的setDaemon(true),设置其daemon实例属性
值为true。
(2)守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。
(3)守护线程创建的线程也是守护线程。在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。

ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终 止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所
以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的 数据统计,方便线程的管理和监控。

ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序
中,ScheduledThreadPoolExecutor的性能要优于Timer。

在ThreadPoolExecutor类的实现中,内部核心的任务提交方法是
execute()方法,虽然用户程序通过submit()也可以提交任务,但是实
际上submit()方法中最终调用的还是execute()方法。

线程池的调度器创建线程的一条重要的规则是:在
corePoolSize已满之后,还需要等阻塞队列已满,才会去创建新的线
程。

Executors.defaultThreadFactory默认实例。使用默认的线程工厂实
例所创建的线程全部位于同一个ThreadGroup(线程组)中,具有相同
的NORM_PRIORITY(优先级为5),而且都是非守护进程状态。

这里提到了两个工厂类,比较容易混淆,故做出说明。Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);ThreadFactory为线程工厂类,用于创建线程(Thread)。

Java中的阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说,在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。

由于IO密集型任务的CPU使用率较低,导致线程空余时间很多,因此通常需要开CPU核心数两倍的线程。
corePoolSize和maximumPoolSize保持一致,使得在接收到新任务时,如果没有空闲工作线程,就优先创建新的线程去执行新任务,而不是优先加入阻塞队列.

CPU密集型任务并行执行的数量应当等于CPU的核心数。

多线程适用的场景一般是:存在相当
比例非CPU耗时操作,如IO、网络操作,需要尽量提高并行化比率以提
升CPU的利用率。

当GC发生时,无论内存够
不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对
象则不会被直接回收。

在这里插入图片描述

什么是内存泄漏?不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。

使用ThreadLocal会发生内存泄漏的前提条件如下:
(1)线程长时间运行而没有被销毁。线程池中的Thread实例很容
易满足此条件。
(2)ThreadLocal引用被设置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry。

凡事都有两面性,使用static、final修饰ThreadLocal实例也会
带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在
Thread实例的生命期内将始终保持为非null,从而导致Key所在的
Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在
强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致
内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之
后,必须调用remove()来进行显式的释放操作。

临界区资源表示一种可以被多个线程使用的公共资源或共享数
据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想
使用该资源的其他线程则必须等待。

卷1

“不可重复读”和“幻读”的区别是:“不可重复读”关注的
重点在于记录的更新操作,对同样的记录,再次读取后发现返回的
数据值不一样了;“幻读”关注的重点在于记录新增或者删除操作
(数据条数发生了变化),同样的条件第一次和第二次查询出来的
记录数不一样。

上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到
应用的缓冲区(进程缓冲区);上层应用使用write系统调用时,仅仅
把数据从应用的缓冲区复制到内核缓冲区。

阻塞和非阻塞的区别是什么呢?阻塞是指用户进程(或者线程)
一直在等待,而不能做别的事情;非阻塞是指用户进程(或者线程)
获得内核返回的状态值就返回自己的空间,可以去做别的事情。在
Java中,非阻塞IO的socket被设置为NONBLOCK模式。

为了提高性能,操作系统引入了一种新的系统调用,专门用于查
询IO文件描述符(含socket连接)的就绪状态。在Linux系统中,新的
系统调用为select/epoll系统调用。通过该系统调用,一个用户进程
(或者线程)可以监视多个文件描述符,一旦某个描述符就绪(一般
是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回
给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态
进行相应的IO系统调用。

IO多路复用(IO Multiplexing)属于一种经典的Reactor模式实
现,有时也称为异步阻塞IO,Java中的Selector属于这种模型。

同步非阻塞IO的特点是应用程序的线程需要不断地进行IO系统调
用,轮询数据是否已经准备好,如果没有准备好就继续轮询,直到完
成IO系统调用为止。
同步非阻塞IO的优点是每次发起的IO系统调用在内核等待数据过
程中可以立即返回,用户线程不会阻塞,实时性较好。
同步非阻塞IO的缺点是不断地轮询内核,这将占用大量的CPU时
间,效率低下。

举个例子来说明IO多路复用模型的流程。发起一个多路复用IO的
read操作的系统调用,流程如下:
(1)选择器注册。首先,将需要read操作的目标文件描述符
(socket连接)提前注册到Linux的select/epoll选择器中,在Java中
所对应的选择器类是Selector类。然后,开启整个IO多路复用模型的
轮询流程。
(2)就绪状态的轮询。通过选择器的查询方法,查询所有提前注
册过的目标文件描述符(socket连接)的IO就绪状态。通过查询的系
统调用,内核会返回一个就绪的socket列表。当任何一个注册过的
socket中的数据准备好或者就绪了就说明内核缓冲区有数据了,内核
将该socket加入就绪的列表中,并且返回就绪事件。
(3)用户线程获得了就绪状态的列表后,根据其中的socket连接
发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内
核缓冲区复制到用户缓冲区。
(4)复制完成后,内核返回结果,用户线程才会解除阻塞的状
态,用户线程读取到了数据,继续执行。

IO多路复用模型与同步非阻塞IO模型是有密切关系的,具体来
说,注册在选择器上的每一个可以查询的socket连接一般都设置成同
步非阻塞模型,只是这一点对于用户程序而言是无感知的。

IO多路复用模型的优点是一个选择器查询线程可以同时处理成千
上万的网络连接,所以用户程序不必创建大量的线程,也不必维护这
些线程,从而大大减少了系统的开销。与一个线程维护一个连接的阻
塞IO模式相比,这一点是IO多路复用模型的最大优势。

通过JDK的源码可以看出,Java语言的NIO组件在Linux系统上是使
用epoll系统调用实现的。所以,Java语言的NIO组件所使用的就是IO
多路复用模型。

IO多路复用模型的缺点是,本质上select/epoll系统调用是阻塞
式的,属于同步IO,需要在读写事件就绪后由系统调用本身负责读
写,也就是说这个读写过程是阻塞的。要彻底地解除线程的阻塞,就
必须使用异步IO模型。

在异步IO模型中,在整个内核的数据处理过程(包括内核将数据
从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复
制到用户缓冲区)中,用户程序都不需要阻塞。

举个例子,发起一个异步IO的read操作的系统调用,流程如下:
(1)当用户线程发起了read系统调用后,立刻就可以去做其他的
事,用户线程不阻塞。
(2)内核开始IO的第一个阶段:准备数据。准备好数据,内核就
会将数据从内核缓冲区复制到用户缓冲区。
(3)内核会给用户线程发送一个信号(Signal),或者回调用户
线程注册的回调方法,告诉用户线程read系统调用已经完成,数据已
经读入用户缓冲区。
(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。

异步IO模型的缺点是应用程序仅需要进行事件的注册与接收,其
余的工作都留给了操作系统,也就是说需要底层内核提供支持。
理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO
多路复用模型的吞吐量。就目前而言,Windows系统下通过IOCP实现了
真正的异步IO。在Linux系统下,异步IO模型在2.6版本才引入,JDK对
它的支持目前并不完善,因此异步IO在性能上没有明显的优势。
大多数高并发服务端的程序都是基于Linux系统的。因而,目前这
类高并发网络应用程序的开发大多采用IO多路复用模型。大名鼎鼎的
Netty框架使用的就是IO多路复用模型,而不是异步IO模型。

在生
产环境Linux系统中,基本上都需要解除文件句柄数的限制。原因是
Linux系统的默认值为1024,也就是说,一个进程最多可以接受1024个
socket连接,这是远远不够的。

ulimit命令只能用于临时修改,如果想永久地把最大文件描述符
数量值保存下来,可以编辑/etc/rc.local开机启动文件,在文件中添
加如下内容:
ulimit -SHn 1000000
以上示例增加了-S和-H两个命令选项。选项-S表示软性极限值,-
H表示硬性极限值。硬性极限值是实际的限制,就是最大可以是100
万,不能再多了。软性极限值则是系统发出警告(Warning)的极限
值,超过这个极限值,内核会发出警告。

普通用户通过ulimit命令可将软性极限值更改到硬性极限值的最
大设置值。如果要更改硬性极限值,必须拥有root用户权限。
要彻底解除Linux系统的最大文件打开数量的限制,可以通过编辑
Linux的极限配置文件/etc/security/limits.conf来做到。修改此文
件,加入如下内容:
soft nofile 1000000
hard nofile 1000000
soft nofile表示软性极限,hard nofile表示硬性极限。

在这里插入图片描述

FileInputStream
FileOutputStream
FileInputStream fis = new FileInputStream(srcFile);
RandomAccessFile rFile = new
RandomAccessFile(“filename.txt”,“rw”);
已上均可getChannel()

public static void nioCopyFile(String srcPath, String
            destPath) {
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        try {
            //如果目标文件不存在,则新建
            if (!destFile.exists()) {
                destFile.createNewFile();
            }
            long startTime = System.currentTimeMillis();
            FileInputStream fis = null;
            FileOutputStream fos = null;
            FileChannel inChannel = null; //输入通道
            FileChannel outchannel = null; //输出通道
            try {
                fis = new FileInputStream(srcFile);
                fos = new FileOutputStream(destFile);
                inChannel = fis.getChannel();
                outchannel = fos.getChannel();
                int length = -1;
                //新建buf,处于写模式
                ByteBufferbuf = ByteBuffer.allocate(1024);
                //从输入通道读取到buf
                while ((length = inChannel.read(buf)) != -1) {
                    //buf第一次模式切换:翻转buf,从写模式变成读模式
                    buf.flip();
                    int outlength = 0;
                    //将buf写入输出的通道
                    while ((outlength = outchannel.write(buf)) !=
                            0) {
                        System.out.println("写入的字节数:" +
                                outlength);
                    }
                    //buf第二次模式切换:清除buf,变成写模式
                    buf.clear();
                }
                //强制刷新到磁盘
                outchannel.force(true);
            } finally {
                //关闭所有的可关闭对象
                IOUtil.closeQuietly(outchannel);
                IOUtil.closeQuietly(fos);
                IOUtil.closeQuietly(inChannel);
                IOUtil.closeQuietly(fis);
            }
            long endTime = System.currentTimeMillis();
            Logger.info("base复制毫秒数:" + (endTime - startTime));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
/**
上面示例代码的主要目的在于演示文件通道以及字节缓冲区的使
用。作为文件复制的程序来说,以上实战代码的效率不是最高的。
*/

//调用终止输出方法,向对方发送一个输出的结束标志
socketChannel.shutdownOutput();

在NIO编程中,一般是一个单线程处理一个选择器,一个选择器可
以监控很多通道。

FileChannel不能与选择器一起使用,因为FileChannel只有阻塞模
式,不能切换到非阻塞模式;而socket相关的所有通道都可以。其
次,一个通道并不一定支持所有的四种IO事件。例如,服务器监听通
道ServerSocketChannel仅支持Accept(接收到新连接)IO事件,而传
输通道SocketChannel则不同,它不支持Accept类型的IO事件。
如何判断通道支持哪些事件呢?可以在注册之前通过通道的
validOps()方法来获取该通道支持的所有IO事件集合。

Reactor模式有点类似事件驱动模式。在事件驱动模式
中,当有事件触发时,事件源会将事件分发到Handler(处理器),由
Handler负责事件处理。Reactor模式中的反应器角色类似于事件驱动
模式中的事件分发器(Dispatcher)角色。

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

可以调用hasArray()方法来判断是否为Heap ByteBuf类型的缓
冲区;如果hasArray()返回值为true,则表示是堆缓冲,否则
为直接内存缓冲区。

Direct ByteBuf的hasArray()会返回false;反过来,如果
hasArray()返回false,不一定代表缓冲区一定就是Direct ByteBuf,
也有可能是CompositeByteBuf。CompositeByteBuf缓冲区是Netty为了
减少内存复制而提供的组合缓冲区

如果Handler业务处理器需要截断流水线的处理流程,不将
ByteBuf数据包送入流水线末端的TailContext入站处理器,并且也不
愿意手动释放ByteBuf缓冲区实例,那么该怎么办呢?继承
SimpleChannelInboundHandler,利用它的自动释放功能来完成。

从Netty 4.1开始,ByteBuf的默认类型是Direct
ByteBuf。注意,Java不能直接访问Direct ByteBuf内部的数据,必须
通过调用getBytes()、readBytes()等方法将数据读入Java数组中才能
继续进行处理。

一个特殊的Netty注解:
@ChannelHandler.Sharable。这个注解的作用是标注一个Handler实例
可以被多个通道安全地共享(多个通道的流水线可以加入同一个
Handler实例)。这种共享操作,Netty默认是不允许的。
如何判断一个Handler是否为@Sharable呢?
ChannelHandlerAdapter提供了实用方法——isSharable()。如果其对
应的实现加上了@Sharable注解,那么这个方法将返回true,表示它可
以被添加到多个ChannelPipeline中。

ByteToMessageDecoder传递给下一站的是解码之
后的Java POJO对象,不是ByteBuf缓冲区。那么问题来了,ByteBuf缓
冲区并没有发送到流水线的TailContext(尾部处理器),将由谁负责
释放引用计数呢?其实,基类ByteToMessageDecoder会完成ByteBuf释
放工作,它会调用ReferenceCountUtil.release(in)方法将之前的
ByteBuf缓冲区的引用计数减1。
这个ByteBuf先被释放了,如果在后面还需要用到,怎么办?可以
在子类的decode()方法中调用一次ReferenceCountUtil.retain(in)来
增加一次引用计数,不过在使用完成后要及时将增加的这次计数减去。

ReplayingDecoder类是ByteToMessageDecoder的子类,作用是:
在读取ByteBuf缓冲区的数据之前,需要检查缓冲区是否有足够
的字节。
若ByteBuf中有足够的字节,则会正常读取;反之,则会停止解
码。

package com.crazymakercircle.netty.decoder;
//…
public class Byte2IntegerReplayDecoder extends ReplayingDecoder
{
 @Override
 public void decode(ChannelHandlerContext ctx,
 ByteBuf in, List<Object>
out) {
 int i = in.readInt();
 Logger.info("解码出一个整数: " + i);
 out.add(i);
 }
}

实质上,ReplayingDecoder的作用远远不止于进行长度判断,它
更重要的作用是用于分包传输的应用场景。

测试用例中除了需要使用String2IntegerEncoder编码器外,还需
要用到Integer2ByteEncoder编码器。String2IntegerEncoder仅仅是
编码的第一棒,负责将字符串编码成整数;Integer2ByteEncoder是编
码的第二棒,将整数进一步变成ByteBuf数据包后才能最终写入通道。
由于出站处理的过程是从后向前的次序,因此Integer2ByteEncoder先
加入流水线,String2IntegerEncoder后加入流水线。

在实际开发中,目前主流的策略是Gson和FastJson结合使用。在
POJO序列化成JSON字符串的应用场景下,使用谷歌的Gson库;在JSON
字符串反序列化成POJO的应用场景下,使用阿里巴巴的FastJson库。

微信的消息传输就采用了Protobuf协议

varint32是一种紧凑的表示数字的方法,不是一种固定长度(如
32位)的数字类型。varint32用一个或多个字节来表示一个数字,值
越小,使用的字节数越少,值越大使用的字节数越多。varint32根据
值的大小自动进行收缩,能够减少用于保存长度的字节数。也就是
说,varint32与int类型的最大区别是:varint32用一个或多个字节来
表示一个数字,int是固定长度的数字。varint32不是固定长度,所以
为了更好地减少通信过程中的传输量,消息头中的长度尽量采用
varint格式。

Protobuf建议字段的命名以下划线分隔(例如first_name),而不是
驼峰式(例如firstName)。

分配标识号的取值范围为1~232(4 294 967 296)。其中,编号
[1, 15]之内的分配标识号,时间和空间效率都是最高的。因为[1,
15]之内的标识号在编码的时候只会占用一个字节,[16, 2047]之内的
标识号要占用两个字节。所以,那些频繁出现的消息字段应该使用[1,
15]之内的标识号。切记:要为将来有可能添加的、频繁出现的字段预
留一些标识号。另外,[1900, 2000]之内的标识号为Protobuf内部保
留值,建议不要在自己的项目中使用。

标识号的特点是:一个消息结构体中的标识号是可以不连续的;
在同一个消息结构体中,不同的字段不能使用相同的标识号。

定长编码(如fixed32)和变长编码(如int32)的区别是:
fixed32的打包效率比int32的效率高,但是使用的空间一般比int32
多。因此,定长编码时间效率高,变长编码空间效率高,可以根据项
目的实际情况选择。一般情况下可以选择fixed32,但是遇到对传输效
率要求比较苛刻的环境时,可以选择int32。

WebSocket协议和HTTP有一个显著的不同:HTTP是
单向通信协议,只有客户端发起HTTP请求,服务端才会返回数据;
WebSocket协议是双向通信协议,在建立连接之后,客户端和服务器都
可以主动向对方发送或接收数据。
WebSocket协议和HTTP还是有关系的:WebSocket的通信连接建立
的前提需要借助HTTP,完成通信连接建立之后,通信连接上的双向通
信就与HTTP无关了。

建立WebSocket连接时,传递的URL参数没有同源策略的限制。那
么什么是同源策略呢?如果两个通信协议的URL的主机名(域名或者
IP)和端口都相同,则两个URL是同源的。同源策略是浏览器的一个安
全功能,不同源的客户端脚本在没有明确授权的情况下不能读写对方
资源。WebSocket并不受同源策略的限制,可以向不同源的URL发起
WebSocket连接请求。

WebSocket有自己的协议规范,其URL规则与HTTP的URL规则不同。
WebSocket中未加密的URL Schema为ws://,而不是http://。
WebSocket中加密的URL Schema为wss://,而不是https://。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐