Luồng Java thực thi thao tác còn lại trong một vòng lặp chặn tất cả các luồng khác


123

Đoạn mã sau đây thực thi hai luồng, một là một bộ đếm thời gian đơn giản ghi nhật ký mỗi giây, thứ hai là một vòng lặp vô hạn thực hiện một thao tác còn lại:

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

Điều này cho kết quả như sau:

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

Tôi không hiểu tại sao tác vụ vô hạn chặn tất cả các luồng khác trong 13,3 giây. Tôi đã cố gắng thay đổi các ưu tiên của luồng và các cài đặt khác, không có gì hoạt động.

Nếu bạn có bất kỳ đề xuất nào để khắc phục điều này (bao gồm điều chỉnh cài đặt chuyển đổi ngữ cảnh của hệ điều hành), vui lòng cho tôi biết.


8
@Marthin Không phải là GC. Đó là JIT. Chạy với -XX:+PrintCompilationtôi nhận được những điều sau tại thời điểm trì hoãn kéo dài kết thúc: TestBlockingThread :: lambda $ 0 @ 2 (24 byte) MÁY TÍNH BỎ: Vòng lặp vô hạn tầm thường (thử lại ở các tầng khác nhau)
Andreas

4
Nó sao chép trên hệ thống của tôi với thay đổi duy nhất là tôi đã thay thế cuộc gọi nhật ký bằng System.out.println. Có vẻ như là một vấn đề về lịch trình bởi vì nếu bạn giới thiệu một giấc ngủ 1ms bên trong vòng lặp trong khi Runnable (true) thì tạm dừng trong luồng khác sẽ biến mất.
JJF

3
Không phải tôi khuyên bạn nên dùng nó, nhưng nếu bạn vô hiệu hóa JIT -Djava.compiler=NONE, điều đó sẽ không xảy ra.
Andreas

3
Bạn có thể vô hiệu hóa JIT cho một phương thức duy nhất. Xem Vô hiệu hóa Java JIT cho một phương thức / lớp cụ thể?
Andreas

3
Không có phân chia số nguyên trong mã này. Vui lòng sửa tiêu đề và câu hỏi của bạn.
Hầu tước Lorne

Câu trả lời:


94

Sau tất cả các giải thích ở đây (nhờ Peter Lawrey ), chúng tôi thấy rằng nguồn chính của sự tạm dừng này là sự an toàn bên trong vòng lặp được đưa ra khá hiếm nên phải mất một thời gian dài để dừng tất cả các luồng để thay thế mã do JIT biên dịch.

Nhưng tôi đã quyết định đi sâu hơn và tìm thấy lý do tại sao hiếm khi đạt được điểm an toàn. Tôi thấy có một chút khó hiểu tại sao bước nhảy lùi của whilevòng lặp không "an toàn" trong trường hợp này.

Vì vậy, tôi triệu tập -XX:+PrintAssemblytất cả vinh quang của nó để giúp đỡ

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

Sau một số điều tra, tôi thấy rằng sau lần biên dịch thứ ba của C2trình biên dịch lambda đã loại bỏ hoàn toàn các cuộc thăm dò ý kiến ​​trong vòng lặp.

CẬP NHẬT

Trong giai đoạn định hình, biến số ikhông bao giờ được nhìn thấy bằng 0. Đó là lý do tại sao C2suy đoán tối ưu hóa nhánh này đi, để vòng lặp được chuyển đổi thành một cái gì đó như

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

Lưu ý rằng vòng lặp vô hạn ban đầu được định hình lại thành vòng lặp hữu hạn thông thường với bộ đếm! Do tối ưu hóa JIT để loại bỏ các cuộc thăm dò an toàn trong các vòng được tính hữu hạn, nên cũng không có cuộc thăm dò ý kiến ​​an toàn nào trong vòng lặp này.

Sau một thời gian, ibọc lại 0, và cái bẫy không phổ biến đã được thực hiện. Phương pháp này đã được tối ưu hóa và tiếp tục thực hiện trong trình thông dịch. Trong quá trình biên dịch lại với một kiến ​​thức mới đã C2nhận ra vòng lặp vô hạn và từ bỏ việc biên dịch. Phần còn lại của phương pháp được tiến hành trong trình thông dịch với các điểm an toàn thích hợp.

Có một bài đăng trên blog tuyệt vời phải đọc "Safepoints: Ý nghĩa, tác dụng phụ và chi phí chung" của Nitsan Wakart bao gồm các điểm an toàn và vấn đề đặc biệt này.

Loại bỏ Safepoint trong các vòng đếm rất dài được biết đến là một vấn đề. Lỗi JDK-5014723(nhờ Vladimir Ivanov ) giải quyết vấn đề này.

Cách giải quyết có sẵn cho đến khi lỗi cuối cùng được sửa.

  1. Bạn có thể thử sử dụng -XX:+UseCountedLoopSafepoints(nó sẽ gây ra hình phạt hiệu năng tổng thể và có thể dẫn đến sự cố JVM JDK-8161147 ). Sau khi sử dụng nó, C2trình biên dịch tiếp tục giữ các điểm an toàn ở các bước nhảy trở lại và tạm dừng ban đầu biến mất hoàn toàn.
  2. Bạn có thể vô hiệu hóa việc biên dịch phương thức có vấn đề bằng cách sử dụng
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. Hoặc bạn có thể viết lại mã của mình bằng cách thêm safepoint bằng tay. Ví dụ: Thread.yield()cuộc gọi vào cuối chu kỳ hoặc thậm chí thay đổi int ithành long i(cảm ơn, Nitsan Wakart ) cũng sẽ sửa lỗi tạm dừng.


7
Đây là câu trả lời thực sự cho câu hỏi làm thế nào để khắc phục .
Andreas

CẢNH BÁO: Không sử dụng -XX:+UseCountedLoopSafepointstrong sản xuất, vì nó có thể làm sập JVM . Cách giải quyết tốt nhất cho đến nay là tự chia vòng lặp dài thành các vòng ngắn hơn.
apangin

@apangin aah. hiểu rồi! cảm ơn bạn :) vì vậy đó là lý do tại sao c2loại bỏ các điểm an toàn! nhưng một điều nữa mà tôi đã không nhận được là những gì sẽ xảy ra tiếp theo. theo như tôi có thể thấy, không còn điểm an toàn nào sau khi không kiểm soát vòng lặp (?) và có vẻ như không có cách nào để làm stw. Vì vậy, có một số loại thời gian chờ xảy ra và tối ưu hóa xảy ra?
vsminkov

2
Nhận xét trước đây của tôi không chính xác. Bây giờ nó hoàn toàn rõ ràng những gì xảy ra. Ở giai đoạn định hình ikhông bao giờ là 0, do đó, vòng lặp được chuyển đổi một cách suy đoán thành một thứ giống như for (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();vòng lặp được tính hữu hạn thông thường. Sau khi ikết thúc trở về 0, bẫy không phổ biến được thực hiện, phương thức được thu nhỏ lại và tiến hành trong trình thông dịch. Trong quá trình biên dịch lại với kiến ​​thức mới, JIT nhận ra vòng lặp vô hạn và từ bỏ việc biên dịch. Phần còn lại của phương thức được thực thi trong trình thông dịch với các điểm an toàn thích hợp.
apangin

1
Bạn chỉ có thể làm cho ia dài thay vì int, điều đó sẽ làm cho vòng lặp "không đếm được" và giải quyết vấn đề.
Nitsan Wakart

64

Nói tóm lại, vòng lặp bạn có không có điểm an toàn bên trong nó trừ khi i == 0đạt được. Khi phương thức này được biên dịch và kích hoạt mã được thay thế, nó cần đưa tất cả các luồng đến điểm an toàn, nhưng điều này mất một thời gian rất dài, khóa không chỉ luồng chạy mã mà tất cả các luồng trong JVM.

Tôi đã thêm các tùy chọn dòng lệnh sau.

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation

Tôi cũng đã sửa đổi mã để sử dụng dấu phẩy động có vẻ mất nhiều thời gian hơn.

boolean b = 1.0 / i == 0;

Và những gì tôi thấy trong đầu ra là

timeElapsed=100
Application time: 0.9560686 seconds
  41423  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds
Application time: 0.0000219 seconds
Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds
  41424  281 %     3       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
timeElapsed=40473
  41425  282 %     4       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
  41426  281 %     3       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
timeElapsed=100

Lưu ý: để mã được thay thế, các luồng phải được dừng tại điểm an toàn. Tuy nhiên, có vẻ như rất hiếm khi đạt đến điểm an toàn như vậy (có thể chỉ khi i == 0Thay đổi tác vụ thành

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};

Tôi thấy một sự chậm trễ tương tự.

timeElapsed=100
Application time: 0.9587419 seconds
  39044  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (28 bytes)   made not entrant
Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds
Application time: 0.0000087 seconds
Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds
timeElapsed=38100
timeElapsed=100

Thêm mã vào vòng lặp một cách cẩn thận, bạn sẽ có độ trễ lâu hơn.

for (int i = 1; i != 0 ; i++) {
    boolean b = 1.0 / i / i == 0;
}

được

 Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds

Tuy nhiên, thay đổi mã để sử dụng phương thức gốc luôn có điểm an toàn (nếu đó không phải là nội tại)

for (int i = 1; i != 0 ; i++) {
    boolean b = Math.cos(1.0 / i) == 0;
}

in

Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds

Lưu ý: thêm if (Thread.currentThread().isInterrupted()) { ... }vào một vòng lặp thêm một điểm an toàn.

Lưu ý: Điều này đã xảy ra trên máy 16 lõi nên không thiếu tài nguyên CPU.


1
Vì vậy, đó là một lỗi JVM, phải không? Trong đó "lỗi" có nghĩa là chất lượng nghiêm trọng của vấn đề triển khai và không vi phạm thông số kỹ thuật.
usr

1
@vsminkov có thể dừng thế giới trong vài phút do thiếu các điểm an toàn nghe có vẻ như nó nên được coi là lỗi. Thời gian chạy có trách nhiệm giới thiệu các điểm an toàn để tránh phải chờ đợi lâu.
Voo

1
@Voo, mặt khác, việc giữ các điểm an toàn trong mỗi lần nhảy lùi có thể tiêu tốn rất nhiều chu kỳ cpu và gây ra sự suy giảm hiệu năng đáng chú ý của toàn bộ ứng dụng. nhưng tôi đồng ý với bạn trong trường hợp cụ thể đó có vẻ hợp pháp để giữ an toàn
vsminkov

9
@Voo tốt ... Tôi luôn nhớ lại hình ảnh này khi nói đến tối ưu hóa hiệu suất: D
vsminkov

1
.NET không chèn các điểm an toàn ở đây (nhưng .NET có mã được tạo chậm). Một giải pháp có thể là chunk vòng lặp. Chia thành hai vòng, làm cho bên trong không kiểm tra các lô 1024 phần tử và các vòng lặp bên ngoài ổ đĩa và các điểm an toàn. Cắt giảm khái niệm trên 1024x, ít hơn trong thực tế.
usr

26

Tìm thấy câu trả lời tại sao . Chúng được gọi là safepoints, và được biết đến nhiều nhất là Thế giới dừng xảy ra vì GC.

Xem bài viết này: Ghi nhật ký tạm dừng thế giới trong JVM

Các sự kiện khác nhau có thể khiến JVM tạm dừng tất cả các luồng ứng dụng. Tạm dừng như vậy được gọi là tạm dừng Thế giới (STW). Nguyên nhân phổ biến nhất khiến tạm dừng STW được kích hoạt là bộ sưu tập rác (ví dụ trong github), nhưng các hành động JIT khác nhau (ví dụ), thu hồi khóa thiên vị (ví dụ), các hoạt động JVMTI nhất định và nhiều hoạt động khác cũng yêu cầu dừng ứng dụng.

Những điểm mà tại đó các chủ đề ứng dụng có thể được dừng lại một cách an toàn được gọi, bất ngờ, safepoints . Thuật ngữ này cũng thường được sử dụng để chỉ tất cả các tạm dừng STW.

Nó ít nhiều phổ biến là các bản ghi GC được kích hoạt. Tuy nhiên, điều này không nắm bắt thông tin về tất cả các điểm an toàn. Để có được tất cả, hãy sử dụng các tùy chọn JVM sau:

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

Nếu bạn đang tự hỏi về việc đặt tên rõ ràng đề cập đến GC, đừng lo lắng - bật các tùy chọn này sẽ ghi lại tất cả các điểm an toàn, không chỉ tạm dừng thu gom rác. Nếu bạn chạy một ví dụ sau (nguồn trong github) với các cờ được chỉ định ở trên.

Đọc Thuật ngữ HotSpot của Điều khoản , nó xác định điều này:

an toàn

Một điểm trong khi thực hiện chương trình mà tại đó tất cả các gốc GC được biết đến và tất cả các nội dung đối tượng heap đều nhất quán. Từ quan điểm toàn cầu, tất cả các luồng phải chặn tại một điểm an toàn trước khi GC có thể chạy. (Trong trường hợp đặc biệt, các luồng chạy mã JNI có thể tiếp tục chạy, vì chúng chỉ sử dụng tay cầm. Trong một lần bảo vệ, chúng phải chặn thay vì tải nội dung của tay cầm.) Theo quan điểm cục bộ, điểm an toàn là một điểm khác biệt trong một khối mã nơi luồng thực thi có thể chặn cho GC. Hầu hết các trang web gọi đủ điều kiện là safepoints.Có những bất biến mạnh mẽ giữ đúng ở mọi điểm an toàn, có thể bị coi nhẹ ở những điểm không an toàn. Cả mã Java được biên dịch và mã C / C ++ đều được tối ưu hóa giữa các điểm an toàn, nhưng ít hơn so với các điểm an toàn. Trình biên dịch JIT phát ra một bản đồ GC tại mỗi điểm an toàn. Mã C / C ++ trong VM sử dụng các quy ước dựa trên macro được cách điệu (ví dụ: TRAPS) để đánh dấu các điểm an toàn tiềm năng.

Chạy với các cờ đã đề cập ở trên, tôi nhận được kết quả này:

Application time: 0.9668750 seconds
Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds
timeElapsed=1015
Application time: 1.0148568 seconds
Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds
timeElapsed=1015
timeElapsed=1014
Application time: 2.0453971 seconds
Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds
timeElapsed=11732
Application time: 1.0149263 seconds
Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds
timeElapsed=1015

Lưu ý sự kiện STW thứ ba:
Tổng thời gian dừng: 10.7951187 giây
Dừng các luồng đã mất: 10,7950774 giây

Bản thân JIT hầu như không mất thời gian, nhưng một khi JVM đã quyết định thực hiện quá trình biên dịch JIT, thì nó đã chuyển sang chế độ STW, tuy nhiên do mã được biên dịch (vòng lặp vô hạn) không có trang web cuộc gọi , không đạt được điểm an toàn nào.

STW kết thúc khi JIT cuối cùng từ bỏ chờ đợi và kết luận mã nằm trong một vòng lặp vô hạn.


"Safepoint - Một điểm trong khi thực hiện chương trình mà tại đó tất cả các gốc GC đều được biết và tất cả các nội dung của đối tượng heap đều nhất quán" - Tại sao điều này không đúng trong một vòng lặp chỉ đặt / đọc các biến loại giá trị cục bộ?
BlueRaja - Daniel Pflughoeft

@ BlueRaja-DannyPflughoeft Tôi đã cố gắng trả lời câu hỏi này trong câu trả lời của mình
vsminkov

5

Sau khi tự mình theo dõi các luồng nhận xét và một số thử nghiệm, tôi tin rằng việc tạm dừng là do trình biên dịch JIT gây ra. Tại sao trình biên dịch JIT mất nhiều thời gian như vậy lại vượt quá khả năng gỡ lỗi của tôi.

Tuy nhiên, vì bạn chỉ yêu cầu làm thế nào để ngăn chặn điều này, tôi có một giải pháp:

Kéo vòng lặp vô hạn của bạn vào một phương thức có thể loại trừ nó khỏi trình biên dịch JIT

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] args) throws InterruptedException     {
        Runnable task = () -> {
            infLoop();
        };
        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    private static void infLoop()
    {
        int i = 0;
        while (true) {
            i++;
            if (i != 0) {
                boolean b = 1 % i == 0;
            }
        }
    }

Chạy chương trình của bạn với đối số VM này:

-XX: CompileCommand = loại trừ, PACKAGE.TestBlockingThread :: infLoop (thay thế GÓI bằng thông tin gói của bạn)

Bạn sẽ nhận được một thông báo như thế này để cho biết khi nào phương thức sẽ được biên dịch JIT:
### Không bao gồm biên dịch: static static.TestBlockingThread :: infLoop
bạn có thể nhận thấy rằng tôi đặt lớp vào một gói được gọi là chặn


1
Trình biên dịch không mất nhiều thời gian, vấn đề là mã không đạt đến điểm an toàn vì không có gì trong vòng lặp ngoại trừ khii == 0
Peter Lawrey

@PeterLawrey nhưng tại sao kết thúc chu kỳ trong whilevòng lặp không phải là điểm an toàn?
vsminkov

@vsminkov Có vẻ như có một điểm an toàn if (i != 0) { ... } else { safepoint(); }nhưng điều này rất hiếm. I E. nếu bạn thoát / ngắt vòng lặp, bạn sẽ nhận được nhiều thời gian giống nhau.
Peter Lawrey

@PeterLawrey sau một chút điều tra Tôi thấy rằng đó là một thực tế phổ biến để tạo ra một điểm an toàn khi nhảy ngược lại vòng lặp. Tôi chỉ tò mò sự khác biệt trong trường hợp cụ thể này là gì. có thể tôi ngây thơ nhưng tôi thấy không có lý do tại sao nhảy lùi lại không "an toàn"
vsminkov

@vsminkov Tôi nghi ngờ rằng JIT thấy một điểm an toàn đang ở trong vòng lặp nên không thêm một cái nào ở cuối.
Peter Lawrey
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.