高并发编程之Netty高性能调优工具类解析

1.1 多线程共享FastThreadLocal

我们在剖析堆外内存分配的时候简单介绍过FastThreadLocal,它类似于JDK的ThreadLocal,也是用于多线程条件下,保证统一线程的对象共享,只是Netty中定义的FastThreadLocal性能要高于JDK的ThreadLocal,本章开始我们来分析其具体原因。

1.1.1 FastThreadLocal的使用和创建

首先看一个最简单的Demo。

从上面示例中看出,首先声明一个内部类FastThreadLocalTest继承FastThreadLocal,并重写initialValue()方法。initialValue()方法就是用来初始化线程共享对象的。然后声明一个成员变量fastThreadLocalTest,类型是内部类FastThreadLocalTest,在构造方法中初始化fastThreadLocalTest。main()方法中创建当前类FastThreadLocalDemo的对象fastThreadLocalDemo,然后启动两个线程,每个线程通过fastThreadLocalDemo.fastThreadLocalTest.get()方式获取线程共享对象,因为fastThreadLocalDemo是相同的,所以fastThreadLocalTest对象也是同一个,同一个对象在不同线程中进行get()。第一个线程循环通过set()方法修改共享对象的值,第二个线程则循环判断fastThreadLocalTest.get()获得的对象和第一次get()获得的对象是否相等。这里输出结果都是true,说明其他线程虽然不断修改共享对象的值,但都不影响当前线程共享对象的值,这样就实现了线程共享对象的功能。

根据上述示例,我们剖析FastThreadLocal的创建,FastThreadLocal的构造方法的代码如下。

这里的index代表FastThreadLocal对象的一个下标,每创建一个FastThreadLocal都会有一个唯一的自增的下标,跟进nextVariableIndex()方法。

上述代码中,获取nextIndex通过getAndIncrement()进行原子自增,创建第一个FastThreadLocal对象时,nextIndex为0;创建第二个FastThreadLocal对象时,nextIndex为1;依此类推,第n个FastThreadLocal对象时的nextIndex为n-1,如下图所示。

回到Demo中,看线程中的这一句。

这是调用了FastThreadLocal对象的get()方法,作用是创建一个线程共享对象。get()方法的代码如下。

这里调用了一个重载的get()方法,参数中通过InternalThreadLocalMap的get()方法获取了一个InternalThreadLocalMap对象。我们跟到InternalThreadLocalMap的get()方法中,分析其是如何获取InternalThreadLocalMap对象的。

这里首先获取当前线程,然后判断当前线程是否为FastThreadLocalThread线程,通常NioEventLoop线程都是FastThreadLocalThread,用于线程则不是FastThreadLocalThread。

如果是FastThreadLocalThread线程,则调用fastGet()方法获取InternalThreadLocalMap,从名字上我们能知道,这是一种效率极高的获取方式。

如果不是FastThreadLocalThread线程,则调用slowGet()方法获取InternalThreadLocalMap,同样根据名字,我们知道这是一种效率不太高的获取方式。

因为我们的Demo并不是EventLoop线程,所以调用slowGet()方法。

首先剖析slowGet()方法。

通过UnpaddedInternalThreadLocalMap.slowThreadLocalMap获取一个ThreadLocal对象slowThreadLocalMap,slowThreadLocalMap是UnpaddedInternalThreadLocalMap类的一个静态属性,类型是ThreadLocal类型。这里的ThreadLocal是JDK的ThreadLocal。

然后通过slowThreadLocalMap对象的get()方法,获取一个InternalThreadLocalMap。如果是第一次获取,InternalThreadLocalMap有可能是null,则在if块中新建一个InternalThreadLocalMap对象,并设置在ThreadLocal对象中。

因为Netty实现的FastThreadLocal要比JDK的ThreadLocal快,所以这里的方法叫作slowGet()方法。

回到InternalThreadLocalMap的get()方法。

继续剖析fastGet()方法,通常EventLoop线程创建FastThreadLocalThread线程,所以EventLoop线程执行到这一步的时候会调用fastGet()方法。

首先FastThreadLocalThread对象直接通过ThreadLocalMap获取ThreadLocalMap对象。如果ThreadLocalMap为null,则创建一个InternalThreadLocalMap对象设置到FastThreadLocalThread的成员变量中。

我们知道,FastThreadLocalThread对象中维护了一个InternalThreadLocalMap类型的成员变量,可以直接通过threadLocalMap()方法获取该变量的值,也就是InternalThreadLocalMap。

跟进InternalThreadLocalMap的构造方法。

这里调用了父类的构造方法,传入一个newIndexedVariableTable(),代码如下。

这里创建了一个长度为32的数组,并将数组中的每一个对象都设置为UNSET,UNSET是一个Object的对象,表示该下标的值没有被设置。

回到InternalThreadLocalMap的构造方法,看其父类的构造方法。

这里初始化了一个数组类型的成员变量IndexedVariables,就是newIndexedVariableTable返回Object的数组。可以知道,每个InternalThreadLocalMap对象中都维护了一个Object类型的数组,那么这个数组有什么作用呢?继续往下剖析。

回到FastThreadLocal的get()方法。

剖析完InternalThreadLocalMap.get()的相关逻辑,继续看重载的get()方法。

首先看这一步。

这一步是获取当前Index下标的Object,其实就是获取每个FastThreadLocal对象绑定的线程共享对象。Index已经分析过,是每一个FastThreadLocal的唯一下标。

跟进indexedVariable()方法。

首先获取indexedVariables。indexedVariables是InternalThreadLocalMap对象中维护的数组,初始大小是32。然后在return中判断当前Index是不是小于当前数组的长度,如果小于则获取当前下标Index的数组元素,否则返回UNSET,代表没有设置的对象。

其实每一个FastThreadLocal对象中所绑定的线程共享对象,都存放在ThreadLocalMap对象中的一个对象数组中,数组中元素的下标对应着FastThreadLocal中的Index属性,对应关系如下图所示。

回到FastThreadLocal重载的get()方法。

根据以上逻辑,第一次获取对象v时只能获取到UNSET对象,因为该对象并没有保存在ThreadLocalMap中的数组IndexedVariables中,所以第一次获取在if判断中为false时,会执行到initialize()方法中。跟到initialize()方法中。

首先调用initialValue()方法,这里的initialValue实际上调用的是FastThreadLocal子类的重写initialValue()方法。在Demo中对应这个方法的代码如下。

通过这个方法会创建一个线程共享对象。通过ThreadLocalMap对象的setIndexedVariable()方法将创建的线程共享对象设置到ThreadLocalMap维护的数组中,参数为FastThreadLocal和创建的对象本身。

setIndexedVariable()方法的代码如下。

先判断FastThreadLocal对象的Index是否超过数组IndexedVariables的长度,如果没有超过,则直接通过下标设置新创建的线程共享对象。通过这个操作,下次获取该对象的时候就可以直接通过数组下标进行取出。

如果Index超过了数组IndexedVariables的长度,则通过expandIndexedVariableTableAndSet()方法将数组扩容,并且根据Index通过数组下标的方式将线程共享对象设置到数组IndexedVariables中。

以上就是线程共享对象的创建和获取的过程。

1.1.2 FastThreadLocal的设值

FastThreadLocal的设值是由set()方法完成的,其实就是通过调用set()方法修改线程共享对象,作用域是当前线程,我们回顾上一节Demo中的一个线程set对象的过程。

set()方法的代码如下。

首先判断当前设置的对象是不是UNSET。因为不是UNSET,所以进到if块中。

if块调用了重载的set()方法,参数仍然为InternalThreadLocalMap,同时,参数也传入了set的Value值。

跟进重载的set()方法。

这里重点关注if(threadLocalMap.setIndexedVariable(index,value))这部分。通过ThreadLocalMap调用setIndexedVariable()方法进行对象的设置,传入了当前FastThreadLocal的下标和Value。setIndexedVariable()方法的代码如下。

这里的逻辑其实和get非常类似,都是直接通过索引操作的,根据索引值,直接通过数组下标的方式对元素进行设置。

回到FastThreadLocal的set()方法。

如果修改的对象是UNSET对象,则会调用remove()方法,代码如下。

Object v=threadLocalMap.removeIndexedVariable(index)这一步是根据索引Index将值设置成UNSET的。

跟进removeIndexedVariable()方法。

这里的逻辑也比较简单,根据Index通过数组下标的方式将元素设置成UNSET对象。

回到remove()方法中,if(v!=InternalThreadLocalMap.UNSET)判断如果设置的值不是UNSET对象,则调用onRemoval()方法。

跟进onRemoval()方法。

它是个空实现,用于交给子类去完成。

1.2 Recycler对象回收站

Recycler我们应该不陌生,因为在前面章节中,有很多地方使用了Recycler。Recycler是Netty实现的一个轻量级对象回收站,很多对象在使用完毕之后,并没有直接交给GC去处理,而是通过对象回收站将对象回收,目的是为了对象重用和减少GC压力。比如ByteBuf对象的回收,因为ByteBuf对象在Netty中会被频繁创建,并且占用比较大的内存空间,所以使用完毕后会通过对象回收站的方式进行回收,以达到资源重用的目的。

1.2.1 Recycler的使用和创建

在Netty中,Recycler的使用是相当频繁的。Recycler的作用是保证对象的循环利用,对象使用完可以通过Recycler回收,需要再次使用则从对象池中取出,不用每次都创建新对象,从而减少对系统资源的占用,同时也减轻了GC的压力。先看一个示例。

首先定义了一个Recycler的成员变量RECYCLER,在匿名内部类中重写了newObject()方法,也就是创建对象的方法,该方法是用户自定义的。newObject()方法返回的new User(handle)代表当回收站没有此类对象的时候,可以通过这种方式创建对象。成员变量RECYCLER可以用来对此类对象回收和再利用。然后定义了一个静态内部类User,User中有个成员变量Handle,在构造方法中为其赋值,Handle的作用是用于对象回收。并且定义了一个方法recycle(),方法中通过handle.recycle(this)这种方式将自身对象进行回收,通过这步操作,可以将对象回收到Recycler中。以上逻辑先做了解,之后会进行详细分析。

在main()方法中,通过RECYCLER的get()方法获取一个User,然后进行回收,再通过get()方法将回收站的对象取出,再次进行回收,最后判断两次取出的对象是否为同一个对象,最后结果输出为true。以上Demo就可以说明Recycler的回收再利用的功能。

简单介绍完Demo,我们就来详细地分析Recycler的机制。在Recycler的类的源码中,我们会看到这样一段逻辑。

这一段逻辑用于保存线程共享对象,而这里的共享对象,就是一个Stack类型的对象。每个Stack中都维护着一个DefaultHandle类型的数组,用于盛放回收的对象,有关Stack和线程的关系如下图所示。

也就是说,在每个Recycler中,都维护着一个线程共享的栈,用于对一类对象的回收。Stack的构造方法的代码如下。

首先介绍几个构造方法中初始化的关键属性。

● Parent:表示Recycler对象自身。

● Thread:表示当前Stack绑定的哪个线程。

● maxCapacity:表示当前Stack的最大容量,表示Stack最多能盛放多少个元素。

● elements:表示Stack中存储的对象,类型为DefaultHandle,可以被外部对象引用,从而实现回收。

● ratioMask:用来控制对象回收的频率,也就是说每次通过Recycler回收对象的时候,不是每次都会进行回收,而是通过该参数控制回收频率。

● maxDelayedQueues:稍微有些复杂,很多时候,一个线程创建的对象,有可能会被另一个线程所释放,而另一个线程释放的对象不会放在当前线程的Stack中,而是存放在一个叫作WeakOrderQueue的数据结构中,里面也存放着一个个DefaultHandle,WeakOrderQueue会存放线程1创建且在线程2进行释放的对象。

现在我们已经知道,maxDelayedQueues属性的意思就是设置该线程能回收的线程对象的最大值。假设当前线程是线程A,maxDelayedQueues值设置为2,那么线程A回收了线程B创建的对象,又回收了线程C创建的对象,就不能再回收线程D创建的对象,最多只能回收2个线程创建的对象。

属性availableSharedCapacity表示在线程A中创建的对象,在其他线程中缓存的最大个数,同样,相关逻辑会在之后的内容进行剖析,另外介绍两个没有在构造方法中初始化的属性。

这里cursor、prev和head是存放了其他线程的链表的指针,用于指向WeakOrderQueue,也是稍作了解,之后会进行详细剖析。有关Stack异线程之间对象的关系如下图所示。

继续介绍Recycler的构造方法,同时熟悉有关Stack各个参数的默认值。

这里调用了重载的构造方法,并传入了参数DEFAULT_MAX_CAPACITY_PER_THREAD。DEFAULT_MAX_CAPACITY_PER_THREAD的默认值是32768,是在static块中被初始化的,可以跟进去自行分析。这个值就代表每个线程中Stack中最多回收的元素的个数。继续跟进重载的构造方法。

这里又调用了重载的构造方法,并且传入32768和MAX_SHARED_CAPACITY_FACTOR。

MAX_SHARED_CAPACITY_FACTOR的默认值是2,同样在static块中进行了初始化,有关该属性的用处稍后讲解。继续跟进构造方法。

这里同样调用了重载的构造方法,传入了32768和2,还有两个属性RATIO和MAX_DELAYED_QUEUES_PER_THREAD。RATIO也在static中被初始化,默认值是8。同上,MAX_DELAYED_QUEUES_PER_THREAD的默认值是2倍CPU核数。继续跟进构造方法。

分析Recycler构造方法,主要就是将几个属性进行了初始化。

● ratioMask:它是获取safeFindNextPositivePowerOfTwo()方法的返回值。在此处safeFindNextPositivePowerOfTwo()方法的返回值是8,因此ratioMask的最终赋值是7。

● maxCapacityPerThread:它是一个大于0的数,如果不满足判断条件就进入else代码块,最终会被赋值为32768。

● 被赋值为32768。

● maxSharedCapacityFactor:最终会被赋值为2。

● maxDelayedQueuesPerThread:被赋值为CPU核数×2。

我们再回到Stack的构造方法。

根据Recycler初始化属性的逻辑,可以知道Stack中几个属性的值。

● maxCapacity:默认值为32768。

● ratioMask:默认值为7。

● maxDelayedQueues:默认值为CPU核数×2。

● availableSharedCapacity:默认值是32768/2,也就是16384。

1.2.2 从Recycler中获取对象

回顾上节Demo中的main()方法,从回收站获取对象。

通过Recycler的get()方法获取对象,代码如下。

首先判断maxCapacityPerThread是否为0,maxCapacityPerThread代表Stack最多能缓存多少个对象,如果缓存0个,说明对象将一个都不会回收。通过调用newObject创建一个对象,并传入一个NOOP_HANDLE,NOOP_HANDLE是一个Handle,我们看其定义。

这里的recycle()方法是一个空实现,代表不进行任何对象回收。回到get()方法中,我们看第二步Stackstack=threadLocal.get(),这里通过FastThreadLocal对象获取当前线程的Stack。获取Stack之后,从Stack中pop出一个Handle,其作用稍后分析。如果取出的对象为null,说明当前回收站内没有任何对象,通常第一次执行到这里对象还没回收,就会是null,这样则会通过stack.newHandle()创建一个Handle。创建出来的Handle的Value属性,通过重写的newObject()方法进行赋值,也就是Demo中的User。跟进newHandle()方法。

这里创建一个DefaultHandle对象,并传入this,这里的this是当前Stack。DefaultHandle的构造方法的代码如下。

这里初始化了stack属性。DefaultHandle中还有一个Value的成员变量。

这里的Value用来绑定回收的对象本身。回到get()方法中,分析Handle,我们回到上一步。

我们分析从Stack中弹出一个Handle的逻辑,跟进pop()方法。

首先获取size,size表示当前Stack的对象数。如果size为0,则调用scavenge()方法,这个方法是异线程回收对象的方法,我们放在之后的小节进行分析。如果size大于0,则size进行自减,代表取出一个元素,然后通过size的数组下标的方式将Handle取出,之后将当前下标设置为null,最后将属性recycleId、lastRecycledId、size进行赋值。recycleId和lastRecycledId会在之后的小节进行分析,回到get()方法。

无论是从Stack中弹出的Handle,还是创建的Handle,最后都要通过handle.value获取实际使用的对象。

1.2.3 相同线程内的对象回收

上节中剖析了从Recycler中获取一个对象,本节分析在创建和回收是同线程的前提下,Recycler是如何进行回收的。回顾前面章节Demo中的main()方法。

这是一个同线程回收对象的典型场景,在一个线程中将对象创建并且回收,我们的User对象定义了recycle方法。

这里的recycle是通过Handle对象的recycle()方法实现对象回收的,实际调用的是DefaultHandle的recycle()方法。跟进recycle()方法。

如果回收的对象为null,则抛出异常。如果不为null,则通过自身绑定Stack的push()方法将自身push到Stack中。push()方法的代码如下。

首先判断当前线程和创建Stack的时候保存的线程是否是同一线程。如果是,说明是同线程回收对象,则执行pushNow()方法将对象放入Stack中。pushNow()方法的代码如下。

如果第一次回收,item.recycleId和item.lastRecycledId都为0,则不会进入if块。继续往下看,item.recycleId=item.lastRecycledId=OWN_THREAD_ID这一步将Handle的recycleId和lastRecycledId赋值为OWN_THREAD_ID,OWN_THREAD_ID在每一个recycle中都是唯一固定的,这里我们只需要记住这个概念就行。然后获取当前size,如果size超过上限大小,则直接返回。这里还有个判断dropHandle。

if(!handle.hasBeenRecycled)表示当前对象之前是否没有被回收过,如果是第一次回收,会返回true,然后进入if。再看if中的判断if((++handleRecycleCount&ratioMask)!=0),handleRecycleCount表示当前位置Stack回收了多少次对象(回收了多少次,不代表回收了多少个对象,因为不是每次回收都会被成功地保存在Stack中),我们之前分析过ratioMask是7,这里(++handleRecycleCount&ratioMask)!=0表示回收的对象数如果不是8的倍数,则返回true,表示只回收1/8的对象,然后将hasBeenRecycled设置为true,表示已经被回收。回到pushNow()方法中,如果size的大小等于Stack中的数组Elements的大小,则将数组Elements进行扩容,最后size通过数组下标的方式将当前Handle设置到Elements的元素中,并将size进行自增。

1.2.4 不同线程间的对象回收

异线程回收对象,就是创建对象和回收对象不在同一条线程的情况下对象回收的逻辑。在1.2.1节简单介绍过,异线程回收对象,是不会放在当前线程的Stack中的,而是放在一个WeakOrderQueue的数据结构中,回顾我们之前的示意图如下图所示。

相关的逻辑,我们跟到源码中,首先从回收对象的入口方法开始。DefaultHandle的recycle()方法代码如下。

继续看push()方法的代码。

上节分析过,同线程会执行到pushNow(),有关具体逻辑也进行了分析。如果不是同线程,则会执行到pushLater()方法,传入Handle对象和当前线程对象,跟进pushLater()方法。

首先通过DELAYED_RECYCLED.get()方法获取一个delayedRecycled对象,我们看DELAYED_RECYCLED的代码。

我们看到DELAYED_RECYCLED是一个FastThreadLocal对象,initialValue()方法创建一个WeakHashMap对象,WeakHashMap是一个Map,Key为Stack,Value为前面提到过的WeakOrderQueue。从中可以分析到,每个线程都维护了一个WeakHashMap对象。WeakHashMap中的元素,是一个Stack和WeakOrderQueue的映射,说明不同的Stack对应不同的WeakOrderQueue。这里的映射关系可以举例说明。

比如线程1创建了一个对象,在线程3进行了回收;线程2创建了一个对象,同样也在线程3进行了回收,那么线程3对应的WeakHashMap中保存了两组关系:线程1对应的Stack和WeakOrderQueue,以及线程2对应的Stack和WeakOrderQueue。我们回到pushLater()方法中,继续往下看。

获取了当前线程的WeakHashMap对象delayedRecycled之后,通过delayedRecycled创建对象的线程的Stack,获取WeakOrderQueue。这里的this,就是创建对象的那个线程所属的Stack,这个Stack是绑定在Handle中的,在创建Handle对象的时候进行的绑定。假设当前线程是线程2,创建Handle的线程是线程1,通过Handle的Stack获取线程1的WeakOrderQueue。if(queue==null)说明线程2没有回收过线程1的对象,则进入if块的逻辑,首先判断if(delayedRecycled.size()>=maxDelayedQueues)。

● delayedRecycled.size()表示当前线程回收其他创建对象的线程的个数,也就是有几个其他的线程在当前线程回收对象。

● maxDelayedQueues表示最多能回收的线程个数,如果超过这个值,就表示当前线程不能再回收其他线程的对象了。

通过delayedRecycled.put(this,WeakOrderQueue.DUMMY)标识创建对象的线程的Stack所对应的WeakOrderQueue不可用,DUMMY可以理解为不可用。如果没有超过maxDelayedQueues,则通过if判断中的WeakOrderQueue.allocate(this,thread)方式创建一个WeakOrderQueue。allocate传入this,也就是创建对象的线程对应的Stack,跟进allocate()方法。

reserveSpace(stack.availableSharedCapacity,LINK_CAPACITY)表示线程1的Stack还能不能分配LINK_CAPACITY个元素,如果可以,则直接通过new的方式创建一个WeakOrderQueue对象。回到reserveSpace()方法。

参数availableSharedCapacity表示线程1的Stack允许外部线程给其缓存多少个对象,之前我们分析过是16384,space默认是16。方法中通过一个CAS操作,将16384减去16,表示Stack可以给其他线程缓存的对象数为16384-16,而这16个元素,将由线程2缓存。

回到pushLater()方法,创建之后通过delayedRecycled.put(this,queue)将Stack和WeakOrderQueue进行关联,通过queue.add(item)将创建的WeakOrderQueue添加一个Handle。讲解WeakOrderQueue之前,先了解下WeakOrderQueue的数据结构。WeakOrderQueue维护了多个Link,Link之间通过链表进行连接,每个Link可以盛放16个Handle。我们分析过,在reserveSpace()方法中将stack.availableSharedCapacity-16,其实就表示先分配16个空间放在Link里,下次回收的时候,如果这16个空间没有填满,则可以继续往里盛放。如果16个空间都已填满,则通过继续添加Link的方式继续分配16个空间用于盛放Handle。WeakOrderQueue和WeakOrderQueue之间也通过链表进行关联,可以根据下图理解上述逻辑。

根据以上思路,我们看WeakOrderQueue的构造方法。

这里有个Head和Tail,都指向一个Link对象,其实在WeakOrderQueue中维护了一个链表,Head和Tail分别代表头节点和尾节点,初始状态下,头节点和尾节点都指向同一个节点。简单看下Link的类的定义。

每次创建一个Link,都会创建一个DefaultHandle类型的数组用于盛放DefaultHandle对象,默认大小是16个。

readIndex是一个读指针,之后小节会进行分析。next节点则指向下一个Link。回到WeakOrderQueue的构造方法中,owner是对当前线程进行一个包装,代表了当前线程。

接下来在一个同步块中,将当前创建的WeakOrderQueue插入Stack指向的第一个WeakOrderQueue,也就是Stack的Head属性,指向我们创建的WeakOrderQueue,如下图所示。

如果线程2创建一个和Stack关联的WeakOrderQueue,Stack的头节点就会指向线程2创建的WeakOrderQueue。如果之后线程3也创建了一个和Stack关联的WeakOrderQueue,Stack的头节点就会指向新创建的线程3的WeakOrderQueue,然后线程3的WeakOrderQueue再指向线程2的WeakOrderQueue。也就是无论哪个线程创建一个和同一个Stack关联的WeakOrderQueue的时候,都插入Stack指向的WeakOrderQueue列表的头部,这样就可以将Stack和其他线程释放对象的容器WeakOrderQueue进行绑定。回到pushLater()方法。

根据之前分析的WeakOrderQueue的数据结构,分析最后一步,也就是WeakOrderQueue的add()方法。

首先看handle.lastRecycledId=id,lastRecycledId表示Handle上次回收的id,而id表示WeakOrderQueue的id,weakOrderQueue每次创建的时候,会自增一个唯一的id。Link tail=this.tail表示获取当前WeakOrderQueue中指向最后一个Link的指针,也就是尾指针。再看if((writeIndex=tail.get())==LINK_CAPACITY),tail.get()表示获取当前Link中已经填充元素的个数,如果等于16,说明元素已经填充满。然后通过reserveSpace()方法判断当前WeakOrderQueue是否还能缓存Stack的对象,reserveSpace()方法会根据Stack的属性availableSharedCapacity-16的方式判断还能否缓存Stack的对象,如果不能再缓存Stack的对象,则返回。如果还能继续缓存,则再创建一个Link,并将尾节点指向新创建的Link,并且原来尾节点的next节点指向新创建的Link,然后获取当前Link的writeIndex,也就是写指针。如果新创建的Link中没有元素,writeIndex为0,之后将尾部的Link的Elements属性,也就是一个DefaultHandle类型的数组,通过数组下标的方式将第writeIndex个节点赋值为要回收的Handle,然后将Handle的Stack属性设置为null,表示当前Handle不是通过Stack进行回收的,最后将Tail节点的元素个数进行+1,表示下一次将从writeIndex+1的位置往里写。

1.2.5 获取不同线程间释放的对象

上节分析了异线程回收对象,原理是通过与Stack关联的WeakOrderQueue进行回收。如果对象经过异线程回收之后,当前线程需要取出对象进行二次利用,当前Stack为空,则会通过当前Stack关联的WeakOrderQueue进行取出,这也是本节要分析的获取异线程释放对象的内容。

在介绍之前,先看Stack类中的两个属性。

这里的cursor、prev和head都是指向链表中WeakOrderQueue的指针,其中head指向最近创建的与Stack关联的WeakOrderQueue,也就是头节点。cursor代表的是寻找当前WeakOrderQueue,prev则是cursor的上一个节点,如下图所示。

我们从获取对象的入口方法Handle的get()方法开始分析。

这块逻辑我们并不陌生,Stack对象通过pop()方法弹出一个Handle,跟进pop()方法。

这里重点关注的是,如果size为空,也就是当前Stack为空的情况下,会执行到scavenge()方法。这个方法就是从WeakOrderQueue获取对象的方法。跟进scavenge()方法。

scavengeSome()方法表示如果已经回收到了对象,则直接返回;如果没有回收到对象,则将prev和cursor两个指针进行重置。继续跟进scavengeSome()方法。

首先获取cursor指针,cursor指针代表要回收的WeakOrderQueue。如果cursor为空,则让其指向头节点,如果头节点也为空,说明当前Stack没有与其关联的WeakOrderQueue,则返回false。通过一个布尔值success标记回收状态,然后获取prev指针,也就是cursor的上一个节点,之后进入一个do-while循环。do-while循环的终止条件是没有遍历到最后一个节点并且回收的状态为false。我们仔细来看do-while循环中的代码逻辑,首先cursor指针会调用transfer()方法,该方法表示从当前指针指向的WeakOrderQueue中将元素放入当前Stack中,如果取出成功则将success设置为true并跳出循环,transfer()方法我们稍后分析。

继续往下看,如果没有获得元素,则会通过next属性获取下一个WeakOrderQueue,然后进入一个判断if(cursor.owner.get()==null)。owner属性是与当前WeakOrderQueue关联的一个线程,get()方法获得关联的线程对象,如果这个对象为null说明该线程不存在,则进入if块,也就是一些清理的工作。if块中又进入一个判断if(cursor.hasFinalData()),表示当前的WeakOrderQueue中是否还有数据,如果有数据则通过for循环将数据通过transfer()方法传输到当前Stack中,传输成功的,将success标记为true。Transfer()方法是将WeakOrderQueue中一个Link中的Handle往Stack传输,这里通过for循环将每个Link中的数据都传输到Stack中。

继续往下看,如果prev节点不为空,则通过prev.next=next将cursor节点进行释放,也就是prev的下一个节点指向cursor的下一个节点。继续往下看else块中的prev=cursor,表示如果当前线程还在,则将prev赋值为cursor,代表prev后移一个节点,最后通过cursor=next将cursor后移一位,然后继续进行循环。循环结束之后,将Stack的prev和cursor属性进行保存。我们跟到transfer()方法中,分析如何将WeakOrderQueue中的Handle传输到Stack中。

剖析之前,我们回顾WeakOrderQueue的数据结构,如下图所示。

我们上节分析过,WeakOrderQueue是由多个Link组成的,每个Link通过链表的方式进行关联,其中Head属性指向第一个Link,Tail属性指向最后一个Link。在每个Link中有多个Handle,Link中维护了一个读指针readIndex,标识着读取Link中Handle的位置。继续分析transfer()方法,首先获取头节点,并判断头节点是否为空,如果头节点为空,说明当前WeakOrderQueue并没有Link,返回false。if(head.readIndex==LINK_CAPACITY)判断读指针是否为16,因为Link中元素最大数量就是16,所以如果读指针为16,说明当前Link中的数据都被取走了。接着判断head.next==null,表示是否还有下一个Link,如果没有,则说明当前WeakOrderQueue没有元素了,返回false。如果当前Head的下一个节点不为null,则将当前头节点指向下一个节点,将原来的头节点进行释放,其移动关系如下图所示。

继续往下看,获取头节点的读指针和Head中元素的数量,计算可以传输元素的大小,如果大小为0,则返回false,如下图所示。

接着,获取当前Stack的大小,当前Stack大小加上可以传输的大小表示Stack中所需要的容量。if(expectedCapacity>dst.elements.length)表示如果需要的容量大于当前Stack中所维护的数组的大小,则将Stack中维护的数组进行扩容,进入if块中。扩容之后会返回actualCapacity,表示扩容之后的大小。再看srcEnd=min(srcStart+actualCapacity-dstSize,srcEnd)这步,srcEnd表示可以从Link中获取的最后一个元素的下标。

这里对srcStart+actualCapacity-dstSize进行拆分,actualCapacity-dstSize表示扩容后的大小-原Stack的大小,也就是最多能往Stack中传输多少元素。读指针+可以往Stack传输的数量和,表示往Stack中传输的最后一个下标,这里的下标和srcEnd中取一个较小的值,也就是既不能超过Stack的容量,也不能造成当前Link中下标越界。

继续往下看,int newDstSize=dstSize表示初始化Stack的下标,表示Stack中从这个下标开始添加数据。然后判断srcStart!=srcEnd,表示能不能从Link中获取内容,如果不能,则返回false,如果可以,则进入if块中,接着获取当前Link的数组Elements和Stack中的数组Elements,通过for循环,以数组下标的方式不断将当前Link中的数据放入Stack中,for循环中首先获取Link的第i个元素,接下来关注一个细节。

这里element.recycleId==0表示对象没有被回收过,则赋值为lastRecycledId,lastRecycledId是WeakOrderQueue中的唯一下标,通过赋值标记Element被回收过,然后继续判断element.recycleId!=element.lastRecycledId,表示该对象被回收过,但是回收的recycleId却不是最后一次回收lastRecycledId,这是一种异常情况,表示一个对象在不同的地方被回收过两次,这种情况则抛出异常,接着将Link的第i个元素设置为null,继续往下看。

这里表示控制回收站回收的频率,之前的章节中分析过,这里不再赘述。

● element.stack=dst表示将Handle的Stack属性设置到当前Stack。

● dstElems[newDstSize++]=element表示通过数组下标的方式将Link中的Handle赋值到Stack的数组中。

继续往下看:

这里的if表示循环结束后,如果Link中的数据已经回收完毕,并且还有下一个节点则会进到reclaimSpace()方法。跟到reclaimSpace()方法。

将availableSharedCapacity加上16,表示WeakOrderQueue还可以继续插入Link。继续看transfer()方法。

● this.head=head.next表示将头节点后移一个元素。

● head.readIndex=srcEnd表示将读指针指向srcEnd,下一次读取可以从srcEnd开始。

● if(dst.size==newDstSize)表示没有向Stack传输任何对象,则返回false,否则就通过dst.size=newDstSize更新Stack的大小为newDstSize,并返回true。

以上就是从Link中往Stack中传输数据的过程。

展开阅读全文

页面更新:2024-03-31

标签:下标   数组   节点   线程   指针   属性   元素   对象   代码   工具   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top