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

Java 为什么这种同步在给定场景中不起作用?,java,multithreading,concurrency,singleton,Java,Multithreading,Concurrency,Singleton,在方法3中,它没有为两个对象提供相同的hashCode,我将Thread.sleep和对象实例化都保存在同步块下,所以我想,第二个线程甚至不应该在第一个完成之前进入这个块,但它仍然在做并创建第二个对象,导致diff hashCode。我在这里说什么?有人能纠正我的理解吗?如果我检查空b4对象创建,那么它的工作正如预期,但为什么我需要再次检查空,因为我的整个代码都在同步块下 package singleton; public class SingletonClass { private

在方法3中,它没有为两个对象提供相同的hashCode,我将Thread.sleep和对象实例化都保存在同步块下,所以我想,第二个线程甚至不应该在第一个完成之前进入这个块,但它仍然在做并创建第二个对象,导致diff hashCode。我在这里说什么?有人能纠正我的理解吗?如果我检查空b4对象创建,那么它的工作正如预期,但为什么我需要再次检查空,因为我的整个代码都在同步块下

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();
    }
}
---------------------------------------------------------------------------
以下是代码(方法3)最终为单例创建并返回两个(或更多)独立对象的一种方式:

  • 线程A进入函数并看到
    singleton的
    null
  • 线程B进入函数并看到
    singleton的
    null
  • 线程A进入同步块
  • 线程B等待,因为它无法进入同步块
  • 线程A分配给
    singleton
  • 线程A退出同步块
  • 线程A返回一个对象
  • 线程B进入同步块
  • 线程B分配给
    singleton
  • 线程B返回一个不同的对象
例如,
null
检查与输入其后面的同步块之间存在间隙

要解决此问题,只需将
getInstance
设置为
synchronized
方法,并删除其中的
synchronized
块:

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

public static synchronized SingletonClass getInstance() {
    if (instance == null) {
            singleton = new SingletonClass();
    }
    return singleton;
}
这就是双重检查锁定习惯用法。在Java4(又名1.4)和更早的版本中,这并不一定可靠,但现在已经可靠了(前提是您在成员上使用
volatile

在评论中,用户2683814提出了一个很好的问题:


在第二个代码段中的null检查之前,您能解释一下对局部变量的赋值吗?直接检查类变量不起作用

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

如果
singleton
为非
null
,则使用本地方式,该方法仅访问
singleton
一次。如果代码没有使用本地,它将访问
单例
至少两次
(一次检查,一次返回)。由于访问一个
volatile
变量稍微昂贵,因此最好使用local(上面代码中的local可以优化为寄存器)

这看起来像是过早的微优化,但如果您不是在性能关键型代码中这样做的,您只需将方法
同步化
,并完全避免双重检查锁定的复杂性。:-)

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

  • 线程A进入函数并看到
    singleton的
    null
  • 线程B进入函数并看到
    singleton的
    null
  • 线程A进入同步块
  • 线程B等待,因为它无法进入同步块
  • 线程A分配给
    singleton
  • 线程A退出同步块
  • 线程A返回一个对象
  • 线程B进入同步块
  • 线程B分配给
    singleton
  • 线程B返回一个不同的对象
例如,
null
检查与输入其后面的同步块之间存在间隙

要解决此问题,只需将
getInstance
设置为
synchronized
方法,并删除其中的
synchronized
块:

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

public static synchronized SingletonClass getInstance() {
    if (instance == null) {
            singleton = new SingletonClass();
    }
    return singleton;
}
这就是双重检查锁定习惯用法。在Java4(又名1.4)和更早的版本中,这并不一定可靠,但现在已经可靠了(前提是您在成员上使用
volatile

在评论中,用户2683814提出了一个很好的问题:


在第二个代码段中的null检查之前,您能解释一下对局部变量的赋值吗?直接检查类变量不起作用

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

如果
singleton
为非
null
,则使用本地方式,该方法仅访问
singleton
一次。如果代码没有使用本地,它将访问
单例
至少两次
(一次检查,一次返回)。由于访问一个
volatile
变量稍微昂贵,因此最好使用local(上面代码中的local可以优化为寄存器)


这看起来像是过早的微优化,但如果您不是在性能关键型代码中这样做的,您只需将方法
同步化
,并完全避免双重检查锁定的复杂性。:-)

在方法三中,检查
singleton
变量;您在任何同步块的外部执行此操作,这就是它不工作的原因:这里不能保证线程在检查之前等待。它们都以最快的速度进行检查,这就是为什么2+个线程都可能在这里看到null,即使其中一个线程已经在创建该实例

然后同步,当然。然而,这并不能神奇地赋予此代码“仅分配singleton一次”的权力——毕竟,该singleton块中的代码将把新创建的
SingletonClass
实例分配给
singleton
变量

<
public class SingletonClass {
    private SingletonClass() {}

    public static SingletonClass getInstance() {
        return Inner.instance;
    }

    private static class Instance {
        private static final SingletonClass instance = new SingletonClass();
    }
}
package singleton;

public class SingletonClassSecond {

    private static SingletonClassSecond singleton = new SingletonClassSecond();

    private SingletonClassSecond() {
    }

    public static SingletonClassSecond getInstance(){
        return singleton;
    }
}

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

package singleton;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class SingletonClassTest {

    @Test
    public void shouldCreateSingleton(){
        SingletonClass singletonOne = SingletonClass.getInstance();
        SingletonClass singletonTwo = SingletonClass.getInstance();
        singletonOne.print("1");
        singletonTwo.print("2");
        Assertions.assertEquals(singletonOne.hashCode(),singletonTwo.hashCode());
    }

}

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

package singleton;

class ThreadUsageTest {
    public static void main(String[] args) {
        Runnable runnable = new ThreadUsageSecond();
        Runnable runnableTwo = new ThreadUsageSecond();
        Runnable runnableThree = new ThreadUsageSecond();
        Runnable runnableFour = new ThreadUsageSecond();
        new Thread(runnable).start();
        new Thread(runnableTwo).start();
        new Thread(runnableThree).start();
        new Thread(runnableFour).start();
    }
}