`
yangdong
  • 浏览: 66620 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

动态分类计数器

    博客分类:
  • Java
阅读更多
昨天的工作遇到一个需求:要求根据用户 ID(Long 型)记录他的访问某个页面的次数。并且在所有用户的累积计数达到某个值后输出、清空并重新计数。这个记数有个特点,某些用户的访问次数会异乎寻常地多。

因为这个记录只是在高访问量的时候做,所以对程序的并发度要求比较高。我们知道,高访问量下数字型对象的装箱拆箱会极大影响效率;在高并发下,锁竞争也会极大影响效率。

对于装箱拆箱的问题,我们可以在一开始的时候用 new Long(0) 之类的来初始化计数器,但是之后的自增计数器的操作还是会先进行拆箱,在栈里运算后再进行装箱。并发的问题我们看起来可以用 ConcurrentHashMap 来解决。但是对于这个需求来说,我们需要在自增计数器之前先把之前的值取出来,所以就需要把 ConcurrentHashMap 给锁住,这样就失去了使用 ConcurrentHashMap 的意义,使得它褪化成了 SynchronizedMap。当然也可以在 CHM 中放入一个自己封装的对象,其中包含计数器,使用的时候给这个封装过的对象上锁。

我个人的想法是使用自定义的 HashMap(不实现 Map 接口),支持并发地自增计数器,同时避免装箱拆箱的问题。最好还能进行分段加锁。因为分段加锁比较复杂,所以我目前没有实现。下面的程序实现了在避免装箱拆箱的前提下并发自增计数器。但不支持动态扩容,不支持序列化。使用的 hash 算法和数据结构抄袭了 JDK 6 的 HashMap。所以如果你感兴趣自己调试的话,你会发现 java.util.HashMap 的 table 数组的内容与此类的 table 数组的内容分布完全一致(前提是两者的数据一样,容量一样)。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.sun.istack.internal.Nullable;

/**
 * 并发计数器哈希 Map(没有实现 {@link Map} 接口)。此类适合于进行动态分类统计。因为不支持动态扩容,所以要求计数达到某个值后
 * 手动清空此 Map 并重新计数(通常客户代码此时应将 Map 清空前的内容写入日志)。如果强行在小容量的时候塞入大量
 * 数据,此 Map 不会被撑爆,但是性能会急剧下降。
 * <p>
 * 性能提示:对于某一部分键的计数器自增操作特别多时此类的表现最佳。如果每个键的计数都很少而键却很多时此类的性能最差。
 * 重置操作会锁住整个实例,使得任何计数器的读取和写入操作都被阻塞。
 * <p>
 * 此类的所有公共接口都是线程安全的。
 * <p>
 * 此类不支持序列化和反序列化。
 * <p>
 * 类的关键实现拷贝了 JDK 6 的 HashMap。
 * 
 * @author ydong
 */
public final class ConcurrentCounterHashMap {
	
	/**
	 * 构造一个新实例。capacity 参数指定了容量。此容量一但指定就不会更改。此实例也不会动态扩容。
	 * 强行塞入过多数据会导致性能下降。
	 * 
	 * @param capacity 容量
	 */
	public ConcurrentCounterHashMap(final int capacity) {
		if (capacity <= 0) throw new IllegalArgumentException();
		if (capacity > 2 << 30) capacity = 2 << 30;
		int _capacity = 1;
		while (_capacity < capacity) _capacity <<= 1;
		
		table = new Entry[_capacity];
	}
	
	/**
	 * 对于指定的 key,其相应的计数器自增一。此方法的副作用是会导致调用 {@link #getCount()} 的值加一。
	 * 
	 * @param key 需要自增一的计数器的键
	 * @return 计数器自增一前的值
	 */
	public long incrementAndGet(final long key) {
		try {
			tableRLock.lock();
			
			final int i = indexFor(hash(hash(key)), table.length);
			
			try {
				for (Entry ent = table[i]; ent != null; ent = ent.next) {
					if (ent.key == key) {
						return ent.incrementAndGet();
					}
				}
			} finally {
				tableRLock.unlock();
			}
			
			tableWLock.lock();
			try {
				table[i] = new Entry(key, 1, table[i]);
				return 0;
			} finally {
				tableWLock.unlock();
			}
		} finally {
			count.incrementAndGet();
		}
	}
	
	/**
	 * 获取与指定的 key 关联的计数器的当前值。
	 * 
	 * @param key 要获取的计数器的键
	 * @return 与指定的 key 关联的计数器的当前值
	 */
	public long get(final long key) {
		tableRLock.lock();
		try {
			final int i = indexFor(hash(hash(key)), table.length);
			
			for (Entry ent = table[i]; ent != null; ent = ent.next) {
				if (ent.key == key) return ent.getCount();
			}
			
			return 0;
		} finally {
			tableRLock.unlock();
		}
	}
	
	/**
	 * 获取所有计数器自增次数,即 {@link #incrementAndGet(long)} 的调用次数。
	 * 此值会在 {@link #reset()} 方法调用后重置为 0。
	 * 
	 * @return 所有计数器自增次数
	 */
	public int getCount() {
		return count.get();
	}
	
	/**
	 * 将当前计数器哈希 Map 重置。所有的计数器及与其关联的键都被销毁。实例恢复到构造函数刚被调用后的状态。
	 * 方法返回此实例被重置前的数据,以 {@link Map} 的形式给出。返回的 {@code Map} 不会受到本实例
	 * 后续操作的影响,在不加锁的情况下可以安全使用。
	 * <p>
	 * 注意此方法会将整个实例锁住,任何计数器自增操作和读取操作都会阻塞。
	 * 
	 * @return 此实例被重置前的数据,以 {@link Map} 的形式给出
	 */
	public Map<Long, Long> dumpAndClear() {
		tableWLock.lock();
		try {
			final Map<Long, Long> ret = new HashMap<Long, Long>();
			
			for (int i = 0; i < table.length; i++) {
				final Entry ent = table[i];
				if (ent != null) {
					ret.put(ent.getKey(), ent.getCount());
					table[i] = null;
				}
			}
			
			return ret;
		} finally {
			tableWLock.unlock();
			count.set(0);
		}
	}
	
	private final Entry[] table;
	private final ReadWriteLock tableLock = new ReentrantReadWriteLock();
	private final Lock tableRLock = tableLock.readLock();
	private final Lock tableWLock = tableLock.writeLock();
	private final AtomicInteger count = new AtomicInteger(0);
	
	private static int hash(final long key) { return (int)(key ^ (key >>> 32)); }
	
	private static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
	
	private static int indexFor(final int hash, final int length) { return hash & (length - 1); }
	
	private static final class Entry {
		
		public Entry(final long key, final long count, @Nullable final Entry next) {
			this.key = key;
			this.count.set(count);
			this.next = next;
		}
		
		public long getKey() { return key; }
		
		public long getCount() { return count.get(); }
		
		public long incrementAndGet() { return count.incrementAndGet(); }
		
		@Override
		public String toString() {
			return String.format("%d=%d", key, count);
		}

		final long key;
		final AtomicLong count = new AtomicLong(0L);
		@Nullable final Entry next;
		
	}

}

分享到:
评论

相关推荐

    关于asp技术的性能计数器实例

    **性能计数器的分类** 性能计数器类别广泛,包括但不限于: 1. **系统类别**:如Processor(CPU)、Memory(内存)等。 2. **网络类别**:如Network Interface(网络接口)。 3. **IIS类别**:如ASP.NET、W3WP(IIS...

    asp.net 检索类别和计数器

    ASP.NET 是一个强大的Web应用程序开发框架,由微软公司推出,用于构建动态网站、Web应用程序和服务。在ASP.NET中,检索类别和计数器是两个重要的概念,尤其在性能监控和数据分析方面。 首先,我们来详细了解一下...

    基于mediapipe和KNN分类算法的健身计数器引体向上计数器深蹲计数器俯卧撑计数器python源码.zip

    该压缩包文件包含了一个基于Mediapipe框架和KNN(K-Nearest Neighbor)分类算法的健身计数器程序,用于自动计算引体向上、深蹲和俯卧撑等健身动作的次数。这个程序是用Python编程语言编写的,对于理解和应用机器学习...

    实验十九。十进制计数器CD4017.docx

    在计数器的分类中,CD4017属于同步型计数器,这意味着它内部所有的触发器均受一个公共时钟信号的控制。同步计数器的优势在于其所有输出与时钟脉冲的上升沿或下降沿同步进行。该计数器广泛应用于数字电路中,用于完成...

    数字电路课程课件:第6章_2 计数器.ppt

    状态转换表展示了计数器从一个状态到下一个状态的变化规则,同时,通过时序图可以直观地看到计数器在时钟脉冲作用下的动态行为。 74161是一个典型的4位同步二进制加法计数器集成电路,它支持异步清零、同步置数和...

    行业分类-设备装置-基于写命令偏置刷新区域计数器的磁盘驱动器.zip

    基于写命令偏置的刷新区域计数器是一种创新的策略,它通过监控写命令的频率和分布,动态调整磁盘驱动器的刷新策略。这种技术的核心在于,当检测到写命令的密集区时,计数器会增加该区域的刷新次数,以确保数据的稳定...

    行业分类-设备装置-一种模拟实时时钟的MCU的计数器的校验装置及其校验方法.zip

    在线校验则是在系统运行过程中持续进行,通过周期性地获取外部时间更新,动态调整RTC计数器,减少累积误差。 具体到该文件"一种模拟实时时钟的MCU的计数器的校验装置及其校验方法.pdf",可能详述了一种创新的校验...

    数字电路说课计数器PPT学习教案.pptx

    知识目标要求学生能理解计数器的功能和分类,掌握中规模集成计数器的应用原理。能力目标则侧重于培养学生的实践技能和团队合作精神,期望他们能在教师的引导下,学会自主探究并解决问题。而德育目标则是通过计数器的...

    基于单片机的计数器.doc

    ### 基于单片机的手动计数器设计与实现 #### 一、整体设计方案概述 本设计旨在开发一款基于AT89S51单片机的手动计数器,该计数器能实现从00至999的手动计数功能,并通过数码管显示计数值。 ##### 2.1 设计方案...

    CSS黑魔法之计数器counter的使用技巧

    CSS计数器不仅仅局限于列表项的计数,还可以用于统计复选框的选中数量、生成连续编号的幻灯片、创建视频播放列表以及动态输出文档等场景。它的工作原理是通过自定义的计数器名称在页面的样式中进行计数,最后在需要...

    十进制计数器信号连续作用.ppt

    存储器的分类不仅限于其存放位置,更深入的是根据其构成的材料和介质。以存储介质为依据,存储器可以分为磁介质、半导体、光电和光盘存储器。每种存储器类型各有其独特的特性、优势和应用领域。磁介质存储器,如硬盘...

    实验六、MSI时序逻辑部件.pdf

    按计算体制分类,计数器可分成二进制和任意进制计数器,而十进制计数器是任意进制计数器中常用的计数器;按计数脉冲引入方式分类,计数器还可以分成同步和异步计数器。 二、寄存器 寄存器用于存储一组二值代码,它...

    实验六、MSI时序逻辑部件.docx

    按计算体制分类,计数器可分成二进制和任意进制计数器,而十进制计数器是任意进制计数器中常用的计数器;按计数脉冲引入方式分类,计数器还可以分成同步和异步计数器。 移位寄存器用于存储一组二值代码,它被广泛地...

    cd40110.pdf

    - **工作温度**:根据分类不同,分为M类(-55℃~125℃)和E类(-40℃~85℃),覆盖了广泛的环境温度条件。 #### 三、静态与动态特性 - **静态特性**包括输出电平电压(VOL和VOH)、输入电平电压(VIL和VIH)、...

    常用时序集成电路及其应用.pptx

    计数器可以根据进位方式、进位制、逻辑功能和集成度进行分类。 * 同步计数器:计数器的同步计数器可以根据时钟信号来同步计数。 * 异步计数器:异步计数器可以根据输入信号来异步计数。 * 模2计数器:模2计数器是指...

    海洋分类 MSSQL V5.1.1

    7. **性能优化**:通过SQL Server的动态管理视图和性能计数器,管理员可以监控系统性能,进行调优,确保高效运行。 8. **云整合**:考虑到云技术的发展,此版本可能支持Azure云平台,使得海洋数据可以在云端进行...

    ADC DAC的分类与指标简介

    5. **信噪比(SNR)和动态范围**,衡量ADC在噪声背景下能分辨的最小信号。 6. **输入失调电流**和**输入电流噪声**,影响输入信号的准确读取。 7. **功耗**,在满足性能需求的同时,低功耗设计对于便携式设备尤为...

    《ASP+HTML+Dreamweaver+Access开发动态网站实例荟萃》

    2. **计数器**:计数器是动态网站中常见的一种元素,用于统计网页访问次数。书中将演示如何使用ASP创建一个简单的页面访问计数器,并存储数据到Access数据库中。 3. **投票调查系统**:投票系统能够收集用户意见,...

    存储器分类

    6. BEDO DRAM(爆发式延伸数据输出动态随机存取存储器)是EDO的升级版,通过内部地址计数器实现突发式读取,提升了数据传输速率,但由于兼容性问题,很快被其他技术取代。 7. MDRAM(多插槽动态随机存取存储器)由...

Global site tag (gtag.js) - Google Analytics