为什么此同步在给定场景中不起作用?

Arc*_*aye 1 java concurrency singleton multithreading

package singleton;

public class SingletonClass {

    private static SingletonClass singleton = null;

    private SingletonClass() {
    }

    static boolean stopThread = true;

    //approach 1 which fails in multithereaded env
    /*public static SingletonClass getInstance(){
        if(null == singleton){
            try {
                if(stopThread){
                    stopThread = false;
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singleton = new SingletonClass();
        }
        return singleton;
    }*/

    //approach 2 which works
    //method is synchronized
   /* public static synchronized SingletonClass getInstance(){
        if(null == singleton){
                try {
                    if(stopThread){
                        stopThread = false;
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                singleton = new SingletonClass();

        }
        return singleton;
    }*/

    ***//approach 3 which is failing but I don't understand why
   //big block of code is synchronized
    public static SingletonClass getInstance(){
        if(null == singleton){
            synchronized (SingletonClass.class){
                try {
                    if(stopThread){
                        stopThread = false;
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                singleton = new SingletonClass();
            }
        }
        return singleton;
    }***


    //small block of code is synchronized, checked null again because even object instantiation is synchronised
    //if we don't check null, it will create new object once again
    //approach 4 which works
   /* public static SingletonClass getInstance(){
        if(null == singleton){
                try {
                    if(stopThread){
                        System.out.println("in thread...");
                        stopThread = false;
               //even if we interchange above 2 lines it makes whole lot of difference
               //till the time it takes to print "in thread"
               //2nd thread reaches there n enters if(stopThread) block because
               //stopThread is still true because 1st thread spent time in writing that sentence and 
               //did not set stopThread = false by the time 2nd thread reached there
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (SingletonClass.class){
                    System.out.println("in this block");
                    if(null == singleton){
                        singleton = new SingletonClass();
                    }
                }
        }
        return singleton;
    }*/

}


---------------------------------------------------------

package singleton;

public class ThreadUsage implements Runnable {

    @Override
    public void run() {
        SingletonClass singletonOne = SingletonClass.getInstance();
        System.out.println(singletonOne.hashCode());
    }
}

----------------------------------------------------------------

package singleton;

class ThreadUsageTest {

    public static void main(String[] args) {
        Runnable runnableOne = new ThreadUsage();
        Runnable runnableTwo = new ThreadUsage();
        new Thread(runnableOne).start();
        new Thread(runnableTwo).start();
    }
}
---------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

在方法3中,它没有为2个对象提供相同的hashCode,我将Thread.sleep以及对象实例化都保留在同步块下,所以我的想法是,第二个线程甚至不应该进入这个块,直到第一个完成为止,但是它仍在执行并创建第二个对象,导致 diff hashCode。我在这里发什么消息?有人可以在这里纠正我的理解吗?如果我检查 null b4 对象创建,那么它会按预期工作,但为什么我需要在这里再次检查 null,因为我的整个代码都在同步块下?

if(null == singleton)
       singleton = new SingletonClass();
Run Code Online (Sandbox Code Playgroud)

T.J*_*der 6

以下是代码(方法 3)最终为单例创建并返回两个(或更多)单独对象的一种方法:

  • 线程A进入函数并null查看singleton
  • 线程B进入该函数并null查看singleton
  • 线程A进入同步块
  • 线程B因为无法进入同步块而等待
  • 线程A分配给singleton
  • 线程A退出同步块
  • 线程A返回一个对象
  • 线程B进入同步块
  • 线程B分配给singleton
  • 线程B返回一个不同的对象

null例如,检查和输入其后的同步块之间存在间隙。

要解决这个问题,只需创建getInstance一个synchronized方法并删除synchronized其中的块即可:

public static synchronized SingletonClass getInstance() {
    if (instance == null) {
            singleton = new SingletonClass();
    }
    return singleton;
}
Run Code Online (Sandbox Code Playgroud)

或者,如果您确实想避免在后续调用中同步,请在 Java 5 或更高版本(希望您正在使用!)上声明singleton volatile并在synchronized块中再次检查:

private static volatile SingletonClass singleton;
// ...
public static SingletonClass getInstance() { // Only works reliably on Java 5 (aka 1.5) and later!
    SingletonClass instance = singleton;
    if (instance == null) {
        synchronized (SingletonClass.class) {
            instance = singleton;
            if (instance == null) {
                singleton = instance = new SingletonClass();
            }
        }
    }
    return instance;
}
Run Code Online (Sandbox Code Playgroud)

这就是双重检查锁定习惯用法。在 Java 4(又名 1.4)及更早版本中,这不一定可靠,但现在是可靠的(前提是您volatile在成员上使用)。

user2683814 在评论中提出了一个很好的问题:

您能否解释一下第二个代码片段中空检查之前对局部变量的赋值?直接检查类变量行不通?

是的,它会起作用,但效率较低。

singleton在非的情况下null,使用本地意味着该方法仅访问singleton 一次。如果代码不使用本地变量,它将singleton至少访问两次(一次检查它,一次返回它)。由于访问volatile变量的成本稍高,因此最好使用本地变量(在上面的代码中可以将其优化为寄存器)。

这可能看起来像是过早的微优化,但如果您不是在性能关键的代码中执行此操作,您只需创建该方法synchronized并完全避免双重检查锁定的复杂性。:-)

  • 一般来说,这也是一个好习惯。当您检查某个条件并对其采取行动时,请确保该条件在采取行动时仍然有效。因此,当您读取“易失性”变量时,要在非“null”时返回该值,请记住局部变量中的值,以确保您返回的是非常非“null”的值,而不是潜在的新值value,第二次访问共享变量时读取。在这种特定场景中,变量不会再次返回到“null”,但是,正如所说,始终避免“检查然后执行”模式是一个好习惯。 (3认同)