JSP 点击量统计
application 作用域计数、并发安全、持久化与真实统计方案。
application 作用域
<%@ page import="java.util.concurrent.atomic.AtomicLong" %>
<%
AtomicLong counter = (AtomicLong) application.getAttribute("hits");
if (counter == null) {
synchronized (application) {
counter = (AtomicLong) application.getAttribute("hits");
if (counter == null) {
counter = new AtomicLong();
application.setAttribute("hits", counter);
}
}
}
long n = counter.incrementAndGet();
%>
<p>本站访问次数:<%= n %></p>本站访问次数:1024The double-checked lock matters: when several threads see null, only the first creates, and the rest re-check inside synchronized and reuse it. Without the inner check you get several counters.
为什么必须用 AtomicLong
<%-- 错误:读和写之间有窗口,并发下会丢计数 --%>
<%
Integer n = (Integer) application.getAttribute("hits");
application.setAttribute("hits", n == null ? 1 : n + 1);
%>
<%-- 正确 --%>
<% ((AtomicLong) application.getAttribute("hits")).incrementAndGet(); %>Two threads read 100, each writes 101: two visits, one increment. incrementAndGet() is a single atomic CPU instruction with no such window.
按会话计数
<%
Integer mine = (Integer) session.getAttribute("myHits");
mine = (mine == null) ? 1 : mine + 1;
session.setAttribute("myHits", mine);
%>
<p>您第 <%= mine %> 次访问本页</p>Requests within one session are essentially serial, so a plain Integer is fine. Only cross-user sharing needs concurrency control.
重启不丢
public class CounterListener implements ServletContextListener {
private static final Path FILE =
Path.of(System.getProperty("java.io.tmpdir"), "hits.txt");
@Override
public void contextInitialized(ServletContextEvent e) {
long init = 0;
try {
if (Files.exists(FILE)) init = Long.parseLong(Files.readString(FILE).trim());
} catch (Exception ignore) { }
e.getServletContext().setAttribute("hits", new AtomicLong(init));
}
@Override
public void contextDestroyed(ServletContextEvent e) {
AtomicLong c = (AtomicLong) e.getServletContext().getAttribute("hits");
try {
Files.writeString(FILE, String.valueOf(c.get()));
} catch (IOException ignore) { }
}
}contextDestroyed is not guaranteed; kill -9 skips it. Reliable counting writes asynchronously to a database on each increment rather than flushing at shutdown.真实场景别自己数
Real PV/UV analytics belong in Nginx log analysis or a dedicated service: no app memory, restart-proof, time-dimensional, and able to separate bots from humans. A hand-rolled counter is for teaching only.
application 作用域
<%@ page import="java.util.concurrent.atomic.AtomicLong" %>
<%
AtomicLong counter = (AtomicLong) application.getAttribute("hits");
if (counter == null) {
synchronized (application) {
counter = (AtomicLong) application.getAttribute("hits");
if (counter == null) {
counter = new AtomicLong();
application.setAttribute("hits", counter);
}
}
}
long n = counter.incrementAndGet();
%>
<p>本站访问次数:<%= n %></p>本站访问次数:1024这里用了双重检查加锁:多个线程同时发现 null 时,只有第一个会创建,其他线程在 synchronized 里再检查一次拿到同一个实例。少了内层检查就会创建出多个计数器。
为什么必须用 AtomicLong
<%-- 错误:读和写之间有窗口,并发下会丢计数 --%>
<%
Integer n = (Integer) application.getAttribute("hits");
application.setAttribute("hits", n == null ? 1 : n + 1);
%>
<%-- 正确 --%>
<% ((AtomicLong) application.getAttribute("hits")).incrementAndGet(); %>两个线程同时读到 100,各自加一写回 101,实际访问了两次却只加了一次。AtomicLong.incrementAndGet() 是一条 CPU 原子指令,不存在这个窗口。
按会话计数
<%
Integer mine = (Integer) session.getAttribute("myHits");
mine = (mine == null) ? 1 : mine + 1;
session.setAttribute("myHits", mine);
%>
<p>您第 <%= mine %> 次访问本页</p>同一个会话的请求基本是串行的,普通 Integer 就够,不需要原子类型。跨用户共享才要并发控制。
重启不丢
public class CounterListener implements ServletContextListener {
private static final Path FILE =
Path.of(System.getProperty("java.io.tmpdir"), "hits.txt");
@Override
public void contextInitialized(ServletContextEvent e) {
long init = 0;
try {
if (Files.exists(FILE)) init = Long.parseLong(Files.readString(FILE).trim());
} catch (Exception ignore) { }
e.getServletContext().setAttribute("hits", new AtomicLong(init));
}
@Override
public void contextDestroyed(ServletContextEvent e) {
AtomicLong c = (AtomicLong) e.getServletContext().getAttribute("hits");
try {
Files.writeString(FILE, String.valueOf(c.get()));
} catch (IOException ignore) { }
}
}contextDestroyed 不保证被调用——kill -9 时进程直接消失。真正可靠的计数应该每次自增后异步写库,而不是只在关闭时刷盘。真实场景别自己数
PV/UV 统计交给 Nginx 日志分析或专业统计服务:不占应用内存、不怕重启、能按时间维度聚合、还能区分爬虫与真人。自己写的计数器只适合教学演示。