单例类中的方法是线程安全的吗?

时间:2018-02-09 21:14:14

标签: java multithreading thread-safety

我正在为我们的应用程序设计一个自定义日志框架。

我正在阅读Patterns For Logging Diagnostic Messages

中使用的http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/NDC.html

在第3页上说:

  

因为它是Singleton,所以它很容易保留消息的顺序。

我认为,例如,给定Singleton类S,如果类B尝试获取S的实例而A类已经获得实例S,则B无法获取S的实例,因为A已经获得了实例的。

这就是为什么根据我的理解保留消息的顺序。

  1. 我的理解是否正确?

  2. B级如何知道A类是用S类完成的,不再需要它,以便B可以获得S?

  3. 如果我的理解是正确的,并且Singleton类S有一些方法:test1()和test2(),如下所示。

  4. test1()和test2()是否是线程安全的?

    这些方法将在类S之外调用,类似于

    S.getInstance().test1("message")

    例如,在A类或B类中

    这意味着当A类和B类试图通过调用test1()在日志文件中写入一些数据时,这些数据将按照获取S实例的顺序写入?

    如果没有,为了在Singleton类S中使方法成为线程安全的,我是否应该在方法test1()和test2()或synchronized上使用lock关键字来获取这些方法?

    public class S {                  // singleton class S
    
        private static S instance;
    
        private S(){}
    
        public static S getInstance(){
            if(instance == null){
                instance = new S();
            }
            return instance;
        }
    
        public static void test1(String log) {
           // writing some data to a log file
        }
    
        public static void test2(String log) {
           // writing some data to a log file
        }
    }
    

5 个答案:

答案 0 :(得分:1)

这绝对不是线程安全的。假设我有两个线程T1,T2和S有属性foo。假设T1和T2正在修改foo的值,然后使用foo的值来执行其他操作。

然后,我可能有T1访问S.getInstance,检查没有设置getInstance,同时,T2可以访问S.getInstance并看到实例未设置。 T1,然后可能设置实例,但由于T2同时也检测到实例未设置,也会为S设置实例。因此,S.instance的值实际上是一个由T2设定。换句话说,在T1和T2之间存在竞争条件,以查看谁可以首先设置S的实例。

为了使这个同步,你肯定应该同步getInstance方法,这样一次只能有一个线程对它起作用。此外,您应该创建S volatile实例,以确保访问S实例的任何线程始终使用“最新”副本。 (因为可能是一个线程在被修改时对该实例进行了一些其他的读操作。)

即。像这样的东西:

public class S {                  // singleton class S

    private volatile static S instance;

    private S(){}

    public synchronized static S getInstance(){
        if(instance == null){
            instance = new S();
        }
        return instance;
    }

    public static void test1(String log) {
       // writing some data to a log file
    }

    public static void test2(String log) {
       // writing some data to a log file
    }
}

此外,这里有一个很好的链接,说明为什么要使用volatile:

What is the point of making the singleton instance volatile while using double lock?

答案 1 :(得分:0)

示例中的代码不是线程安全的。如果两个并发线程试图同时获取单例对象,则每个线程都有可能创建一个对象。您应该在synchronized方法上使用getInstance关键字来避免此行为。

您还必须在访问对象数据的单例类(非静态属性)上将每个实例方法(非静态)标记为synchronized。这样,两种不同的方法永远不会同时运行,从而防止数据混乱。

在您的示例中,只要他们从实例调用的方法都是synchronized,您就不需要在test1test2上使用synchronized

您的S课程可能看起来像这样

public class S {

    private static S instance;

    private S() {}

    public synchronized static S getInstance() {
        if (instance == null) {
            instance = new S();
        }
        return instance;
    }

    public synchronized doSomething() {
        // Code that uses data from instance
    }

    public static void test1(String log) {
        // ...
        S.getInstance().doSomthing();
        // ...
    }

    public static void test2(String log) {
        // ...
        S.getInstance().doSomthing();
        // ...
    }
}

答案 2 :(得分:0)

首先,上面的代码示例不是安全的单例。静态方法getInstance可能存在竞争条件。如果两个或多个线程同时运行if(instance==null),则将构造多个 S 实例。要通过提供类实例的静态最终字段来更正此问题,请使用 eager-initialization

实施例: -

public class S{

     private static final S INSTANCE = new S();

     private S(){}

     public static S getInstance(){
         return INSTANCE;
     }

     public void doSomething(){
         //the rest of the code
     }
}

自JDK 5以来更好,使用 enum 类型来表示安全的单例。它还免费提供防止序列化攻击的保护。

实施例: -

public enum Singleton {

    S;

    public void doSomething(){/*...*/}
}

其次,如果doSomething不修改 S 实例状态或者它是无状态对象,那么它是线程安全的。否则必须提供同步保护以在多线程环境中保持其状态正确性。

注意:过去,许多天真的双锁定法实现解决了懒惰加载单例而没有同步的问题充满了危险。请参阅以下由Doug Lee,Joshua Bloch& amp; amp;等。进一步阅读。

The "Double-Checked Locking is Broken" Declaration

答案 3 :(得分:-1)

回答你的问题:

  1. 不,你的理解不正确。
  2. 使用某种锁定机制,例如Semaphore
  3. 这完全取决于这些方法中的内容。如果它们只使用方法本地的变量(即堆栈中而不在堆中的变量),则该方法可能是线程安全的。如果需要访问类的本地变量(例如日志文件),则需要一个锁定机制。
  4. 只是要添加,您不能使用静态getInstance方法并期望它是线程安全的。完成此任务的最佳方法是使代码如下:

    private final static S INSTANCE = new S();
    

    只有在您第一次访问它时才会实例化。

答案 4 :(得分:-2)

这不是线程安全的。想象一下两个竞争线程试图获取S的实例,并且如果实例为空则同时检查。为了保护线程安全,您需要使用synchronized关键字。试试这个:

public class S {                  // singleton class S

    private static S instance;

    private S(){}

    public static S getInstance(){
        if(instance == null){
            synchronized (S.class)
            {
                if(instance == null)
                {
                    instance = new S();
                }
            }

        }
        return instance;
    }

    public static void test1(String log) {
        // writing some data to a log file
    }

    public static void test2(String log) {
        // writing some data to a log file
    }
}

另一个选项是,为了避免上面的双重检查锁定,可以像这样同步getInstance方法(尽管使用并增加了性能命中):

public static synchronized S getInstance()
    {
        if (instance == null)
        {
            instance = new S();
        }

        return instance;
    }