现在 java 双重锁是不是可以不加volatile了?

2022-09-20 最新发现

(话说,segmentfault的问题沉得好快啊……我折腾这么久反而越来越没人看了,好烦)
我费心费力搞来了机器码(jdk1.8)来看(这次使用了别的测试代码),还是没有看到指令重排的情况,重复执行好几遍了,还是重现不了,代码原地址是这个。下面是我测试得到的截图和代码
输出:
图片.png
对应代码段:
对比两图可知没有重排.png

完整代码:

public class DCL2 {

    public static void main(String[] args) {

        for (int i = 0; i < 1_000_000; i++) {
            final State state = new State();

            // a = 0, b = 0, c = 0

            // Write values
            new Thread(() -> {
                state.a = 1;
                // a = 1, b = 0, c = 0
                state.b = 1;
                // a = 1, b = 1, c = 0
                state.c = state.a + 1;
                // a = 1, b = 1, c = 2
            }).start();

            // Read values - this should never happen, right?
            new Thread(() -> {
                // copy in reverse order so if we see some invalid state we know this is caused by reordering and not by a race condition in reads/writes
                // we don't know if the reordered statements are the writes or reads (we will se it is writes later)
                int tmpC = state.c;
                int tmpB = state.b;
                int tmpA = state.a;

                if (tmpB == 1 && tmpA == 0) {
                    System.out.println("Hey wtf!! b == 1 && a == 0");
                }
                if (tmpC == 2 && tmpB == 0) {
                    System.out.println("Hey wtf!! c == 2 && b == 0");
                }
                if (tmpC == 2 && tmpA == 0) {
                    System.out.println("Hey wtf!! c == 2 && a == 0");
                }
            }).start();

        }
        System.out.println("done");
    }

    static class State {
        int a = 0;
        int b = 0;
        int c = 0;
    }

}

我觉得……我已经不知怎么证明指令重排会导致双重锁有问题了,指令重排的触发是不是有什么条件、而双重锁校验并不会触发它?

我这边打起精神,接着看原始论文的测试代码,关键代码段再放出来一遍,源码要么往下多翻翻要么看论文,不然太长:
图片.png

图片.png

看看它的机器码是什么情况,如下图所示:
图片.png

图中标红的两块是论文里说会出问题的地方,他们论文里的截图是这样的:

图片.png

意思是说,在初始化完成之前,就开始给singletons[i]赋值了,但我这边没发现这问题……


2022-09-15 最新发现

我在虚拟机里安装了java1.4 去跑,依然没有遇到报错,顶多就是参数过大导致了OOM问题。

所以,DCL检查有漏洞的问题到底其实怎样才能重现?我看所有人只是在分析指令重排和内存模型,并没有人可以给出重现错误的代码,如果说恰好没遇到,我好歹也运行了十几遍了,各种大小参数都试过,还是没能重现,这个测试代码是提出这个问题的原始论文里的测试代码,我不清楚怎么调出内存里的执行情况,因此DCL到底是个什么问题我还是希望能整个明白。


原问题

我看了以前别人的文章,说指令重排和多线程访问时有可能使双重锁检查失效,但是我自己跑代码去测试并没有发现这类问题,我甚至将里面测试代码的线程数和单例数调到几十万都没能让它报错。

因此,是不是现在的java可以放心使用双重锁模式了?是什么原因导致这个问题被修复了?什么时候被修复的?烦请各位大佬解答下。

我上面其实都有链接,但我还是代码再贴一遍吧。按上面链接的说法checkSingleton方法应该会打印一些错误信息。我的代码里已经去掉了多余的进度日志,方便观看。

package com.concurrency;

/**
 * @author : 
 * @date : 2022-09-14 14:18
 **/
public class DoubleCheckTest
{


    // static data to aid in creating N singletons
    static final Object dummyObject = new Object(); // for reference init
    static final int A_VALUE = 256; // value to initialize 'a' to
    static final int B_VALUE = 512; // value to initialize 'b' to
    static final int C_VALUE = 1024;
    static ObjectHolder[] singletons;  // array of static references
    static Thread[] threads; // array of racing threads
    static int threadCount; // number of threads to create
    static int singletonCount; // number of singletons to create


    static volatile int recentSingleton;


    // I am going to set a couple of threads racing,
    // trying to create N singletons. Basically the
    // race is to initialize a single array of
    // singleton references. The threads will use
    // double checked locking to control who
    // initializes what. Any thread that does not
    // initialize a particular singleton will check
    // to see if it sees a partially initialized view.
    // To keep from getting accidental synchronization,
    // each singleton is stored in an ObjectHolder
    // and the ObjectHolder is used for
    // synchronization. In the end the structure
    // is not exactly a singleton, but should be a
    // close enough approximation.
    //


    // This class contains data and simulates a
    // singleton. The static reference is stored in
    // a static array in DoubleCheckFail.
    static class Singleton
    {
        public int a;
        public int b;
        public int c;
        public Object dummy;

        public Singleton()
        {
            a = A_VALUE;
            b = B_VALUE;
            c = C_VALUE;
            dummy = dummyObject;
        }
    }

    static void checkSingleton(Singleton s, int index)
    {
        int s_a = s.a;
        int s_b = s.b;
        int s_c = s.c;
        Object s_d = s.dummy;
        if(s_a != A_VALUE)
            System.out.println("[" + index + "] Singleton.a not initialized " +
                    s_a);
        if(s_b != B_VALUE)
            System.out.println("[" + index
                    + "] Singleton.b not intialized " + s_b);

        if(s_c != C_VALUE)
            System.out.println("[" + index
                    + "] Singleton.c not intialized " + s_c);

        if(s_d != dummyObject)
            if(s_d == null)
                System.out.println("[" + index
                        + "] Singleton.dummy not initialized,"
                        + " value is null");
            else
                System.out.println("[" + index
                        + "] Singleton.dummy not initialized,"
                        + " value is garbage");
    }

    // Holder used for synchronization of
    // singleton initialization.
    static class ObjectHolder
    {
        public Singleton reference;
    }

    static class TestThread implements Runnable
    {
        public void run()
        {
            for(int i = 0; i < singletonCount; ++i)
            {
                ObjectHolder o = singletons[i];
                if(o.reference == null)
                {
                    synchronized(o)
                    {
                        if (o.reference == null) {
                            o.reference = new Singleton();
                            recentSingleton = i;
                        }
                        // shouldn't have to check singelton here
                        // mutex should provide consistent view
                    }
                }
                else {
                    checkSingleton(o.reference, i);
                    int j = recentSingleton-1;
                    if (j > i) i = j;
                }
            }
        }
    }

    public static void main(String[] args)
    {
        if( args.length != 2 )
        {
            System.err.println("usage: java DoubleCheckFail" +
                    " <numThreads> <numSingletons>");
        }
        // read values from args
//        threadCount = Integer.parseInt(args[0]);
//        singletonCount = Integer.parseInt(args[1]);
        threadCount = Integer.parseInt("30");
        singletonCount = Integer.parseInt("3000000");

        // create arrays
        threads = new Thread[threadCount];
        singletons = new ObjectHolder[singletonCount];

        // fill singleton array
        for(int i = 0; i < singletonCount; ++i)
            singletons[i] = new ObjectHolder();

        // fill thread array
        for(int i = 0; i < threadCount; ++i)
            threads[i] = new Thread( new TestThread() );

        // start threads
        for(int i = 0; i < threadCount; ++i)
            threads[i].start();

        // wait for threads to finish
        for(int i = 0; i < threadCount; ++i)
        {
            try
            {
//                System.out.println("waiting to join " + i);
                threads[i].join();
            }
            catch(InterruptedException ex)
            {
                System.out.println("interrupted");
            }
        }
        System.out.println("done");
    }
}
阅读 1k
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏