在多线程环境中使用 Singleton 类的首选方法是什么?
假设我有 3 个线程,并且它们都尝试同时访问getInstance()
单例类的方法 -
- 如果不保持同步会发生什么?
synchronized
getInstance()
使用method 还是使用synchronized
block inside是一种好习惯getInstance()
。
请告知是否有其他出路。
在多线程环境中使用 Singleton 类的首选方法是什么?
假设我有 3 个线程,并且它们都尝试同时访问getInstance()
单例类的方法 -
synchronized
getInstance()
使用method 还是使用synchronized
block inside是一种好习惯getInstance()
。请告知是否有其他出路。
如果你在谈论单例的线程安全、延迟初始化,这里有一个很酷的代码模式,它可以在没有任何同步代码的情况下完成 100% 线程安全的延迟初始化:
public class MySingleton {
private static class MyWrapper {
static MySingleton INSTANCE = new MySingleton();
}
private MySingleton () {}
public static MySingleton getInstance() {
return MyWrapper.INSTANCE;
}
}
这将仅在被调用时实例化单例getInstance()
,并且它是 100% 线程安全的!这是一个经典。
它之所以起作用,是因为类加载器有自己的同步来处理类的静态初始化:保证在使用类之前所有静态初始化都已完成,并且在此代码中,类仅在getInstance()
方法内使用,所以当类加载内部类。
顺便说一句,我期待着有一个@Singleton
注释可以处理这些问题的那一天。
一个特定的不信者声称包装类“什么都不做”。这是证明它确实重要的证据,尽管在特殊情况下。
基本区别在于,对于包装类版本,单例实例是在加载包装类时创建的,这是在第一次调用时创建的getInstance()
,但是对于非包装版本 - 即简单的静态初始化 - 创建实例加载主类时。
如果您只有简单的getInstance()
方法调用,那么几乎没有区别 - 区别在于使用包装版本时,所有其他sttic 初始化将在创建实例之前完成,但这很容易通过简单地处理源中最后列出的静态实例变量。
但是,如果您按name加载类,情况就完全不同了。调用Class.forName(className)
一个类会导致静态初始化发生,因此如果要使用的单例类是您服务器的属性,那么在简单版本中,静态实例将在调用时创建Class.forName()
,而不是在getInstance()
调用时创建。我承认这有点做作,因为您需要使用反射来获取实例,但是这里有一些完整的工作代码可以证明我的观点(以下每个类都是顶级类):
public abstract class BaseSingleton {
private long createdAt = System.currentTimeMillis();
public String toString() {
return getClass().getSimpleName() + " was created " + (System.currentTimeMillis() - createdAt) + " ms ago";
}
}
public class EagerSingleton extends BaseSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
public class LazySingleton extends BaseSingleton {
private static class Loader {
static final LazySingleton INSTANCE = new LazySingleton();
}
public static LazySingleton getInstance() {
return Loader.INSTANCE;
}
}
主要的:
public static void main(String[] args) throws Exception {
// Load the class - assume the name comes from a system property etc
Class<? extends BaseSingleton> lazyClazz = (Class<? extends BaseSingleton>) Class.forName("com.mypackage.LazySingleton");
Class<? extends BaseSingleton> eagerClazz = (Class<? extends BaseSingleton>) Class.forName("com.mypackage.EagerSingleton");
Thread.sleep(1000); // Introduce some delay between loading class and calling getInstance()
// Invoke the getInstace method on the class
BaseSingleton lazySingleton = (BaseSingleton) lazyClazz.getMethod("getInstance").invoke(lazyClazz);
BaseSingleton eagerSingleton = (BaseSingleton) eagerClazz.getMethod("getInstance").invoke(eagerClazz);
System.out.println(lazySingleton);
System.out.println(eagerSingleton);
}
输出:
LazySingleton was created 0 ms ago
EagerSingleton was created 1001 ms ago
如您所见,未包装的简单实现是在Class.forName()
调用时创建的,这可能是在静态初始化准备好执行之前。
鉴于您想让它真正线程安全,这个任务在理论上并不简单。
在@IBM找到了一篇关于这个问题的非常好的论文
只是获得单例不需要任何同步,因为它只是一个读取。因此,只需同步 Sync 的设置即可。除非两个线程尝试在启动时同时创建单例,否则您需要确保检查实例是否设置了两次(一个在同步外部,一个在同步内部)以避免在最坏的情况下重置实例。
然后您可能需要考虑 JIT(即时)编译器如何处理乱序写入。此代码将在某种程度上接近解决方案,尽管无论如何都不会是 100% 线程安全的:
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
Singleton inst = instance;
if (inst == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
}
}
return instance;
}
所以,你也许应该诉诸一些不那么懒惰的东西:
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
或者,有点臃肿,但更灵活的方法是避免使用静态单例并使用诸如Spring 之类的注入框架来管理“单例”对象的实例化(并且您可以配置延迟初始化)。
getInstance
只有当你懒惰地初始化你的单例时,你才需要内部同步。如果您可以在线程启动之前创建一个实例,您可以在 getter 中删除同步,因为引用变得不可变。当然,如果单例对象本身是可变的,则需要同步其访问可以同时更改的信息的方法。
这个问题实际上取决于创建实例的方式和时间。如果您的getInstance
方法延迟初始化:
if(instance == null){
instance = new Instance();
}
return instance
然后你必须同步,否则你可能会得到多个实例。这个问题通常在关于Double Checked Locking的讨论中得到处理。
否则,如果您预先创建一个静态实例
private static Instance INSTANCE = new Instance();
那么不需要同步该getInstance()
方法。
有效java中描述的最佳方法是:
public class Singelton {
private static final Singelton singleObject = new Singelton();
public Singelton getInstance(){
return singleObject;
}
}
不需要同步。
没有人按照Effective Java中的建议使用 Enums吗?
如果您确定您的 java 运行时使用的是新的 JMM(Java 内存模型,可能比 5.0 更新),那么仔细检查锁就可以了,但是在实例前面添加一个 volatile。否则,您最好使用 Bohemian 所说的静态内部类,或 Florian Salihovic 所说的“Effective Java”中的 Enum。
答案已经在这里被接受,但我想分享测试来回答你的第一个问题。
如果不保持同步会发生什么?
SingletonTest
当您在多线程环境中运行时,这将是完全灾难的类。
/**
* @author MILAN
*/
public class SingletonTest
{
private static final int PROCESSOR_COUNT = Runtime.getRuntime().availableProcessors();
private static final Thread[] THREADS = new Thread[PROCESSOR_COUNT];
private static int instancesCount = 0;
private static SingletonTest instance = null;
/**
* private constructor to prevent Creation of Object from Outside of the
* This class.
*/
private SingletonTest()
{
}
/**
* return the instance only if it does not exist
*/
public static SingletonTest getInstance()
{
if (instance == null)
{
instancesCount++;
instance = new SingletonTest();
}
return instance;
}
/**
* reset instancesCount and instance.
*/
private static void reset()
{
instancesCount = 0;
instance = null;
}
/**
* validate system to run the test
*/
private static void validate()
{
if (SingletonTest.PROCESSOR_COUNT < 2)
{
System.out.print("PROCESSOR_COUNT Must be >= 2 to Run the test.");
System.exit(0);
}
}
public static void main(String... args)
{
validate();
System.out.printf("Summary :: PROCESSOR_COUNT %s, Running Test with %s of Threads. %n", PROCESSOR_COUNT, PROCESSOR_COUNT);
long currentMili = System.currentTimeMillis();
int testCount = 0;
do
{
reset();
for (int i = 0; i < PROCESSOR_COUNT; i++)
THREADS[i] = new Thread(SingletonTest::getInstance);
for (int i = 0; i < PROCESSOR_COUNT; i++)
THREADS[i].start();
for (int i = 0; i < PROCESSOR_COUNT; i++)
try
{
THREADS[i].join();
}
catch (InterruptedException e)
{
e.printStackTrace();
Thread.currentThread().interrupt();
}
testCount++;
}
while (instancesCount <= 1 && testCount < Integer.MAX_VALUE);
System.out.printf("Singleton Pattern is broken after %d try. %nNumber of instances count is %d. %nTest duration %dms", testCount, instancesCount, System.currentTimeMillis() - currentMili);
}
}
程序的输出清楚地表明您需要使用 getInstance 作为synchronized
或添加synchronized
锁来封闭新的 SingletonTest。
摘要 :: PROCESSOR_COUNT 32,使用 32 个线程运行测试。 单例模式在 133 次尝试后被破坏。 实例数为 30。 测试持续时间 500ms
为简单起见,我认为使用枚举类是一种更好的方法。我们不需要做任何同步。Java 通过构造,始终确保只创建一个常量,无论有多少线程试图访问它。
仅供参考,在某些情况下,您需要将单例替换为其他实现。然后我们需要修改类,这违反了 Open Close 原则。单例的问题是,由于具有私有构造函数,您无法扩展该类。因此,客户端通过接口进行交谈是一种更好的做法。
使用枚举类和接口实现单例:
客户端.java
public class Client{
public static void main(String args[]){
SingletonIface instance = EnumSingleton.INSTANCE;
instance.operationOnInstance("1");
}
}
SingletonIface.java
public interface SingletonIface {
public void operationOnInstance(String newState);
}
EnumSingleton.java
public enum EnumSingleton implements SingletonIface{
INSTANCE;
@Override
public void operationOnInstance(String newState) {
System.out.println("I am Enum based Singleton");
}
}