blogjava-凯发k8网页登录

blogjava-凯发k8网页登录http://www.blogjava.net/dlevin/category/54910.htmlin general the oo style is to use a lot of little objects with a lot of little methods that give us a lot of plug points for overriding and variation. to do is to be -nietzsche, to bei is to do -kant, do be do be do -sinatrazh-cnwed, 12 aug 2015 23:15:00 gmtwed, 12 aug 2015 23:15:00 gmt60深入guava源码之stripehttp://www.blogjava.net/dlevin/archive/2013/12/25/407990.htmldlevindlevinwed, 25 dec 2013 02:03:00 gmthttp://www.blogjava.net/dlevin/archive/2013/12/25/407990.htmlhttp://www.blogjava.net/dlevin/comments/407990.htmlhttp://www.blogjava.net/dlevin/archive/2013/12/25/407990.html#feedback3http://www.blogjava.net/dlevin/comments/commentrss/407990.htmlhttp://www.blogjava.net/dlevin/services/trackbacks/407990.html当前jdk对并发编程的支持 sun在java5中引入了concurrent包,它对java的并发编程提供了强大的支持。首先,它提供了lock接口,可用了更细粒度的控制锁的区域,它的实现类有reentrantlock,readlock,writelock,其中readlock和writelock共同用于实现reetrantreadwritelock(它继承自readwritelock,但是没有实现lock接口,readwritelock接口也没有继承lock接口)。而且,它还提供了一些常用并发场景下的类工具:semaphore、countdownlatch和cyclicbarrier。它们个字的应用场景:
  1. semaphore(信号量)
    有n个非线程安全的资源(资源池),这些资源使用一个semaphore(计数信号量)保护,每个线程在使用这些资源时需要首先获得一个信号量(acquire)表示当前资源池还有可用资源,然后线程从该资源池中获取并移除一个资源,在使用完后,将该资源交回给资源池,并释放已经获得信号量(release)(这里的“移除”、“交回”并不一定需要显示操作,只是一种形象的描述,之所以这么描述是应为这里的各个资源是一样的,因而对一个线程它每次拿到的资源不一定是同一个资源,用于区分stripe的使用场景),其中pool是一种典型的应用。
  2. countdownlatch(闭锁)
    有n个task,它们执行完成后需要执行另外一个收尾的task(aggregated task),比如在做report计算中,有n个report要计算,而在所有report计算完成后需要生成一个基于所有report结果的一个总的report,而这个总的report需要等到所有report计算出结果后才能开始,此时就可以定义一个countdownlatch,其初始值是n,在总的report计算前调用countdownlatch的await方法等待其他report执行完成,而其他report在完成后都会调用countdownlatch中的countdown方法。
  3. cyclicbarrier(关卡)
    每个线程执行完成后需要等待,直到n个线程都执行完成后,才能继续执行,在n个线程执行完成之后,而下一次执行开始之前可以添加自定义逻辑(通过构建cyclicbarrier实例时传入一个runnable实例自定义逻辑),即在每个线程执行完成后调用cyclicbarrier的await方法并等待(即所谓的关卡),当n个线程都完成后,自定义的runnable实例会自动被执行(如果存在这样的runnable实例的话),然后所有线程继续下一次执行。这个现实中的例子没有想到比较合适的。。。。
  4. exchanger(交换者)
    exchanger是一种特殊的cyclicbarrier,它只有两个线程参与,一个生产者,一个消费者,有两个队列共同参与,生产者和消费者各自有一个队列,其中生产者向它的队列添加数据,而消费者从它包含的队列中拿数据,当生产者中的队列满时调用exchange方法,传入自己原有的队列,期待交换得到消费者中空的队列;而当消费者中的队列满时同样调用exchange方法,传入自己的原有队列,期待获取到生产者中已经填满的队列。这样,生产者和消费者可以和谐的生产消费,并且它们的步骤是一致的(不管哪一方比另一方快都会等待另一方)。
最后,java5中还提供了一些atomic类以实现简单场景下高效非lock方式的线程安全,以及blockingqueue、synchronizer、completionservice、concurrenthashmap等工具类。

在这里需要特别添加对concurrenthashmap的描述,因为guava中的stripe就是对concurrenthashmap实现思想的抽象。在《》一文中已经详细讲述了concurrenthashmap的实现,我们都知道concurrenthashmap的实现是基于segment的,它内部包含了多个segment,因而它内部的锁是基于segment而不是整个map,从而减小了锁的粒度,提升了性能。而这种分段锁不仅仅在hashmap用到。

stripe的应用场景

虽然jdk中已经为我们提供了很多用于并发编程的工具类,但是它并没有提供对以下应用场景的支持:有n个资源,我们希望对每个资源的操作都是线程安全的,这里我们不能用semaphore,因为semaphore是一个池的概念,它所管理的资源是同质的,比如从数据库的连接池中获取connection操作的一种实现方式是内部保存一个semaphore变量,在每次获取connection时,先调用semaphore的acquire方法以保证连接池中还有空闲的connection,如果有,则可以随机的选择一个connection实例,当connection实例返回时,该connection实例必须从空闲列表中移除,从而保证只有一个线程获取到connection,以保证一次只有一个线程使用一个connection(在java中数据库的connection是线程安全,但是我们在使用时依然会用连接池的方式创建多个connection而不是在一个应用程序中只用一个connection是因为有些数据库厂商在实现connection时,一个connection内的所有操作都时串行的,而不是并行的,比如mysql的connection实现,因而为了提升并行性,采用多个connection方式)。而这里的需求是对每个资源的操作都是线程安全的,比如对jdk中hashmap的实现采用一个数组链表的结构(参考《》),如果我们将链表作为一个资源单位(这里的链表资源和上述的数据库连接资源是不一样的,对数据库连接每个线程只需要拿到任意一个connection实例即可,而这里的链表资源则是不同链表是不一样的,因而对每个操作,我们需要获取特定的链表,然后对链表以线程安全的方式操作,因为这里多个线程会对同一个链表同时操作),那么为了保证对各个单独链表操作的线程安全(如hashmap的put操作,不考虑rehash的情况,有些其他操作需要更大粒度的线程安全,比如contains等),其中一种简单的实现方式是为每条链表关联一个锁,对每条链表的读写操作使用其关联锁即可。然而如果链表很多,就需要使用很多锁,会消耗很多资源,虽然它的锁粒度最小,并发性很高。然而如果各个链表之间没有很高的并发性,我们就可以让多个链表共享一个锁以减少锁的使用量,虽然增大了锁的粒度,但是如果这些链表的并发程度并不是很高,那增大的锁的粒度对并发性并没有很大的影响。

在实际应用中,我们有一个cache系统,它包含key和payload的键值对(map),在cache中map的实现已经是线程安全了,然而我们不仅仅是向cache中写数据要保证线程安全,在操作payload时,也需要保证线程安全。因为我们在cache中的数据量很大,为每个payload配置一个单独的锁显然不现实,也不需要因为它们没有那么高的并发行,因而我们需要一种机制将key分成不同的group,而每个group共享一个锁(这就是concurrenthashmap的实现思路)。通过key即可获得一个锁,并且每个相同的key获得的锁实例是相同的(获得相同锁实例的key它们不一定相等,因为这是一对多的关系)。

stripe的简单实现

根据以上应用场景,stripe的实现很简单,只需要内部保存一个lock数组,对每个给定的key,计算其hash值,根据hash值计算其锁对应的数组下标,而该下标下的lock实例既是和该key关联的lock实例。这里通过hash值把key和lock实例关联起来,为了扩展性,在实现时还可以把计算数组下标的逻辑抽象成一个接口,用户可以通过传入自定义该接口的实现类实例加入用户自定义的关联逻辑,默认采用hash值关联方式。

stripe在guava中的实现

在guava中,stripe以抽象类的形式存在,它定义了通过给定key或index获得相应lock/semaphore/readwritelock实例:
public abstract class striped {
  /**
   * returns the stripe that corresponds to the passed key. it is always guaranteed that if
   * {
@code key1.equals(key2)}, then {@code get(key1) == get(key2)}.
   *
   * 
@param key an arbitrary, non-null key
   * 
@return the stripe that the passed key corresponds to
   
*/
  public abstract l get(object key);

  /**
   * returns the stripe at the specified index. valid indexes are 0, inclusively, to
   * {
@code size()}, exclusively.
   *
   * 
@param index the index of the stripe to return; must be in {@code [0size())}
   * 
@return the stripe at the specified index
   
*/
  public abstract l getat(int index);

  /**
   * returns the index to which the given key is mapped, so that getat(indexfor(key)) == get(key).
   
*/
  abstract int indexfor(object key);

  /**
   * returns the total number of stripes in this instance.
   
*/
  public abstract int size();

  /**
   * returns the stripes that correspond to the passed objects, in ascending (as per
   * {
@link #getat(int)}) order. thus, threads that use the stripes in the order returned
   * by this method are guaranteed to not deadlock each other.
   *
   * 

it should be noted that using a {@code striped} with relatively few stripes, and
   * {
@code bulkget(keys)} with a relative large number of keys can cause an excessive number
   * of shared stripes (much like the birthday paradox, where much fewer than anticipated birthdays
   * are needed for a pair of them to match). please consider carefully the implications of the
   * number of stripes, the intended concurrency level, and the typical number of keys used in a
   * {
@code bulkget(keys)} operation. see http://www.mathpages.com/home/kmath199.htm">balls
   * in bins model for mathematical formulas that can be used to estimate the probability of
   * collisions.
   *
   * 
@param keys arbitrary non-null keys
   * 
@return the stripes corresponding to the objects (one per each object, derived by delegating
   *         to {
@link #get(object)}; may contain duplicates), in an increasing index order.
   
*/
  public iterable bulkget(iterable keys);
}

可以使用一下几个静态工厂方法创建相应的striped实例,其中lazyweakxxx创建的striped实例中锁以弱引用的方式存在(在什么样的场景中使用呢?):
/**
 * creates a {
@code striped} with eagerly initialized, strongly referenced locks.
 * every lock is reentrant.
 *
 * 
@param stripes the minimum number of stripes (locks) required
 * 
@return a new {@code striped}
 
*/
public static striped lock(int stripes);
/**
 * creates a {
@code striped} with lazily initialized, weakly referenced locks.
 * every lock is reentrant.
 *
 * 
@param stripes the minimum number of stripes (locks) required
 * 
@return a new {@code striped}
 
*/
public static striped lazyweaklock(int stripes);
/**
 * creates a {
@code striped} with eagerly initialized, strongly referenced semaphores,
 * with the specified number of permits.
 *
 * 
@param stripes the minimum number of stripes (semaphores) required
 * 
@param permits the number of permits in each semaphore
 * 
@return a new {@code striped}
 
*/
public static striped semaphore(int stripes, final int permits);
/**
 * creates a {
@code striped} with lazily initialized, weakly referenced semaphores,
 * with the specified number of permits.
 *
 * 
@param stripes the minimum number of stripes (semaphores) required
 * 
@param permits the number of permits in each semaphore
 * 
@return a new {@code striped}
   
*/
public static striped lazyweaksemaphore(int stripes, final int permits);
/**
 * creates a {
@code striped} with eagerly initialized, strongly referenced
 * read-write locks. every lock is reentrant.
 *
 * 
@param stripes the minimum number of stripes (locks) required
 * 
@return a new {@code striped}
 
*/
public static striped readwritelock(int stripes);
/**
 * creates a {
@code striped} with lazily initialized, weakly referenced
 * read-write locks. every lock is reentrant.
 *
 * 
@param stripes the minimum number of stripes (locks) required
 * 
@return a new {@code striped}
 
*/
public static striped lazyweakreadwritelock(int stripes);

striped有两个具体实现类,compactstriped和lazystriped,他们都继承自poweroftwostriped(用于表达内部保存的stripes值是2的指数值)。poweroftwostriped实现了indexfor()方法,它使用hash值做映射函数:
  private abstract static class poweroftwostriped extends striped {
    /** capacity (power of two) minus one, for fast mod evaluation */
    final int mask;

    @override final int indexfor(object key) {
      int hash = smear(key.hashcode());
      return hash & mask;
    }
  }
  private static int smear(int hashcode) {
    hashcode ^= (hashcode >>> 20) ^ (hashcode >>> 12);
    return hashcode ^ (hashcode >>> 7) ^ (hashcode >>> 4);
  }
compactstriped类使用一个数组保存所有的lock/semaphore/readwritelock实例,在初始化时就建立所有的锁实例;而lazystriped类使用一个值为weakreference的concurrentmap做为数据结构,index值为key,lock/semaphore/readwritelock的weakreference为值,所有锁实例在用到时动态创建。在compactstriped中创建锁实例时对reentrantlock/semaphore创建采用paddedxxx版本,不知道为何要做pad。

striped类实现的类图如下:


dlevin 2013-12-25 10:03 发表评论
]]>
java cache系列之guava cache实现详解http://www.blogjava.net/dlevin/archive/2013/10/20/404847.htmldlevindlevinsat, 19 oct 2013 16:17:00 gmthttp://www.blogjava.net/dlevin/archive/2013/10/20/404847.htmlhttp://www.blogjava.net/dlevin/comments/404847.htmlhttp://www.blogjava.net/dlevin/archive/2013/10/20/404847.html#feedback3http://www.blogjava.net/dlevin/comments/commentrss/404847.htmlhttp://www.blogjava.net/dlevin/services/trackbacks/404847.html
在guava cachebuilder的注释中给定guava cache以下的需求:
  1. automatic loading of entries into the cache
  2. least-recently-used eviction when a maximum size is exceeded
  3. time-based expiration of entries, measured since last access or last write
  4. keys automatically wrapped in weakreference
  5. values automatically wrapped in weakreference or softreference soft
  6. notification of evicted (or otherwise removed) entries
  7. accumulation of cache access statistics
对于这样的需求,如果要我们自己来实现,我们应该怎么设计?对于我来说,对于其核心实现我会做如下的设计:
  1. 定义一个cacheconfig类用于纪录所有的配置,如cacheloader,maximum size、expire time、key reference level、value reference level、eviction listener等。
  2. 定义一个cache接口,该接口类似map(或concurrentmap),但是为了和map区别开来,因而重新定义一个cache接口。
  3. 定义一个实现cache接口的类cacheimpl,它接收cacheconfig作为参数的构造函数,并将cacheconfig实例保存在字段中。
  4. 在实现上模仿concurrenthashmap的实现方式,有一个segment数组,其长度由配置的concurrencylevel值决定。为了实现最近最少使用算法(lru),添加accessqueue和writequeue字段,这两个queue内部采用双链表,每次新创建一个entry,就将这个entry加入到这两个queue的末尾,而每读取一个entry就将其添加到accessqueue的末尾,没更新一个entry将该entry添加到writequeue末尾。为了实现key和value上的weakreference、softreference,添加referencequeue类型的keyreferencequeue和valuereferencequeue字段。
  5. 在每次调用方法之前都遍历accessqueue和writequeue,如果发现有entry已经expire,就将该entry从这两个queue上和cache中移除。然后遍历keyreferencequeue和valuereference,如果发现有项存在,同样将它们移除。在移除时如果有evictionlistener注册着,则调用该listener。
  6. 对segment实现,它时一个cacheentry数组,cacheentry是一个链节点,它包含hash、key、vlaue、next。cacheentry根据是否需要包装在weakreference中创建weakentry或strongentry,而对value根据是否需要包装在weakreference、softreference中创建weakvaluereference、softvaluereference、strongvaluereference。在get操作中对于需要使用cacheloader加载的值先添加一个具有loadingvaluereference值的entry,这样可以保证同一个key只加载依次。在加载成功后将loadingvaluereference根据配置替换成其他weak、soft、strong valuereference。
  7. 对于cache access statistics,只需要有一个类在需要的地方做一些统计计数即可。
  8. 最后我必须得承认以上的设计有很多是对guava cache的参考,我有点后悔没有在看源码之前考虑这个问题,等看过以后思路就被它的实现给羁绊了。。。。
guava cache的数据结构
因为新进一家公司,要熟悉新公司项目以及项目用到的第三方库的代码,因而几个月来看了许多代码。然后越来越发现要理解一个项目的最快方法是先搞清楚该项目的底层数据结构,然后再去看构建于这些数据结构以上的逻辑就会容易许多。记得在还是学生的时候,有在一本书上看到过一个大牛说的一句话:程序=数据结构+算法;当时对这句话并不是和理解,现在是很赞同这句话,我对算法接触的不多,因而我更倾向于将这里的算法理解长控制数据流动的逻辑。因而我们先来熟悉一下guava cache的数据结构。

cache类似于map,它是存储键值对的集合,然而它和map不同的是它还需要处理evict、expire、dynamic load等逻辑,需要一些额外信息来实现这些操作。在面向对象思想中,经常使用类对一些关联性比较强的数据做封装,同时把操作这些数据相关的操作放到该类中。因而guava cache使用referenceentry接口来封装一个键值对,而用valuereference来封装value值。这里之所以用reference命令,是因为guava cache要支持weakreference key和softreference、weakreference value。

valuereference
对于valuereference,因为guava cache支持强引用的value、softreference value以及weakreference value,因而它对应三个实现类:strongvaluereference、softvaluereference、weakvaluereference。为了支持动态加载机制,它还有一个loadingvaluereference,在需要动态加载一个key的值时,先把该值封装在loadingvaluereference中,以表达该key对应的值已经在加载了,如果其他线程也要查询该key对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次(可以在evict以后重新加载)。在该只加载完成后,将loadingvaluereference替换成其他valuereference类型。对新创建的loadingvaluereference,由于其内部oldvalue的初始值是unset,它isactive为false,isloading为false,因而此时的loadingvaluereference的isactive为false,但是isloading为true。每个valuereference都纪录了weight值,所谓weight从字面上理解是“该值的重量”,它由weighter接口计算而得。weight在guava cache中由两个用途:1. 对weight值为0时,在计算因为size limit而evict是忽略该entry(它可以通过其他机制evict);2. 如果设置了maximumweight值,则当cache中weight和超过了该值时,就会引起evict操作。但是目前还不知道这个设计的用途。最后,guava cache还定义了stength枚举类型作为valuereference的factory类,它有三个枚举值:strong、soft、weak,这三个枚举值分别创建各自的valuereference,并且根据传入的weight值是否为1而决定是否要创建weight版本的valuereference。以下是valuereference的类图:

这里valuereference之所以要有对referenceentry的引用是因为在value因为weakreference、softreference被回收时,需要使用其key将对应的项从segment的table中移除;copyfor()函数的存在是因为在expand(rehash)重新创建节点时,对weakreference、softreference需要重新创建实例(个人感觉是为了保持对象状态不会相互影响,但是不确定是否还有其他原因),而对强引用来说,直接使用原来的值即可,这里很好的展示了对彼变化的封装思想;notifiynewvalue只用于loadingvaluereference,它的存在是为了对loadingvaluereference来说能更加及时的得到cacheloader加载的值。

referenceentry
referenceentry是guava cache中对一个键值对节点的抽象。和concurrenthashmap一样,guava cache由多个segment组成,而每个segment包含一个referenceentry数组,每个referenceentry数组项都是一条referenceentry链。并且一个referenceentry包含key、hash、valuereference、next字段。除了在referenceentry数组项中组成的链,在一个segment中,所有referenceentry还组成access链(accessqueue)和write链(writequeue),这两条都是双向链表,分别通过previousaccess、nextaccess和previouswrite、nextwrite字段链接而成。在对每个节点的更新操作都会将该节点重新链到write链和access链末尾,并且更新其writetime和accesstime字段,而没找到一个节点,都会将该节点重新链到access链末尾,并更新其accesstime字段。这两个双向链表的存在都是为了实现采用最近最少使用算法(lru)的evict操作(expire、size limit引起的evict)。

guava cache中的referenceentry可以是强引用类型的key,也可以weakreference类型的key,为了减少内存使用量,还可以根据是否配置了expireafterwrite、expireafteraccess、maximumsize来决定是否需要write链和access链确定要创建的具体reference:strongentry、strongwriteentry、strongaccessentry、strongwriteaccessentry等。创建不同类型的referenceentry由其枚举工厂类entryfactory来实现,它根据key的strongth类型、是否使用accessqueue、是否使用writequeue来决定不同的entryfactry实例,并通过它创建相应的referenceentry实例。referenceentry类图如下:

writequeue和accessqueue
为了实现最近最少使用算法,guava cache在segment中添加了两条链:write链(writequeue)和access链(accessqueue),这两条链都是一个双向链表,通过referenceentry中的previousinwritequeue、nextinwritequeue和previousinaccessqueue、nextinaccessqueue链接而成,但是以queue的形式表达。writequeue和accessqueue都是自定义了offer、add(直接调用offer)、remove、poll等操作的逻辑,对于offer(add)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对remove操作,直接从该链中移除该节点;对poll操作,将头节点的下一个节点移除,并返回。
  static final class writequeue extends abstractqueue> {
    final referenceentry head = new abstractreferenceentry() ....
    @override
    public boolean offer(referenceentry entry) {
      // unlink
      connectwriteorder(entry.getpreviousinwritequeue(), entry.getnextinwritequeue());
      // add to tail
      connectwriteorder(head.getpreviousinwritequeue(), entry);
      connectwriteorder(entry, head);
      return true;
    }
    @override
    public referenceentry peek() {
      referenceentry next = head.getnextinwritequeue();
      return (next == head) ? null : next;
    }
    @override
    public referenceentry poll() {
      referenceentry next = head.getnextinwritequeue();
      if (next == head) {
        return null;
      }
      remove(next);
      return next;
    }
    @override
    public boolean remove(object o) {
      referenceentry e = (referenceentry) o;
      referenceentry previous = e.getpreviousinwritequeue();
      referenceentry next = e.getnextinwritequeue();
      connectwriteorder(previous, next);
      nullifywriteorder(e);
      return next != nullentry.instance;
    }
    @override
    public boolean contains(object o) {
      referenceentry e = (referenceentry) o;
      return e.getnextinwritequeue() != nullentry.instance;
    }
....
  }

对于不需要维护writequeue和accessqueue的配置(即没有expire time或size limit的evict策略)来说,我们可以使用discarding_queue以节省内存:
  static final queueextends object> discarding_queue = new abstractqueue
网站地图