ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

java高并发核心编程读书笔记

java高并发核心编程读书笔记 JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是通过JNI本地调用的方式委托操作系统的线程管理API完成的。当Java线程的Thread实例的start()方法被调用后操作系统中的对应线程进入的并不是运行状态而是就绪状态而Java线程并没有这个就绪状态。Thread.State是一个内部枚举类定义了6个枚举常量分别代表Java线程的6种状态具体如下publicstaticenumState{NEW,//新建RUNNABLE,//可执行包含操作系统的就绪、运行两种状态BLOCKED,//阻塞WAITING,//等待TIMED_WAITING,//限时等待TERMINATED;//终止}1yield仅能使一个线程从运行状态转到就绪状态而不是阻塞状态。2yield不能保证使得当前正在运行的线程迅速转换到就绪状态。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 PoolThreadFactory为线程工厂类用于创建线程Thread。Java中的阻塞队列BlockingQueue与普通队列相比有一个重要的特点在阻塞队列为空时会阻塞当前线程的元素获取操作。具体来说在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞直到阻塞队列中有了元素当队列中有元素后被阻塞的线程会自动被唤醒唤醒过程不需要用户程序干预。由于IO密集型任务的CPU使用率较低导致线程空余时间很多因此通常需要开CPU核心数两倍的线程。corePoolSize和maximumPoolSize保持一致使得在接收到新任务时如果没有空闲工作线程就优先创建新的线程去执行新任务而不是优先加入阻塞队列.CPU密集型任务并行执行的数量应当等于CPU的核心数。多线程适用的场景一般是存在相当比例非CPU耗时操作如IO、网络操作需要尽量提高并行化比率以提升CPU的利用率。当GC发生时无论内存够不够仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。什么是内存泄漏不再用到的内存没有及时释放归还给系统就叫作内存泄漏。使用ThreadLocal会发生内存泄漏的前提条件如下1线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。2ThreadLocal引用被设置为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模式实现有时也称为异步阻塞IOJava中的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 1000000hard nofile 1000000soft nofile表示软性极限hard nofile表示硬性极限。FileInputStreamFileOutputStreamFileInputStream fis new FileInputStream(srcFile);RandomAccessFile rFile newRandomAccessFile(“filename.txt”“rw”);已上均可getChannel()publicstaticvoidnioCopyFile(StringsrcPath,StringdestPath){FilesrcFilenewFile(srcPath);FiledestFilenewFile(destPath);try{//如果目标文件不存在则新建if(!destFile.exists()){destFile.createNewFile();}longstartTimeSystem.currentTimeMillis();FileInputStreamfisnull;FileOutputStreamfosnull;FileChannelinChannelnull;//输入通道FileChanneloutchannelnull;//输出通道try{fisnewFileInputStream(srcFile);fosnewFileOutputStream(destFile);inChannelfis.getChannel();outchannelfos.getChannel();intlength-1;//新建buf处于写模式ByteBufferbufByteBuffer.allocate(1024);//从输入通道读取到bufwhile((lengthinChannel.read(buf))!-1){//buf第一次模式切换翻转buf从写模式变成读模式buf.flip();intoutlength0;//将buf写入输出的通道while((outlengthoutchannel.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);}longendTimeSystem.currentTimeMillis();Logger.info(base复制毫秒数(endTime-startTime));}catch(IOExceptione){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的默认类型是DirectByteBuf。注意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中有足够的字节则会正常读取反之则会停止解码。packagecom.crazymakercircle.netty.decoder;//…publicclassByte2IntegerReplayDecoderextendsReplayingDecoder{Overridepublicvoiddecode(ChannelHandlerContextctx,ByteBufin,ListObjectout){intiin.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~2324 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://。
返回列表