您的位置:首页 > 运维架构 > Apache

一个LRUMap的实现——来自apache common-collections框架

2016-03-04 23:34 661 查看
30. 下面哪个Map最适合用来实现LRU Cache?

A. Hashtable

B. TreeMap

C. HashMap

D. IdentityHashMap

E. WeakHashMap

网上给的答案是A。为什么?

大家一般如何实现这个 最近最少使用 cache?

今天主要分享一个LRUmap的实现。我们经常会用到需要使用map来保存数据的时候,由于map本身的映射高效,最适合做随机读取的存储结构。当然LRU算法是在有限大小的存储集合下的一种调度算法,即最近最少使用。对于一个给定大小限制的容器,如何分配资源就涉及到调度,而LRU就是一种经典的调度了,在容器中定义一个最后使用时间,当容器满时,再来新的元素,那么淘汰最近最少使用的元素,把新的元素替换之,这是最直接的思想。

apache common-collections框架里有一个LRUMap的实现,其继承自抽象的linkedmap和抽象的hashmap。下面给出一段测试代码,来看看LRU的直观效果:

1: public static void main(String[] args) {

2:         // TODO Auto-generated method stub

3:         LRUMap map = new LRUMap(3);

4:         map.put("1", 1);

5:         map.put("2", 2);

6:         //map.get("1");

7:         map.put("3", 3);

8:         map.put("4", 4);

9:

10:         for(Iterator it = map.entrySet().iterator();it.hasNext();){

11:             System.out.println(it.next());

12:         }

13:     }


使用的版本是common-collections3.2.1,直接执行,结果会显示

2=2

3=3

4=4

如果把第6行的注释打开,那么执行结果会是

1=1

3=3

4=4

这样就符合了LRU的原理,在调用了一次get后,1对应的数据不再是最近最少使用。

具体实现也颇有趣,LRUmap继承linkedmap,维护了一个linked list来保存内部数据

1: /** Header in the linked list */

2:     protected transient LinkEntry header;


linkentry又是一个循环双向的链表,且继承了hashentry,hashentry虽然也是commons框架自己实现,但是与jdk的实现同理,也是利用链接桶来预防冲突

1: protected static class LinkEntry extends HashEntry {

2:         /** The entry before this one in the order */

3:         protected LinkEntry before;

4:         /** The entry after this one in the order */

5:         protected LinkEntry after;

6:

7:         /**

8:          * Constructs a new entry.

9: * 

10:          * @param next  the next entry in the hash bucket sequence

11:          * @param hashCode  the hash code

12:          * @param key  the key

13:          * @param value  the value

14:          */

15:         protected LinkEntry(HashEntry next, int hashCode, Object key, Object value) {

16:             super(next, hashCode, key, value);

17:         }

18:     }


当进行put操作时,LRUMap会调用abstracthashmap的put方法,与传统一样,计算hashcode,在对应的hashcode桶定位index,然后做一个addmapping操作。本来在抽象类中的addmapping是传统的,等同于jdk中hashmap的addentry,但是LRUMap这里重写了addmapping,主要进行了map是否已满的判断,如果map未满,那么直接插入,否则,LRU将会定位到将被替换掉的entry的位置,然后做一个reuseMapping的操作,将该替换掉的entryremove,将新加进来的entry放到队尾。这样就完成了put操作。

进行get操作时,首先依据hashmap的原则找到entry,在返回value之前进行了LRU调整moveToMRU操作。该操作判断这个entry是否是队尾,如果是,那么什么都不用干,它就是最近被使用的,如果不是,那么调整它到队尾。

全部的源码见下:

1: /*

2:  *  Licensed to the Apache Software Foundation (ASF) under one or more

3:  *  contributor license agreements.  See the NOTICE file distributed with

4:  *  this work for additional information regarding copyright ownership.

5:  *  The ASF licenses this file to You under the Apache License, Version 2.0

6:  *  (the "License"); you may not use this file except in compliance with

7:  *  the License.  You may obtain a copy of the License at

8:  *

9:  *      http://www.apache.org/licenses/LICENSE-2.0[/code] 
10:  *

11:  *  Unless required by applicable law or agreed to in writing, software

12:  *  distributed under the License is distributed on an "AS IS" BASIS,

13:  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

14:  *  See the License for the specific language governing permissions and

15:  *  limitations under the License.

16:  */

17: package org.apache.commons.collections.map;

18:

19: import java.io.IOException;

20: import java.io.ObjectInputStream;

21: import java.io.ObjectOutputStream;

22: import java.io.Serializable;

23: import java.util.Map;

24:

25: import org.apache.commons.collections.BoundedMap;

26:

27: /**

28:  * A <code>Map</code> implementation with a fixed maximum size which removes

29:  * the least recently used entry if an entry is added when full.

30:  * <p>

31:  * The least recently used algorithm works on the get and put operations only.

32:  * Iteration of any kind, including setting the value by iteration, does not

33:  * change the order. Queries such as containsKey and containsValue or access

34:  * via views also do not change the order.

35:  * <p>

36:  * The map implements <code>OrderedMap</code> and entries may be queried using

37:  * the bidirectional <code>OrderedMapIterator</code>. The order returned is

38:  * least recently used to most recently used. Iterators from map views can

39:  * also be cast to <code>OrderedIterator</code> if required.

40:  * <p>

41:  * All the available iterators can be reset back to the start by casting to

42:  * <code>ResettableIterator</code> and calling <code>reset()</code>.

43:  * <p>

44:  * <strong>Note that LRUMap is not synchronized and is not thread-safe.</strong>

45:  * If you wish to use this map from multiple threads concurrently, you must use

46:  * appropriate synchronization. The simplest approach is to wrap this map

47:  * using {@link java.util.Collections#synchronizedMap(Map)}. This class may throw

48:  * <code>NullPointerException</code>'s when accessed by concurrent threads.

49:  *

50:  * @since Commons Collections 3.0 (previously in main package v1.0)

51:  * @version $Revision: 646777 $ $Date: 2008-04-10 13:33:15 +0100 (Thu, 10 Apr 2008) $

52:  *

53:  * @author James Strachan

54:  * @author Morgan Delagrange

55:  * @author Stephen Colebourne

56:  * @author Mike Pettypiece

57:  * @author Mario Ivankovits

58:  */

59: public class LRUMap

60:         extends AbstractLinkedMap implements BoundedMap, Serializable, Cloneable {

61:

62:     /** Serialisation version */

63:     private static final long serialVersionUID = -612114643488955218L;

64:     /** Default maximum size */

65:     protected static final int DEFAULT_MAX_SIZE = 100;

66:

67:     /** Maximum size */

68:     private transient int maxSize;

69:     /** Scan behaviour */

70:     private boolean scanUntilRemovable;

71:

72:     /**

73:      * Constructs a new empty map with a maximum size of 100.

74:      */

75:     public LRUMap() {

76:         this(DEFAULT_MAX_SIZE, DEFAULT_LOAD_FACTOR, false);

77:     }

78:

79:     /**

80:      * Constructs a new, empty map with the specified maximum size.

81:      *

82:      * @param maxSize  the maximum size of the map

83:      * @throws IllegalArgumentException if the maximum size is less than one

84:      */

85:     public LRUMap(int maxSize) {

86:         this(maxSize, DEFAULT_LOAD_FACTOR);

87:     }

88:

89:     /**

90:      * Constructs a new, empty map with the specified maximum size.

91:      *

92:      * @param maxSize  the maximum size of the map

93:      * @param scanUntilRemovable  scan until a removeable entry is found, default false

94:      * @throws IllegalArgumentException if the maximum size is less than one

95:      * @since Commons Collections 3.1

96:      */

97:     public LRUMap(int maxSize, boolean scanUntilRemovable) {

98:         this(maxSize, DEFAULT_LOAD_FACTOR, scanUntilRemovable);

99:     }

100:

101:     /**

102:      * Constructs a new, empty map with the specified initial capacity and

103:      * load factor.

104:      *

105:      * @param maxSize  the maximum size of the map, -1 for no limit,

106:      * @param loadFactor  the load factor

107:      * @throws IllegalArgumentException if the maximum size is less than one

108:      * @throws IllegalArgumentException if the load factor is less than zero

109:      */

110:     public LRUMap(int maxSize, float loadFactor) {

111:         this(maxSize, loadFactor, false);

112:     }

113:

114:     /**

115:      * Constructs a new, empty map with the specified initial capacity and

116:      * load factor.

117:      *

118:      * @param maxSize  the maximum size of the map, -1 for no limit,

119:      * @param loadFactor  the load factor

120:      * @param scanUntilRemovable  scan until a removeable entry is found, default false

121:      * @throws IllegalArgumentException if the maximum size is less than one

122:      * @throws IllegalArgumentException if the load factor is less than zero

123:      * @since Commons Collections 3.1

124:      */

125:     public LRUMap(int maxSize, float loadFactor, boolean scanUntilRemovable) {

126:         super((maxSize < 1 ? DEFAULT_CAPACITY : maxSize), loadFactor);

127:         if (maxSize < 1) {

128:             throw new IllegalArgumentException("LRUMap max size must be greater than 0");

129:         }

130:         this.maxSize = maxSize;

131:         this.scanUntilRemovable = scanUntilRemovable;

132:     }

133:

134:     /**

135:      * Constructor copying elements from another map.

136:      * <p>

137:      * The maximum size is set from the map's size.

138:      *

139:      * @param map  the map to copy

140:      * @throws NullPointerException if the map is null

141:      * @throws IllegalArgumentException if the map is empty

142:      */

143:     public LRUMap(Map map) {

144:         this(map, false);

145:     }

146:

147:     /**

148:      * Constructor copying elements from another map.

149:      * <p/>

150:      * The maximum size is set from the map's size.

151:      *

152:      * @param map  the map to copy

153:      * @param scanUntilRemovable  scan until a removeable entry is found, default false

154:      * @throws NullPointerException if the map is null

155:      * @throws IllegalArgumentException if the map is empty

156:      * @since Commons Collections 3.1

157:      */

158:     public LRUMap(Map map, boolean scanUntilRemovable) {

159:         this(map.size(), DEFAULT_LOAD_FACTOR, scanUntilRemovable);

160:         putAll(map);

161:     }

162:

163:     //-----------------------------------------------------------------------

164:     /**

165:      * Gets the value mapped to the key specified.

166:      * <p>

167:      * This operation changes the position of the key in the map to the

168:      * most recently used position (first).

169:      *

170:      * @param key  the key

171:      * @return the mapped value, null if no match

172:      */

173:     public Object get(Object key) {

174:         LinkEntry entry = (LinkEntry) getEntry(key);

175:         if (entry == null) {

176:             return null;

177:         }

178:         moveToMRU(entry);

179:         return entry.getValue();

180:     }

181:

182:     //-----------------------------------------------------------------------

183:     /**

184:      * Moves an entry to the MRU position at the end of the list.

185:      * <p>

186:      * This implementation moves the updated entry to the end of the list.

187:      *

188:      * @param entry  the entry to update

189:      */

190:     protected void moveToMRU(LinkEntry entry) {

191:         if (entry.after != header) {

192:             modCount++;

193:             // remove

194:             entry.before.after = entry.after;

195:             entry.after.before = entry.before;

196:             // add first

197:             entry.after = header;

198:             entry.before = header.before;

199:             header.before.after = entry;

200:             header.before = entry;

201:         } else if (entry == header) {

202:             throw new IllegalStateException("Can't move header to MRU" +

203:                 " (please report this to commons-dev@jakarta.apache.org)");

204:         }

205:     }

206:

207:     /**

208:      * Updates an existing key-value mapping.

209:      * <p>

210:      * This implementation moves the updated entry to the top of the list

211:      * using {@link #moveToMRU(AbstractLinkedMap.LinkEntry)}.

212:      *

213:      * @param entry  the entry to update

214:      * @param newValue  the new value to store

215:      */

216:     protected void updateEntry(HashEntry entry, Object newValue) {

217:         moveToMRU((LinkEntry) entry);  // handles modCount

218:         entry.setValue(newValue);

219:     }

220:

221:     /**

222:      * Adds a new key-value mapping into this map.

223:      * <p>

224:      * This implementation checks the LRU size and determines whether to

225:      * discard an entry or not using {@link #removeLRU(AbstractLinkedMap.LinkEntry)}.

226:      * <p>

227:      * From Commons Collections 3.1 this method uses {@link #isFull()} rather

228:      * than accessing <code>size</code> and <code>maxSize</code> directly.

229:      * It also handles the scanUntilRemovable functionality.

230:      *

231:      * @param hashIndex  the index into the data array to store at

232:      * @param hashCode  the hash code of the key to add

233:      * @param key  the key to add

234:      * @param value  the value to add

235:      */

236:     protected void addMapping(int hashIndex, int hashCode, Object key, Object value) {

237:         if (isFull()) {

238:             LinkEntry reuse = header.after;

239:             boolean removeLRUEntry = false;

240:             if (scanUntilRemovable) {

241:                 while (reuse != header && reuse != null) {

242:                     if (removeLRU(reuse)) {

243:                         removeLRUEntry = true;

244:                         break;

245:                     }

246:                     reuse = reuse.after;

247:                 }

248:                 if (reuse == null) {

249:                     throw new IllegalStateException(

250:                         "Entry.after=null, header.after" + header.after + " header.before" + header.before +

251:                         " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +

252:                         " Please check that your keys are immutable, and that you have used synchronization properly." +

253:                         " If so, then please report this to commons-dev@jakarta.apache.org as a bug.");

254:                 }

255:             } else {

256:                 removeLRUEntry = removeLRU(reuse);

257:             }

258:

259:             if (removeLRUEntry) {

260:                 if (reuse == null) {

261:                     throw new IllegalStateException(

262:                         "reuse=null, header.after=" + header.after + " header.before" + header.before +

263:                         " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +

264:                         " Please check that your keys are immutable, and that you have used synchronization properly." +

265:                         " If so, then please report this to commons-dev@jakarta.apache.org as a bug.");

266:                 }

267:                 reuseMapping(reuse, hashIndex, hashCode, key, value);

268:             } else {

269:                 super.addMapping(hashIndex, hashCode, key, value);

270:             }

271:         } else {

272:             super.addMapping(hashIndex, hashCode, key, value);

273:         }

274:     }

275:

276:     /**

277:      * Reuses an entry by removing it and moving it to a new place in the map.

278:      * <p>

279:      * This method uses {@link #removeEntry}, {@link #reuseEntry} and {@link #addEntry}.

280:      *

281:      * @param entry  the entry to reuse

282:      * @param hashIndex  the index into the data array to store at

283:      * @param hashCode  the hash code of the key to add

284:      * @param key  the key to add

285:      * @param value  the value to add

286:      */

287:     protected void reuseMapping(LinkEntry entry, int hashIndex, int hashCode, Object key, Object value) {

288:         // find the entry before the entry specified in the hash table

289:         // remember that the parameters (except the first) refer to the new entry,

290:         // not the old one

291:         try {

292:             int removeIndex = hashIndex(entry.hashCode, data.length);

293:             HashEntry[] tmp = data;  // may protect against some sync issues

294:             HashEntry loop = tmp[removeIndex];

295:             HashEntry previous = null;

296:             while (loop != entry && loop != null) {

297:                 previous = loop;

298:                 loop = loop.next;

299:             }

300:             if (loop == null) {

301:                 throw new IllegalStateException(

302:                     "Entry.next=null, data[removeIndex]=" + data[removeIndex] + " previous=" + previous +

303:                     " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +

304:                     " Please check that your keys are immutable, and that you have used synchronization properly." +

305:                     " If so, then please report this to commons-dev@jakarta.apache.org as a bug.");

306:             }

307:

308:             // reuse the entry

309:             modCount++;

310:             removeEntry(entry, removeIndex, previous);

311:             reuseEntry(entry, hashIndex, hashCode, key, value);

312:             addEntry(entry, hashIndex);

313:         } catch (NullPointerException ex) {

314:             throw new IllegalStateException(

315:                     "NPE, entry=" + entry + " entryIsHeader=" + (entry==header) +

316:                     " key=" + key + " value=" + value + " size=" + size + " maxSize=" + maxSize +

317:                     " Please check that your keys are immutable, and that you have used synchronization properly." +

318:                     " If so, then please report this to commons-dev@jakarta.apache.org as a bug.");

319:         }

320:     }

321:

322:     /**

323:      * Subclass method to control removal of the least recently used entry from the map.

324:      * <p>

325:      * This method exists for subclasses to override. A subclass may wish to

326:      * provide cleanup of resources when an entry is removed. For example:

327:      * <pre>

328:      * protected boolean removeLRU(LinkEntry entry) {

329:      *   releaseResources(entry.getValue());  // release resources held by entry

330:      *   return true;  // actually delete entry

331:      * }

332:      * </pre>

333:      * <p>

334:      * Alternatively, a subclass may choose to not remove the entry or selectively

335:      * keep certain LRU entries. For example:

336:      * <pre>

337:      * protected boolean removeLRU(LinkEntry entry) {

338:      *   if (entry.getKey().toString().startsWith("System.")) {

339:      *     return false;  // entry not removed from LRUMap

340:      *   } else {

341:      *     return true;  // actually delete entry

342:      *   }

343:      * }

344:      * </pre>

345:      * The effect of returning false is dependent on the scanUntilRemovable flag.

346:      * If the flag is true, the next LRU entry will be passed to this method and so on

347:      * until one returns false and is removed, or every entry in the map has been passed.

348:      * If the scanUntilRemovable flag is false, the map will exceed the maximum size.

349:      * <p>

350:      * NOTE: Commons Collections 3.0 passed the wrong entry to this method.

351:      * This is fixed in version 3.1 onwards.

352:      *

353:      * @param entry  the entry to be removed

354:      */

355:     protected boolean removeLRU(LinkEntry entry) {

356:         return true;

357:     }

358:

359:     //-----------------------------------------------------------------------

360:     /**

361:      * Returns true if this map is full and no new mappings can be added.

362:      *

363:      * @return <code>true</code> if the map is full

364:      */

365:     public boolean isFull() {

366:         return (size >= maxSize);

367:     }

368:

369:     /**

370:      * Gets the maximum size of the map (the bound).

371:      *

372:      * @return the maximum number of elements the map can hold

373:      */

374:     public int maxSize() {

375:         return maxSize;

376:     }

377:

378:     /**

379:      * Whether this LRUMap will scan until a removable entry is found when the

380:      * map is full.

381:      *

382:      * @return true if this map scans

383:      * @since Commons Collections 3.1

384:      */

385:     public boolean isScanUntilRemovable() {

386:         return scanUntilRemovable;

387:     }

388:

389:     //-----------------------------------------------------------------------

390:     /**

391:      * Clones the map without cloning the keys or values.

392:      *

393:      * @return a shallow clone

394:      */

395:     public Object clone() {

396:         return super.clone();

397:     }

398:

399:     /**

400:      * Write the map out using a custom routine.

401:      */

402:     private void writeObject(ObjectOutputStream out) throws IOException {

403:         out.defaultWriteObject();

404:         doWriteObject(out);

405:     }

406:

407:     /**

408:      * Read the map in using a custom routine.

409:      */

410:     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

411:         in.defaultReadObject();

412:         doReadObject(in);

413:     }

414:

415:     /**

416:      * Writes the data necessary for <code>put()</code> to work in deserialization.

417:      */

418:     protected void doWriteObject(ObjectOutputStream out) throws IOException {

419:         out.writeInt(maxSize);

420:         super.doWriteObject(out);

421:     }

422:

423:     /**

424:      * Reads the data necessary for <code>put()</code> to work in the superclass.

425:      */

426:     protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

427:         maxSize = in.readInt();

428:         super.doReadObject(in);

429:     }

430:

431: }


部分操作需要追溯其父类代码,这里就不贴了,有兴趣的朋友可以直接到源码阅读。

LRUMap对于构建缓存或者连接池之类的技术经常用到,common-collections框架给了现成的实现,大家在不需要修改的情况下,完全可以不必reinvent the wheel,直接用,好用且稳定。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: