在单例类上获取 Java 锁,了解原因

Ber*_*own 1 java performance

假设我们在 servlet 容器环境 java/j2ee 中有此代码。它是一个基本的网络应用程序。我们有监控工具提到此类在请求线程执行期间获得了大部分锁定。这仅仅是因为这里的单例方法吗?如果有锁,这段代码是否会遇到竞争条件?导致大量执行时间?锁位于单例类上。

public class SomeSingleton {

  private static final Thing object = new Thing();

    public static SomeSingleton instance = null;    

    private final Properties logfiles = new Properties();

    public static SomeSingleton getInstance() {
        if (instance == null) {
            createInstance();
        }
        return instance;
    }

 
    /**
     * Imagine this method called
     * SomeSingleton.getInstance().log()
     */
    public void log(final String message) {
        try {
            synchronized (logfiles) {
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

小服务程序:

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 
      SomeSingleton.getInstance().log()
  }
Run Code Online (Sandbox Code Playgroud)

Bas*_*que 5

太长了;博士

\n
    \n
  • 将您的单例定义为enum.
  • \n
  • 如果您的代码涉及阻塞(如果您的代码不受 CPU 限制),请使用虚拟线程。\n
      \n
    • ReentrantLock使用长时间运行(阻塞)的代码保护线程不安全的资源。仅在不阻塞的简短代码周围使用synchronized,例如检查内存中的防护。
    • \n
    \n
  • \n
\n

只有一把锁,但不是真正的单例

\n
\n

在单例类上获取 Java 锁,了解原因

\n
\n

“锁”不是复数,而是单数。您的代码中只有一个锁,从这一行开始:“synchronized (logfiles) {”。

\n
\n

锁位于单例类上。

\n
\n

如果这就是您所说的“单例类”的意思,那么锁不在的类上。SomeSingleton锁位于对象Properties中持有的对象上logfiles

\n
\n

SomeSingleton

\n
\n

正如Eggen 的 Answer中所解释的,您的类实际上并不是单例。

\n

您有一个竞争条件,其中多个线程可能正在访问您的方法并在您完成将实例分配给 之前getInstance读取 a 。在运行时,您的代码实际上可以实例化多个对象。nullSomeSingleton instanceSomeSingleton

\n

另外,如果您打算通过 进行访问,则SomeSingleton instance应该是private而不是。publicgetInstance

\n

修改后,使用私有构造函数

\n

您需要修改该代码。我将对其进行更改,以通过对象引用提供单个实例public final,并将构造函数设为私有以确保不会发生无意的实例化。

\n

改变这个:

\n
public class SomeSingleton {\n\n  private static final Thing object = new Thing();\n\n    public static SomeSingleton instance = null;    \n\n    private final Properties logfiles = new Properties();\n\n    public static SomeSingleton getInstance() {\n        if (instance == null) {\n            createInstance();\n        }\n        return instance;\n    }\n\n \n    /**\n     * Imagine this method called\n     * SomeSingleton.getInstance().log()\n     */\n    public void log(final String message) {\n        try {\n            synchronized (logfiles) {\n            }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 改为以下内容。

\n

我们标记instance为是public因为这是我们指定的调用方法的访问路径。

\n

我们标记instancefinal以防止重新分配给另一个对象。

\n

为了清晰起见,我们将变量重新static组织在一起。

\n

我们添加一个构造函数,使其private避免不受控制的实例化。

\n

我们初始化logfiles作为风格问题,像我这样的一些人希望在一个地方看到该对象的所有初始化,而不是分散在声明行中。理性的人可能会不同意。

\n

此代码instance在静态声明中实例化\xe2\x80\xa6 instance = new SomeSingleton();。我们不再需要你的getInstance方法了。该实例化在类加载时发生。该instance字段已标记final以防止分配任何其他实例。

\n
public class SomeSingleton \n{\n    // Statics.\n    public static final SomeSingleton instance = new SomeSingleton();\n    private static final Thing thing = new Thing();  \n\n    // Member fields.\n    private final Properties logfiles;  // To enable logging.\n    \n    // Private constructor.\n    private SomeSingleton() \n    {\n        this.logfiles = new Properties() ;\n    }\n\n    /**\n     * Perform logging by calling:\n     * SomeSingleton.instance.log( \xe2\x80\xa6 )\n     */\n    public void log( final String message ) \n    {\n        synchronized ( this.logfiles ) \n        {\n            \xe2\x80\xa6  \n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

ReentrantLock而不是synchronized虚拟线程

\n

为了与Java 21 +中的虚拟线程兼容,我们替换为synchronizedReentrantLock对象替换。

\n

仅当 (a) 正在执行的工作不完全受CPU 限制(涉及阻塞)并且 (b) 需要一段时间才能执行时才执行此操作。如果短暂的话,只需使用synchronized. 固定虚拟线程暂时不是问题。

\n
public class SomeSingleton \n{\n    // Statics.\n    public static final SomeSingleton instance = new SomeSingleton();\n    private static final Thing thing = new Thing();  \n\n    // Member fields.\n    private final Properties logfiles;  // To enable logging.\n    private final Lock loggingLock;  // To protect logging.\n    \n    // Private constructor.\n    private SomeSingleton() \n    {\n        this.logfiles = new Properties() ;\n        this.loggingLock = new ReentrantLock();\n    }\n\n    /**\n     * Perform logging by calling:\n     * SomeSingleton.instance.log( \xe2\x80\xa6 )\n     */\n    public void log( final String message ) \n    {\n        this.loggingLock.lock();  // Blocks until lock becomes available.\n        try \n        {\n            \xe2\x80\xa6  // Involves blocking, such as I/O.\n        } \n        finally \n        {\n            this.loggingLock.unlock();\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

单例作为enum

\n

但即使这样也并不理想。单例模式是一件非常棘手的事情!

\n

当前的观点是,Java 中的单例通常最好实现为enum. 有关更多信息,请参阅:

\n\n

枚举是隐式的staticand final,无需添加这些修饰符。

\n

在转换为枚举时,我们可以删除这一行:public static final SomeSingleton instance = new SomeSingleton();。当类加载时,命名的枚举对象会填充(我们的单例的实例化),就像我们static上面提到的字段一样。

\n
public enum SomeSingleton \n{\n    // enum\n    INSTANCE ;\n\n    // Statics.\n    private static final Thing thing = new Thing();  \n\n    // Member fields.\n    private final Properties logfiles;  // To enable logging.\n    private final Lock loggingLock;  // To protect logging.\n    \n    // Private constructor.\n    private SomeSingleton() \n    {\n        this.logfiles = new Properties() ;\n        this.loggingLock = new ReentrantLock();\n    }\n\n    /**\n     * Perform logging by calling:\n     * SomeSingleton.INSTANCE.log( \xe2\x80\xa6 )\n     */\n    public void log( final String message ) \n    {\n        this.loggingLock.lock();  // Blocks until lock becomes available.\n        try \n        {\n            \xe2\x80\xa6  // Involves blocking, such as I/O.\n        } \n        finally \n        {\n            this.loggingLock.unlock();\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n