Java: counting/profiling 同步调用

Java: counting/profiling synchronization calls

我正在寻找一种方法来列出 运行 并行 java 应用程序的所有同步调用,以便检测可伸缩性问题(根据 threads/cores)。据我了解,每次输入同步块时,机器都需要同步缓存。这会影响所有 CPU 运行(以多种方式,如内存带宽),即使 运行 任务未因进入同步区域而被阻塞。

设置:

我有一个在更高级别并行化的大型应用程序,即它具有并行执行的复杂任务。并行化在术语中起作用,即所有内核都处于负载状态并且我没有阻塞线程。性能仍然没有随内核扩展,这可能有几个原因。我感兴趣的特定可能原因是是否有很多同步调用(例如,进入同步块、使用锁等)。

任务

我想找出在我的代码中(实际执行的)哪些地方有这样的同步调用,以及每个同步实际执行的频率。有很多引用的库,因此不可能只对同步关键字或类似的东西使用常规代码搜索,因为这会搜索很多从未执行过的代码并带来很多误报。完美的解决方案是拥有一个分析器,它列出所有已执行的同步位置和调用次数。但是,我尝试过的分析器只允许对方法调用进行计数。因此,这里的问题是找到所有实际相关的方法。

或者,如果我能找到一些入口点(主方法)引用的同步位置,它也会有所帮助。 IE。通过递归地遍历代码并检查所有引用的方法、类 等以实现此类同步。在这种情况下,稍后可以使用常规分析器找出频率。

问题

是否有工具或工作流程能够为更大的项目归档上述任务。

提前感谢您的回答。

您可以使用 CPU 分析器来执行此操作。如果你有一个需要很长时间才能获得锁的同步方法,它看起来会花费很长时间。如果时间不长的话,可以不用管。

现在,如果一个方法耗时很长,可能会分不清是不是同步锁。如果你真的不能告诉 运行 阅读代码,你可以将实现移动到一个私有方法,而 public 方法所做的只是获取锁。这将使延迟是在获取锁还是运行宁代码时变得更加清楚。

使用分析器的另一个原因;当你猜测问题可能是什么时,它几乎从来都不是你最初想到的,即使你已经对程序进行性能调优 Java 十年,你首先想到的可能是前 5 名或前 10 名但很少是你遇到的最大问题。

这是一个 java 应用程序,因此您可以在 jdk1.8.XX.XX\bin[=13 中使用 jdk 工具=].使用 visualVM 或 jmc(java 任务控制),您可以可视化线程、锁。 并且您可以在应用程序(log4j 或其他工具)中添加日志以计算执行时间。

除非在此块上存在争用,否则进入和离开同步块是相当便宜的操作。在无竞争的情况下 synchronized 只是一个原子 CAS,或者如果 UseBiasedLocking 优化成功则几乎是一个无操作。虽然看起来可以使用 Instrumentation API 进行同步分析器,但这没有多大意义。

多线程应用程序的问题是争用同步。 JVM 有一些内部计数器来监视锁争用 (see this question). Or you can even write a simple ad-hoc tool to track all contended locks using JVMTI events.

然而,不仅锁会引起争用。即使是非阻塞算法也会因共享资源的竞争而受到影响。 Here is a good example 此类可扩展性问题。所以,我同意@PeterLawrey 的观点,最好从 CPU 分析器开始,因为它通常更容易发现性能问题。

以下非常简单的示例表明,在单线程算法中,与单个监视器同步也会花费一些时间。它表明此示例中的同步 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();
        }
    }
}