【问题标题】:Why this synchronisation is not working in given scenario?为什么这种同步在给定的场景中不起作用?
【发布时间】:2020-02-07 12:24:58
【问题描述】:
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 中,它没有为 2 个对象提供相同的 hashCode,我将 Thread.sleep 和对象实例化都保留在同步块下,所以我在想的是,第二个线程甚至不应该进入这个块直到第一个完成,但它仍在做并创建导致 diff hashCode 的第二个对象。我在这里发什么信息?有人可以在这里纠正我的理解吗?如果我检查 null b4 对象的创建,那么它会按预期工作,但是为什么我需要在这里再次检查 null,因为我的整个代码都在同步块下?

if(null == singleton)
       singleton = new SingletonClass();

【问题讨论】:

  • 您将获得两个实例,因为您在同步块之外检查 null。将检查放入内部或使整个方法同步。
  • 使用双重锁定。

标签: java multithreading concurrency singleton


【解决方案1】:

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

  • 线程A进入函数,看到nullsingleton
  • 线程B进入函数,看到nullsingleton
  • 线程 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;
}

或者,如果您真的想在 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;
}

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

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

你能解释一下在第二个代码 sn-p 中的 null 检查之前对局部变量的赋值吗?直接检查类变量不行吗?

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

singleton 不是null 的情况下,使用本地表示该方法仅访问singleton一次。如果代码不使用本地代码,它将访问singleton 至少两次(一次检查,一次返回)。由于访问volatile 变量有点贵,最好使用本地(在上面的代码中可以优化成寄存器)。

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

【讨论】:

  • 您能解释一下在第二个代码 sn-p 中的 null 检查之前对局部变量的赋值吗?直接检查类变量不行吗?
  • @user2683814 - 好问题!我已将问题和答案添加到答案中。
  • 一般来说也是一个好习惯。当您检查一个条件并对其采取行动时,请确保该条件在执行时仍然有效。因此,当您读取volatile 变量时,要在非null 时返回值,请记住局部变量中的值,以确保它是您要返回的非null 值,而不是潜在的新值值,在第二次访问共享变量时读取。在这种特定情况下,变量不会再次返回到null,但是,如前所述,始终避免“check-then-act”模式是一个好习惯。
  • @Holger - 大时代。
【解决方案2】:

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

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

两个相关说明:

[1] java 内存模型指出,任何给定的字段都像薛定谔的猫:每个线程都有它的副本,或者没有 - 取决于线程模型。一个单独的副本在任意时间被发送到每个其他线程的副本,或者发送给其中的一些副本,从其他线程接收更新也是如此。你不能依赖这个机制,它甚至可能不会被使用,没有办法控制它(volatile 可以帮助但正确使用有点棘手)。关键是编写代码,使其无关紧要。一旦你在代码之间建立了'comes before' / 'comes after' 关系,例如因为你使用了一个同步块,这种任意性质就会消失,并且你可以保证可见性(所以如果代码 A 出现在代码 B 之前,例如因为它们都在同一个对象上同步并且 A“赢得”战斗,一旦 B 开始运行,A 在任何地方写入的任何内容都将对 B 可见,这是有保证的,因为这里存在 CA/CB 关系)。

把那个空检查放进去,问题突然就消失了。

[2] 如果您想要完成的只是只有一个 SingletonClass 实例,那么您就找错了树。这不是如何做到这一点。实际上很简单。你所做的就是这一行:

public class SingletonClass {
    public static final SingletonClass instance = new SingletonClass();

    private SingletonClass() {
        // ensure nobody but you can call this.
    }
}

就是这样。您可能认为这意味着该类在您的应用程序启动时被初始化,但事实并非如此。仅当运行使用该类的某些代码时才加载类。假设 SingletonClass 的所有使用都涉及获取该单例实例(通常是真的),这和任何事情一样好。如果由于某些奇怪的原因代码可能与 SC 交互而不获取单例,您仍然可以使用这种机制,只是使用内部类:

public class SingletonClass {
    private SingletonClass() {}

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

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

这保证在有人调用 getInstance() 之前不会调用该构造函数,只调用一次,不能调用两次,并且以最有效的方式执行。

编辑:格式化。

【讨论】:

  • 没有线程安全机制的共享变量比薛定谔的猫还要糟糕。毕竟,薛定谔的猫在你感知到它已经死去之后就不会再活过来了……换句话说,“每个线程都有它的副本,或者没有”过于简单化了.每个单独的读取操作可能以不同的及时性结束。线程可能会读取最新值,然后在后续读取中使用过时值。这就是为什么问题的方法 4 可能碰巧有效,但仍然不正确。
【解决方案3】:

所以问题是两个线程都可以同时到达方法,因此在第一个进入同步块之前,两个线程都会立即将对象设为 null。

使用@rzwitserloot 和@T.J. Crowder cmets,我得出的结论是,没有必要使用 synchronized 来创建 SIngleton 对象。 下面的代码可以做到这一点,它也在用线程测试 n ju​​nits 进行测试

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();
    }
}

所有 4 个线程的哈希码相同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-04
    • 1970-01-01
    • 2011-01-23
    • 1970-01-01
    • 1970-01-01
    • 2021-12-06
    • 1970-01-01
    相关资源
    最近更新 更多