多线程环境中的Java Singleton类行为

时间:2015-03-31 11:19:26

标签: java multithreading

我已经阅读过很多关于此问题的stackoverflow问题,但措辞不同而且没有回答我正在寻找的问题。我不希望任何代码解决方案,但在多线程环境中对单例对象的行为解释。带有一些解释链接的答案会很棒。

问题1: 如果有多个线程需要访问singleton对象(比如在spring config中配置为singleton bean),那么只有一个线程能够访问而其他线程阻塞直到当前保持线程释放它

对我来说,阻塞在这里是有道理的,因为只有一个对象,并且所有线程不能同时获得相同的对象。

因此,如果我将DAO配置为单例bean,那么如果多个用户(线程)尝试执行读取并通过该DAO 进行写入,那么它实际上并不是同时发生但是顺序 y - - >数据库密集型应用程序中的性能问题。

另一方面,大多数DAO都是无状态的(至少在我的情况下),所以不是将其配置为Singleton,我可以在任何需要的地方实例化它并执行并发操作,但每个对象实例化可能需要一些时间和内存。所以这是一个设计决定。

在我的情况下,由于DAO没有状态变量,因此内存可以忽略不计,所以如果我的上述理论是正确的,那么我会选择第二个选项,即选择内存并发。

问题2:对于提出的问题here

我认为最好的答案在下面是S.Lott(我不知道如何指向链接 直接回答所以复制答案):

  

单身人士解决了一个(也是唯一一个)问题。

     

资源争用。

     

如果你有一些资源

     

(1)只能有一个实例,而

     

(2)您需要管理该单个实例

     

你需要一个单身人士。

这里我的问题是如果上面的答案是真的 - >这就是将记录器实现为单例的原因,因为您有一个日志文件(单个资源)。

如果一个线程已将某些日志的一部分写入文件然后暂停,然后线程2再次写入其部分然后再写入线程1,那么由于时间切片,您不希望这样做,从而使日志文件乱码。

我在文本上面划线,因为我认为如果一个线程正在记录,那么由于singleton配置,第二个线程根本无法记录(阻止),因此避免使日志文件变成乱码。

我的理解是对的吗?

5 个答案:

答案 0 :(得分:2)

"单例"在Java编程语言中没有任何意义。它只是一种设计模式。许多线程共享的单个对象的行为与许多线程共享的任何其他对象的行为没有区别。

如果安全,只有当 时,才能确保安全。

答案 1 :(得分:1)

您正在连接单例模式和多线程应用程序,假设必须序列化对单例对象的访问;这不是真的。

要求是单例必须是线程安全的。

考虑你问题中的DAO示例;假设每次调用都是无状态的(即你不在类中共享变量,但只是在方法中),你不需要同一个DAO的多个实例,在Spring应用程序中通常会有一个或多个管理器类(通常是你)然后使用AOP管理这些管理器类的DB事务;他们每个人都有一个DAO的引用。每次调用DAO对象时,它都会从数据源获取数据库连接并执行所需的操作,然后释放与数据源的连接。

当多个线程调用您的经理类时,您需要以线程安全的方式获取/释放数据库连接。通常Spring会隐藏这种复杂性,您无需担心这一点。

dao的伪代码类似于

public void doSomeDBOperation(YourObject param) {
    Connection connection=acquireDBConnection();//the connection must be acquired in a thread safe way
    SQLStatement statement=connection.createStatement(yourSQL);
    //do the operation with your param;
    releaseDBConnection(connection);
}

Spring在AOP方面获得了一个Db连接,它有点类似,它保存在线程局部变量中,因此它可以被多个DAO使用,你可以通过连接来管理事务。

因此,一个管理器,一个dao和多个数据库连接是一种并行管理多线程操作的方法(假设您使用的是连接池,只序列化DBconnection租用/发布)并在不阻塞的情况下进行数据库操作(在java级别)在数据库级别,操作可能会锁定,以保证完整性约束。)

关于记录器问题,我假设您正在引用大多数日志库的使用方式,即在每个类中声明一个静态记录器:

public class MyClass {
    private static final Logger logger=LoggerFactory.get(MyClass.class.getName());
    .....
}

记录器类和您记录的文件没有直接链接,使用Log4J,LogBack等您可以通过一次日志调用登录到多个文件,甚至可以记录到非文件的内容(例如套接字) 。真正的写操作由appender完成,appender必须是线程安全的,并在需要时序列化对底层资源的访问。在您的应用程序中,您声明了多个记录器(每个类一个),因此每个类只有一个单例,而不是您应用程序中的一个记录器类。

不同的线程可以在paralled中调用相同的logger,但是underlyng appender保证在需要时序列化对underlyng文件(o to something else)的访问(如果你有一个appender发送邮件而不是写一个文件你可以同时做到。)

答案 2 :(得分:1)

你的报价是正确但不完整的。单身人士有更多的目的,希望以后能够明白。

第一个问题的答案是,它取决于bean及其实现方式。 Spring实例化的实例不保证线程安全。为了回答这个问题,假设有两种可能的实现类型:有状态和无状态。如果存在任何需要可变外部资源(例如,字段变量,数据库访问,文件访问)的方法,则可以认为实现有状态。另一方面,如果所有方法都能够仅使用其参数和不可变外部资源来执行其任务,则实现是无状态的。

class Stateful {
    private String s; // shared resource
    void setS(String s) {
        this.s = s;
    }
}

class Stateless {
    int print(String s) {
        return s.size();
    }
}

现在,如果实现是无状态的,那么它通常是单例的有意义。因为无状态类本质上是线程安全的。多个线程可以同时使用这些方法(不阻塞)。仅仅因为一个类是无状态的并不意味着它消耗的内存可以忽略不计。在Java中,实例化一个类被认为是一项昂贵的操作。

有状态实现需要做更多工作才能成为单身人士。再次为了回答这个问题,假设有两类实现:阻塞和非阻塞。阻塞如上所述(尽管存在各种类型的阻塞)。非阻塞意味着多个线程可以同时调用这些方法。通常,有一个专门用于非阻塞实现进行处理的线程。

记录器是非阻塞实现的很好的例子。下面是一个高度简化的代码。

class Logger {
    private List<String> msgs = new CopyOnWriteArrayList<>();
    void log(String msg) {
        msgs.add(msg);
    }
    private void process() {...} // used internally by a thread specially for Logger
}

关于问题2,基本答案是否定的;单身人士并非都是坏人。设计图案是双刃剑。明智地使用它们并改进代码;严重地使用它们会产生比他们解决的问题更多的问题。

答案 3 :(得分:0)

如果你想结合单线程和单个日志文件没有单线实现的单个日志文件,单例只会影响一个线程,以避免另一个线程弹出。

现在如果你通过使用多个线程打破这一点,单身人士不会帮助你。如果他们这样做,第二个线程将无法记录。

答案 4 :(得分:0)

无论@james说什么都是对的。提出你的问题:

问题1:除非您使get方法返回同步对象,否则多个线程可以无阻塞地访问singleton对象。请参阅下面的内容(多个线程可以获取对singleton对象的引用而不会被阻止)。

public class Singleton {
    private Singleton() {
    }

    private static class SingletonHelper {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

问题2:您的理解不正确。如果一个线程正在写入,另一个线程必须等待,这不是因为单例配置,而是因为可能存在对象(文件)可能最终处于不一致状态;同样适用于未正确同步的任何对象。