JSP 点击量统计

application 作用域计数、并发安全、持久化与真实统计方案。

Intermediate
Key Points全站计数放 application 作用域,用 AtomicLong 保证原子性;要重启不丢就得落库。

application 作用域

JSP
<%@ 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>
Output
本站访问次数:1024

The 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

JSP
<%-- 错误:读和写之间有窗口,并发下会丢计数 --%>
<%
    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.

按会话计数

JSP
<%
    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.

重启不丢

Java
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.

Friends & Partners