Java:计算/分析同步调用

5
我正在寻找一种方法来列出运行中的并行Java应用程序的所有同步调用,以便检测可伸缩性问题(以线程/核心为衡量标准)。据我所知,每次进入同步块时,机器都需要同步缓存。这会影响所有正在运行的CPU(以多种方式,如内存带宽),即使运行任务没有被进入同步区域所阻塞。
设置:
我有一个大型应用程序,其并行化在更高级别上进行,即执行并行的复杂任务。并行化工作在术语上,即所有核心都处于负载状态,而且没有阻塞的线程。尽管如此,性能与核心不成比例,这可能有几个原因。我感兴趣的特定可能原因是是否存在大量同步调用(例如进入同步块,使用锁等)。
任务:
我想找出代码中(实际执行的)有此同步调用的地方,并确定每个同步操作实际上执行了多少次。由于有许多引用库,因此无法仅对同步关键字或类似关键字进行常规代码搜索,因为这将搜索许多从未执行的代码并带来许多错误结果。完美的解决方案是拥有一个分析器,它列出了所有已执行的同步位置和调用次数。然而,我尝试过的分析器只允许计算方法调用次数。因此,问题在于找到所有实际相关的方法。
另外,如果我可以找到由某个入口点(主方法)引用的同步位置,也将有所帮助。即通过递归遍历代码并检查所有引用的方法,类等是否具有这种同步功能。在这种情况下,以后可以使用常规分析器找出频率。
问题:
是否有工具或工作流程能够针对更大的项目完成上述任务?感谢您提前回答。
4个回答

4
您可以使用CPU分析器来实现这一点。如果您有一个同步方法需要花费很长时间才能获取锁定,那么它看起来就会花费很长时间。如果它不需要花费很长时间,那么您就不必担心它了。 现在,如果一个方法需要花费很长时间,可能并不清楚它是否是由于同步锁造成的。如果您真的无法通过代码来判断,请将实现移动到一个私有方法中,所有公共方法都只需获取锁定。这将使得延迟是否在获取锁定或运行代码方面更加清晰明确。 使用分析器的另一个原因是,当您猜测问题所在时,它几乎从未是您最初想到的,即使您已经对Java程序进行性能调优十年了,您最初想到的可能是前五名或前十名,但很少是您遇到的最大问题。

这里的问题是,没有显示任何阻塞线程,而且线程总是有工作要做,但性能仍然无法扩展。因此,我认为锁定可能会导致一些额外的内存流量,也会影响其他内存访问。 - Till Schäfer
@TillSchäfer 你说得对。CPU密集型进程扩展性差的常见原因是L3缓存争用。它比L1慢10-20倍,是一个共享资源。如果你有10个核心,所有访问中有1%接触到L3缓存,那么内存访问的平均时间会增加一倍。即99% * 1 + 1% * 10 * (10 to 20)。一旦L3缓存被充分利用,添加更多线程将不会有太大帮助,反而可能会使情况变得更糟。 - Peter Lawrey
@TillSchäfer 我们做的是最小化线程之间共享的数据量,并尽可能减少垃圾,因为这可以提高 L1/L2 缓存的效率,减少对 L3 的访问。简而言之,进行内存分析并降低垃圾产生率。 - Peter Lawrey

3
进入和离开同步块是一项非常便宜的操作,除非该块存在争用。在无争用的情况下,synchronized只是一个原子CAS或者几乎是一个空操作,如果UseBiasedLocking优化成功的话。虽然使用Instrumentation API可以编写同步分析器,但这并没有太多意义。
对于多线程应用程序而言,问题在于争用同步。JVM有一些内部计数器来监视锁争用(请参阅此问题)。或者你甚至可以编写一个简单的特定工具,使用JVMTI事件跟踪所有有争议的锁。
然而,不仅锁可以造成争用。即使非阻塞算法也可能因为竞争共享资源而受到影响。这里有一个很好的例子说明了这种可扩展性问题。因此,我同意@PeterLawrey的观点,最好从CPU分析器开始,因为通常更容易找到性能问题。

你的意思是,当一个线程被另一个线程阻塞时,就会出现争用,对吗?非常感谢你的建议,它们真的很有帮助。 - Till Schäfer
你认为进入同步块会对内存带宽问题产生影响吗?如果内存是瓶颈,缓存同步可能会增加一些额外的流量,或者我错了吗? - Till Schäfer
@TillSchäfer 不,synchronized块并不是那么难。我怀疑它可能不是内存流量的根本问题。没有所谓的“缓存同步”事情,而是一个缓存一致性协议和相关的内存屏障。它们确实会消耗一些资源,但并不太多,并且它们处理特定的缓存行,而不是整个内存。 - apangin
我认为这个答案是错误的。例如,将BufferedOutputStream / BufferedInputStream替换为非同步版本已经将我们的代码速度提高了4倍。当然,这些实例仅从单个线程中使用。 - Horcrux7
@Horcrux7 如果你有反例,请分享基准代码,这样我就可以回复或解释为什么会发生这种情况。我做了一个演示,展示了不同的基准测试(即使是由好工具制作)也可能导致完全相反的结果。关于“同步”的性能也是如此。演示文稿不是用英语写的,但是这里有一些幻灯片,以便让你了解可能出现的问题。https://drive.google.com/open?id=0B7LUxTf0AV-fbkJKVUszVVFKakU - apangin
@apangin 请查看单独回答中的示例代码 https://dev59.com/RJHea4cB1Zd3GeqPli3l#40546105 - Horcrux7

1

这是一个Java应用程序,因此您可以在 jdk1.8.XX.XX\bin 中使用JDK工具。使用visualVM或jmc(Java任务控制)可以可视化线程和锁定。还可以在应用程序中添加日志(log4j或其他工具)以计算执行时间。


0

下面这个非常简单的示例展示了即使在单线程算法中,使用单个监视器进行同步也会耗费一些时间。它表明,在此示例中,使用同步的BufferedOutputStream大约慢了1/4。它将100 MB流式传输到nop流。更复杂的代码可能会导致更多的性能降低。

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class BenchmarkTest {

    public static void main( String[] args ) throws IOException {
        while( true ) {
//            testNop();
            testSync();
            testNoSync();
        }
    }

    static void testNop() throws IOException {
        BenchmarkTest test = new BenchmarkTest();
        test.out = new OutputStream() {
            @Override
            public void write( int b ) throws IOException {
                // nop
            }
        };
        test.run( "            nop OutputStream" );
    }

    static void testSync() throws IOException {
        BenchmarkTest test = new BenchmarkTest();
        test.out = new BufferedOutputStream( new OutputStream() {
            @Override
            public void write( int b ) throws IOException {
                // nop
            }
        }, 32768 );
        test.run( "   sync BufferedOutputStream" );
    }

    static void testNoSync() throws IOException {
        BenchmarkTest test = new BenchmarkTest();
        test.out = new FastBufferedOutputStream( new OutputStream() {
            @Override
            public void write( int b ) throws IOException {
                // nop
            }
        }, 32768 );
        test.run( "no sync BufferedOutputStream" );
    }

    private OutputStream out;

    void run( String testName ) throws IOException {
        long time = System.currentTimeMillis();
        for( int i = 0; i < 100_000_000; i++ ) {
            out.write( i );
        }
        System.out.println( testName + " time: " + (System.currentTimeMillis() - time) );
    }

    static public class FastBufferedOutputStream extends OutputStream {
        private byte[]       buf;

        private int          count;

        private OutputStream out;

        /**
         * Creates a BufferedOutputStream without synchronized.
         *
         * @param out the underlying output stream.
         */
        public FastBufferedOutputStream( OutputStream out ) {
            this( out, 8192 );
        }

        /**
         * Creates a BufferedOutputStream without synchronized.
         *
         * @param out the underlying output stream.
         * @param size the buffer size.
         * @exception IllegalArgumentException if size &lt;= 0.
         */
        public FastBufferedOutputStream( OutputStream out, int size ) {
            this.out = out;
            this.buf = new byte[size];
        }

        /**
         * Flush the internal buffer
         * 
         * @throws IOException if any I/O error occur
         */
        private void flushBuffer() throws IOException {
            if( count > 0 ) {
                out.write( buf, 0, count );
                count = 0;
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void write( int b ) throws IOException {
            if( count >= buf.length ) {
                flushBuffer();
            }
            buf[count++] = (byte)b;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void write( byte[] b, int off, int len ) throws IOException {
            if( len >= buf.length ) {
                /* If the request length exceeds the size of the output buffer,
                   flush the output buffer and then write the data directly.
                   In this way buffered streams will cascade harmlessly. */
                flushBuffer();
                out.write( b, off, len );
                return;
            }
            if( len > buf.length - count ) {
                flushBuffer();
            }
            System.arraycopy( b, off, buf, count, len );
            count += len;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void flush() throws IOException {
            flushBuffer();
            out.flush();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void close() throws IOException {
            flushBuffer();
            out.close();
        }
    }
}

不幸的是,这个例子涉及到了许多常见的基准测试陷阱。请参考这个问题。可以修改此代码,以便syncnoSync实现显示相同的时间。 - apangin
@apangin 您的示例与实际代码无关。1.)永远不会在单线程代码中添加同步。2.)大多数情况下,您无法访问代码深处的所有同步对象。例如,如果我在 BufferedOutputStream 周围添加 ZipOutputStream 或 DataOutputStream。3.)您的更改仅对 64 位 VM 产生影响。在 32 位 VM 中没有这样的效果。仍然存在 1/4 的差异,这非常神秘。 - Horcrux7
即使没有外部synchronized,差异也大约为1.25倍。而不是你之前声称的4倍。考虑到你的基准测试也与实际用例无关(即它只测量同步而没有有用的工作),这仍然意味着未经竞争的同步调用相当便宜。 - apangin
@apangin 我已经写过,这个测试只显示1/4或0.25的差异。因子4在我们的实际应用程序中存在。这是一个Web服务器应用程序。我认为问题在于同时运行多个任务。每个任务是单线程的,但一个线程的同步调用也会减慢其他任务。效果随着线程数的增加而增加。频繁计算同步的另一个负面影响可能是GC。我的测试忽略了这一点。我不喜欢简化的基准测试。我使用真实的应用程序进行测试。有太多的影响。 - Horcrux7
我不想对我从未见过的应用程序的性能进行猜测。有时锁定可以使代码运行得更快!但是所有这些示例和基准测试数字如果没有适当的分析,就没有任何实际意义。 - apangin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接