摘要:原理剖析第篇之服務端啟動工作原理分析下一大致介紹由于篇幅過長難以發布,所以本章節接著上一節來的,上一章節為原理剖析第篇之服務端啟動工作原理分析上那么本章節就繼續分析的服務端啟動,分析的源碼版本為二三四章節請看上一章節詳見原理剖析第篇之
原理剖析(第 011 篇)Netty之服務端啟動工作原理分析(下)
-
一、大致介紹1、由于篇幅過長難以發布,所以本章節接著上一節來的,上一章節為【原理剖析(第 010 篇)Netty之服務端啟動工作原理分析(上)】; 2、那么本章節就繼續分析Netty的服務端啟動,分析Netty的源碼版本為:netty-netty-4.1.22.Final;二、三、四章節請看上一章節
詳見 原理剖析(第 010 篇)Netty之服務端啟動工作原理分析(上)
四、源碼分析Netty服務端啟動上一章節,我們主要分析了一下線程管理組對象是如何被實例化的,并且還了解到了每個線程管理組都有一個子線程數組來處理任務;
那么接下來我們就直接從4.6開始分析了:
1、源碼: // NettyServer.java // 將 Boss、Worker 設置到 ServerBootstrap 服務端引導類中 serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // 指定通道類型為NioServerSocketChannel,一種異步模式,OIO阻塞模式為OioServerSocketChannel .localAddress("localhost", port)//設置InetSocketAddress讓服務器監聽某個端口已等待客戶端連接。 .childHandler(new ChannelInitializer4.7、serverBootstrap調用bind綁定注冊() {//設置childHandler執行所有的連接請求 @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new PacketHeadDecoder()); ch.pipeline().addLast(new PacketBodyDecoder()); ch.pipeline().addLast(new PacketHeadEncoder()); ch.pipeline().addLast(new PacketBodyEncoder()); ch.pipeline().addLast(new PacketHandler()); } }); 2、主要為后序的通信設置了一些配置參數而已,指定構建的Channel為NioServerSocketChannel,說明需要啟動的是服務端Netty; 而后面的服務端Channel實例化,就是需要通過這個參數反射實例化得到; 3、同時還設置childHandler,這個childHandler也是有順序的,服務端讀數據時執行的順序是PacketHeadDecoder、PacketBodyDecoder、PacketHandler; 而服務端寫數據時執行的順序是PacketHandler、PacketBodyEncoder、PacketHeadEncoder; 所以在書寫方式大家千萬別寫錯了,按照本示例代碼的方式書寫即可;
1、源碼: // NettyServer.java // 最后綁定服務器等待直到綁定完成,調用sync()方法會阻塞直到服務器完成綁定,然后服務器等待通道關閉,因為使用sync(),所以關閉操作也會被阻塞。 ChannelFuture channelFuture = serverBootstrap.bind().sync(); 2、這里其實沒什么好看的,接下來我們就主要看看這個bind()方法主要干了些啥,就這么簡簡單單一句代碼就把服務端給啟動起來了,有點神氣了;4.8、bind()操作
1、源碼: // AbstractBootstrap.java /** * Create a new {@link Channel} and bind it. */ public ChannelFuture bind() { validate(); SocketAddress localAddress = this.localAddress; if (localAddress == null) { throw new IllegalStateException("localAddress not set"); } return doBind(localAddress); // 創建一個Channel,并且綁定它 } // AbstractBootstrap.java private ChannelFuture doBind(final SocketAddress localAddress) { final ChannelFuture regFuture = initAndRegister(); // 初始化和注冊 // 執行到此,服務端大概完成了以下幾件事情: // 1、實例化NioServerSocketChannel,并為Channel配備了pipeline、config、unsafe對象; // 2、將多個handler添加至pipeline雙向鏈表中,并且等待Channel注冊成功后需要給每個handler觸發添加或者移除事件; // 3、將NioServerSocketChannel注冊到NioEventLoop的多路復用器上; final Channel channel = regFuture.channel(); if (regFuture.cause() != null) { return regFuture; } // 既然NioServerSocketChannel的Channel綁定到了多路復用器上,那么接下來就是綁定地址,綁完地址就可以正式進行通信了 if (regFuture.isDone()) { // At this point we know that the registration was complete and successful. ChannelPromise promise = channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { // Registration future is almost always fulfilled already, but just in case it"s not. final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause = future.cause(); if (cause != null) { // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an // IllegalStateException once we try to access the EventLoop of the Channel. promise.setFailure(cause); } else { // Registration was successful, so set the correct executor to use. // See https://github.com/netty/netty/issues/2586 promise.registered(); doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } } 2、大致一看,原來doBind方法主要干了兩件事情,initAndRegister與doBind0; 3、initAndRegister主要做的事情就是初始化服務端Channel,并且將服務端Channel注冊到bossGroup子線程的多路復用器上; 4、doBind0則主要完成服務端啟動的最后一步,綁定地址,綁定完后就可以正式進行通信了;4.9、initAndRegister()初始化和注冊
1、源碼: // AbstractBootstrap.java final ChannelFuture initAndRegister() { Channel channel = null; try { // 反射調用clazz.getConstructor().newInstance()實例化類 // 同時也實例化了Channel,如果是服務端的話則為NioServerSocketChannel實例化對象 // 在實例化NioServerSocketChannel的構造方法中,也為每個Channel創建了一個管道屬性對象DefaultChannelPipeline=pipeline對象 // 在實例化NioServerSocketChannel的構造方法中,也為每個Channel創建了一個配置屬性對象NioServerSocketChannelConfig=config對象 // 在實例化NioServerSocketChannel的構造方法中,也為每個Channel創建了一個unsafe屬性對象NioMessageUnsafe=unsafe對象 channel = channelFactory.newChannel(); // 調用ReflectiveChannelFactory的newChannel方法 // 初始化剛剛被實例化的channel init(channel); } catch (Throwable t) { if (channel != null) { // channel can be null if newChannel crashed (eg SocketException("too many open files")) channel.unsafe().closeForcibly(); // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); } // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t); } // config().group()=bossGroup或parentGroup,然后利用parentGroup去注冊NioServerSocketChannel=channel ChannelFuture regFuture = config().group().register(channel); if (regFuture.cause() != null) { if (channel.isRegistered()) { channel.close(); } else { channel.unsafe().closeForcibly(); } } // If we are here and the promise is not failed, it"s one of the following cases: // 1) If we attempted registration from the event loop, the registration has been completed at this point. // i.e. It"s safe to attempt bind() or connect() now because the channel has been registered. // 2) If we attempted registration from the other thread, the registration request has been successfully // added to the event loop"s task queue for later execution. // i.e. It"s safe to attempt bind() or connect() now: // because bind() or connect() will be executed *after* the scheduled registration task is executed // because register(), bind(), and connect() are all bound to the same thread. return regFuture; } 2、逐行分析后會發現,首先通過反射實例化服務端channel對象,然后將服務端channel初始化一下; 3、然后調用bossGroup的注冊方法,將服務端channel作為參數傳入; 4、至此,方法名也表明該段代碼的意圖,實例化并初始化服務端Channel,然后注冊到bossGroup子線程的多路復用器上;4.10、init服務端Channel
1、源碼: // ServerBootstrap.java @Override void init(Channel channel) throws Exception { final Map4.11、config().group().register(channel), Object> options = options0(); synchronized (options) { setChannelOptions(channel, options, logger); } final Map , Object> attrs = attrs0(); synchronized (attrs) { for (Entry , Object> e: attrs.entrySet()) { @SuppressWarnings("unchecked") AttributeKey
1、源碼: // MultithreadEventLoopGroup.java @Override public ChannelFuture register(Channel channel) { // next()對象其實是NioEventLoopGroup內部中的children[]屬性中的其中一個,通過一定規則挑選一個NioEventLoop // 那么也就是說我們最終調用的是NioEventLoop來實現注冊channel return next().register(channel); // 從另外一個層面來講,我們要想注冊一個Channel,那么就可以直接調用NioEventLoopGroup父類中的register(Channel)即可注冊Channel, // 并且會按照一定的規則順序通過next()挑選一個NioEventLoop并將Channel綁定到它上面 // 如果NioEventLoopGroup為bossGroup的話,那么該方法注冊的肯定是NioServerSocketChannel對象 // 如果NioEventLoopGroup為workerGroup的話,那么該方法注冊的肯定是ServerSocketChannel對象 } // SingleThreadEventLoop.java @Override public ChannelFuture register(Channel channel) { // 當前this對象是屬于children[]屬性中的其中一個 // 將傳入的Channel與當前對象this一起封裝成DefaultChannelPromise對象 // 然后再調用當前對象的register(ChannelPromise)注冊方法 return register(new DefaultChannelPromise(channel, this)); } // SingleThreadEventLoop.java @Override public ChannelFuture register(final ChannelPromise promise) { // 校驗當前傳參是否為空,原則上既然是不可能為空的,因為上一個步驟是通過new出來的一個對象 ObjectUtil.checkNotNull(promise, "promise"); // promise.channel()其實就是上面new DefaultChannelPromise(channel, this)通過封裝后又取出這個channel對象 // promise.channel().unsafe()而每個Channel都有一個unsafe對象,對于NioServerSocketChannel來說NioMessageUnsafe=unsafe // 當前this對象是屬于children[]屬性中的其中一個 promise.channel().unsafe().register(this, promise); return promise; } // AbstractUnsafe.java @Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { // eventLoop對象是屬于children[]屬性中的其中一個 // 而當前類又是Channel的一個抽象類AbstractChannel,也是NioServerSocketChannel的父類 if (eventLoop == null) { throw new NullPointerException("eventLoop"); } if (isRegistered()) { promise.setFailure(new IllegalStateException("registered to an event loop already")); return; } if (!isCompatible(eventLoop)) { promise.setFailure( new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName())); return; } // 這里的 this.eventLoop 就是Children[i]中的一個,也就是具體執行任務的線程封裝對象 AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) { // 如果對象eventLoop中的線程對象和當前線程比對是一樣的話 register0(promise); // 那么則直接調用注冊方法register0 } else { try { // 比對的結果如果不一樣,十有八九都是該eventLoop的線程還未啟動, // 因此利用eventLoop的execute將register0(promise)方法作為任務添加到任務隊列中,并啟動線程來執行任務 eventLoop.execute(new Runnable() { @Override public void run() { register0(promise); } }); // 而服務端Channel的注冊,走的是該else分支,因為線程都還沒創建,eventLoop.inEventLoop()肯定就是false結果 } catch (Throwable t) { logger.warn( "Force-closing a channel whose registration task was not accepted by an event loop: {}", AbstractChannel.this, t); closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); } } } // SingleThreadEventExecutor.java /** * 向任務隊列中添加任務task。 * * @param task */ @Override public void execute(Runnable task) { if (task == null) { // 如果傳入的task任務為空,則直接拋空指針異常,此方法嚴格控制傳入參數必須非空 throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); // 判斷要添加的任務的這個線程,是不是和正在運行的nioEventLoop的處于同一個線程? if (inEventLoop) { // 如果是,則說明就是當前線程正在添加task任務,那么則直接調用addTask方法添加到隊列中 addTask(task); // 添加task任務 } else { startThread(); // 如果不是當前線程,則看看實例化的對象nioEventLoop父類中state字段是否標識有新建線程,沒有的話則利用線程池新創建一個線程,有的話則不用理會了 addTask(task); // 添加task任務 // 防止意外情況,還需要判斷下是否被關閉掉,如果被關閉掉的話,則將剛剛添加的任務刪除掉并采取拒絕策略直接拋出RejectedExecutionException異常 if (isShutdown() && removeTask(task)) { reject(); // 拒絕策略直接拋出RejectedExecutionException異常 } } // addTaskWakesUp:添加任務時需要喚醒標志,默認值為false,通過構造方法傳進來的也是false // wakesUpForTask(task):不是NonWakeupRunnable類型的task則返回true,意思就是只要不是NonWakeupRunnable類型的task,都需要喚醒阻塞操作 if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } } 2、通過一路跟蹤config().group().register(channel)該方法進去,最后會發現,源碼會調用一個register0(promise)的代碼來進行注冊; 3、但是跳出來一看,細細回味config().group().register(channel)這段代碼,可以得出這樣的一個結論: 若以后大家想注冊channel的話,直接通過線程管理組調用register方法,傳入想要注冊的channel對象即可; 4、當然還有一點請大家留意,execute(Runnable task)可以隨意調用添加任務,如果線程已啟動則直接添加,未啟動的話則先啟動線程再添加任務; 5、那么我們還是先盡快進入register0(promise)看看究竟是如何注冊channel的;4.12、register0(promise)
1、源碼: // AbstractUnsafe.java private void register0(ChannelPromise promise) { try { // check if the channel is still open as it could be closed in the mean time when the register // call was outside of the eventLoop if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } boolean firstRegistration = neverRegistered; doRegister(); // 調用Channel的注冊方法,讓Channel的子類AbstractNioChannel來實現注冊 // 執行到此,說明Channel已經注冊到了多路復用器上,并且也沒有拋出什么異常,那么接下來就賦值變量表明已經注冊成功 neverRegistered = false; registered = true; // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the // user may already fire events through the pipeline in the ChannelFutureListener. pipeline.invokeHandlerAddedIfNeeded(); // 會回調initAndRegister中init方法的p.addLast的initChannel回調 safeSetSuccess(promise); pipeline.fireChannelRegistered(); // Only fire a channelActive if the channel has never been registered. This prevents firing // multiple channel actives if the channel is deregistered and re-registered. if (isActive()) { // 檢測Channel是否處于活躍狀態,這里調用的是底層的socket的活躍狀態 if (firstRegistration) { pipeline.fireChannelActive(); // 這里也是注冊成功后會僅僅只會被調用一次 } else if (config().isAutoRead()) { // This channel was registered before and autoRead() is set. This means we need to begin read // again so that we process inbound data. // // See https://github.com/netty/netty/issues/4805 beginRead(); // 設置Channel的讀事件 } } } catch (Throwable t) { // Close the channel directly to avoid FD leak. closeForcibly(); closeFuture.setClosed(); safeSetFailure(promise, t); } } // AbstractNioChannel.java @Override protected void doRegister() throws Exception { boolean selected = false; for (;;) { // 自旋式的死循環,如果正常操作不出現異常的話,那么則會一直嘗試將Channel注冊到多路復用器selector上面 try { // eventLoop()對象是屬于children[]屬性中的其中一個,children是NioEventLoop類型的對象 // 而前面也了解到過,在實例化每個children的時候,會為每個children創建一個多路復用器selector與unwrappedSelector selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); // 如果將Channel注冊到了多路復用器上的成功且沒有拋什么異常的話,則返回跳出循環 return; } catch (CancelledKeyException e) { if (!selected) { // Force the Selector to select now as the "canceled" SelectionKey may still be // cached and not removed because no Select.select(..) operation was called yet. eventLoop().selectNow(); selected = true; } else { // We forced a select operation on the selector before but the SelectionKey is still cached // for whatever reason. JDK bug ? throw e; } } } } // DefaultChannelPipeline.java final void invokeHandlerAddedIfNeeded() { assert channel.eventLoop().inEventLoop(); if (firstRegistration) { // pipeline標識是否已注冊,默認值為true firstRegistration = false; // 馬上置位false,告訴大家該方法只會被調用一次 // We are now registered to the EventLoop. It"s time to call the callbacks for the ChannelHandlers, // that were added before the registration was done. // 到此為止,我們已經將Channel注冊到了NioEventLoop的多路復用器上,那么接下來是時候回調Handler被添加進來 callHandlerAddedForAllHandlers(); } } // DefaultChannelPipeline.java private void callHandlerAddedForAllHandlers() { final PendingHandlerCallback pendingHandlerCallbackHead; synchronized (this) { assert !registered; // 測試registered是否為false,因為該方法已經表明只會被調用一次,所以這里就嚴格判斷 // This Channel itself was registered. registered = true; // 而且當registered設置為true后,就不會再改變該值的狀態 pendingHandlerCallbackHead = this.pendingHandlerCallbackHead; // Null out so it can be GC"ed. this.pendingHandlerCallbackHead = null; } // This must happen outside of the synchronized(...) block as otherwise handlerAdded(...) may be called while // holding the lock and so produce a deadlock if handlerAdded(...) will try to add another handler from outside // the EventLoop. PendingHandlerCallback task = pendingHandlerCallbackHead; // 通過while循環,單向鏈表一個個回調task的execute,該回調添加的就回調添加,該回調移除的則回調移除 while (task != null) { task.execute(); task = task.next; } } 2、看完register0(promise)是不是覺得,原來服務端channel的注冊是這么簡單,最后就是調用javaChannel().register(...)這個方法一下,然后就這么稀里糊涂的注冊到多路復用器上了; 3、在注冊完之際,還會找到之前的單向鏈表對象pendingHandlerCallbackHead,并且依依回調task.execute方法; 4、然后觸發fireChannelRegistered注冊成功事件,告知上層說我們的服務端channel已經注冊成功了,大家請知悉一下; 5、最后通過beginRead設置服務端的讀事件標志,就是說服務端的channel僅對讀事件感興趣; 6、至此initAndRegister這塊算是講完了,那么接下來就看看最后一個步驟綁定ip地址,完成通信前的最后一步;4.13、doBind0(regFuture, channel, localAddress, promise)
1、源碼: // AbstractBootstrap.java private static void doBind0( final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) { // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up // the pipeline in its channelRegistered() implementation. // 服務端啟動最后一個步驟,綁完地址就可以正式進行通信了 channel.eventLoop().execute(new Runnable() { @Override public void run() { if (regFuture.isSuccess()) { // 服務端channel直接調用bind方法進行綁定地址 channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { promise.setFailure(regFuture.cause()); } } }); } // AbstractChannel.java @Override public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { return pipeline.bind(localAddress, promise); } // DefaultChannelPipeline.java @Override public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) { return tail.bind(localAddress, promise); } // AbstractChannelHandlerContext.java @Override public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) { if (localAddress == null) { throw new NullPointerException("localAddress"); } if (isNotValidPromise(promise, false)) { // cancelled return promise; } final AbstractChannelHandlerContext next = findContextOutbound(); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeBind(localAddress, promise); } else { safeExecute(executor, new Runnable() { @Override public void run() { next.invokeBind(localAddress, promise); } }, promise, null); } return promise; } // AbstractChannelHandlerContext.java private void invokeBind(SocketAddress localAddress, ChannelPromise promise) { if (invokeHandler()) { try { ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise); } catch (Throwable t) { notifyOutboundHandlerException(t, promise); } } else { bind(localAddress, promise); } } // HeadContext.java @Override public void bind( ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { unsafe.bind(localAddress, promise); } // AbstractUnsafe.java @Override public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { assertEventLoop(); if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } // See: https://github.com/netty/netty/issues/576 if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) && localAddress instanceof InetSocketAddress && !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() && !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) { // Warn a user about the fact that a non-root user can"t receive a // broadcast packet on *nix if the socket is bound on non-wildcard address. logger.warn( "A non-root user can"t receive a broadcast packet if the socket " + "is not bound to a wildcard address; binding to a non-wildcard " + "address (" + localAddress + ") anyway as requested."); } boolean wasActive = isActive(); try { doBind(localAddress); } catch (Throwable t) { safeSetFailure(promise, t); closeIfClosed(); return; } if (!wasActive && isActive()) { invokeLater(new Runnable() { @Override public void run() { pipeline.fireChannelActive(); } }); } safeSetSuccess(promise); } // NioServerSocketChannel.java @Override protected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() >= 7) { javaChannel().bind(localAddress, config.getBacklog()); } else { javaChannel().socket().bind(localAddress, config.getBacklog()); } } 2、經過這么一路調用,其實最終會發現,綁定地址也是通過javaChannel().bind(...)這么簡短的一句話就搞定了; 而前面的注冊到多路復用器上調用的是javaChannel().register(...)一句簡短代碼; 從而可得出這么一個結論:只要關系到channel的注冊綁定,最終核心底層都是調用這個channel的bind和register方法; 3、至此,服務端的啟動流程算是完結了。。五、總結
最后我們來總結下,通過分析Netty的服務端啟動,經過的流程如下: ? 創建兩個線程管理組,以及實例化每個線程管理組的子線程數組children[]; ? 設置啟動類參數,比如channel、localAddress、childHandler等參數; ? 反射實例化NioServerSocketChannel,創建ChannelId、unsafe、pipeline等對象; ? 初始化NioServerSocketChannel,設置attr、option,添加新的handler到服務端pipeline管道中; ? 調用JDK底層做ServerSocketChannel注冊到多路復用器上,并且注冊成功后回調pipeline管道中的單向鏈表依次執行task任務; ? 調用JDK底層做NioServerSocketChannel綁定端口,并觸發active事件;六、下載地址
https://gitee.com/ylimhhmily/SpringCloudTutorial.git
SpringCloudTutorial交流QQ群: 235322432
SpringCloudTutorial交流微信群: 微信溝通群二維碼圖片鏈接
歡迎關注,您的肯定是對我最大的支持!!!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71082.html
摘要:端引導類線程管理組線程管理組將設置到服務端引導類中指定通道類型為,一種異步模式,阻塞模式為設置讓服務器監聽某個端口已等待客戶端連接。 原理剖析(第 010 篇)Netty之服務端啟動工作原理分析(上) - 一、大致介紹 1、Netty這個詞,對于熟悉并發的童鞋一點都不陌生,它是一個異步事件驅動型的網絡通信框架; 2、使用Netty不需要我們關注過多NIO的API操作,簡簡單單的使用即可...
摘要:簡單來說就是把注冊的動作異步化,當異步執行結束后會把執行結果回填到中抽象類一般就是公共邏輯的處理,而這里的處理主要就是針對一些參數的判斷,判斷完了之后再調用方法。 閱讀這篇文章之前,建議先閱讀和這篇文章關聯的內容。 1. 詳細剖析分布式微服務架構下網絡通信的底層實現原理(圖解) 2. (年薪60W的技巧)工作了5年,你真的理解Netty以及為什么要用嗎?(深度干貨)...
摘要:在結構上引入了頭結點和尾節點,他們分別指向隊列的頭和尾,嘗試獲取鎖入隊服務教程在它提出十多年后的今天,已經成為最重要的應用技術之一。隨著編程經驗的日積月累,越來越感覺到了解虛擬機相關要領的重要性。 JVM 源碼分析之 Jstat 工具原理完全解讀 http://click.aliyun.com/m/8315/ JVM 源碼分析之 Jstat 工具原理完全解讀 http:...
摘要:本文會以引出問題為主,后面有時間的話,筆者陸續會抽些重要的知識點進行詳細的剖析與解答。敬請關注服務端思維微信公眾號,獲取最新文章。 原文地址:梁桂釗的博客博客地址:http://blog.720ui.com 這里,筆者結合自己過往的面試經驗,整理了一些核心的知識清單,幫助讀者更好地回顧與復習 Java 服務端核心技術。本文會以引出問題為主,后面有時間的話,筆者陸續會抽些重要的知識點進...
摘要:大家好,我是冰河有句話叫做投資啥都不如投資自己的回報率高。馬上就十一國慶假期了,給小伙伴們分享下,從小白程序員到大廠高級技術專家我看過哪些技術類書籍。 大家好,我是...
閱讀 3226·2021-10-13 09:40
閱讀 3688·2019-08-30 15:54
閱讀 1309·2019-08-30 13:20
閱讀 2993·2019-08-30 11:26
閱讀 475·2019-08-29 11:33
閱讀 1099·2019-08-26 14:00
閱讀 2356·2019-08-26 13:58
閱讀 3366·2019-08-26 10:39