Cuplikan kode berikut mengeksekusi dua utas, satu pengatur waktu sederhana yang mencatat setiap detik, yang kedua adalah loop tak terbatas yang menjalankan operasi sisa:
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);
}
}
}
}
Ini memberikan hasil sebagai berikut:
[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
Saya tidak mengerti mengapa tugas tak terbatas memblokir semua utas lainnya selama 13,3 detik. Saya mencoba mengubah prioritas utas dan pengaturan lainnya, tidak ada yang berhasil.
Jika Anda memiliki saran untuk memperbaikinya (termasuk menyesuaikan pengaturan pengalihan konteks OS), beri tahu saya.
java
multithreading
kms333
sumber
sumber
-XX:+PrintCompilation
saya mendapatkan yang berikut pada saat penundaan yang diperpanjang berakhir: TestBlockingThread :: lambda $ 0 @ 2 (24 byte) COMPILE SKIPPED: trivial infinite loop (coba lagi di tingkat yang berbeda)-Djava.compiler=NONE
, itu tidak akan terjadi.Jawaban:
Setelah semua penjelasan di sini (terima kasih kepada Peter Lawrey ) kami menemukan bahwa sumber utama dari jeda ini adalah bahwa titik aman di dalam loop cukup jarang dicapai sehingga membutuhkan waktu lama untuk menghentikan semua utas untuk penggantian kode yang dikompilasi JIT.
Tetapi saya memutuskan untuk melangkah lebih dalam dan menemukan mengapa titik aman jarang dicapai. Saya merasa agak membingungkan mengapa lompatan belakang
while
loop tidak "aman" dalam kasus ini.Jadi saya memanggil
-XX:+PrintAssembly
dengan segala kemuliaan untuk membantuSetelah beberapa penyelidikan saya menemukan bahwa setelah rekompilasi ketiga dari
C2
compiler lambda membuang polling safepoint di dalam loop sepenuhnya.MEMPERBARUI
Selama tahap pembuatan profil variabel
i
tidak pernah terlihat sama dengan 0. Itulah mengapa secaraC2
spekulatif mengoptimalkan cabang ini, sehingga loop diubah menjadi sesuatu sepertiPerhatikan bahwa perulangan tak terbatas awalnya dibentuk ulang menjadi perulangan hingga reguler dengan penghitung! Karena pengoptimalan JIT untuk menghilangkan jajak pendapat titik aman dalam pengulangan terhitung hingga, tidak ada jajak pendapat titik aman dalam pengulangan ini.
Setelah beberapa waktu,
i
dibungkus kembali0
, dan jebakan yang tidak biasa diambil. Metode ini tidak dioptimalkan dan terus dijalankan di penerjemah. Selama kompilasi ulang dengan pengetahuan baruC2
mengenali loop tak terbatas dan menyerahkan kompilasi. Metode lainnya dilakukan oleh penerjemah dengan titik aman yang tepat.Ada entri blog bagus yang harus dibaca "Titik Aman: Arti, Efek Samping, dan Overhead" oleh Nitsan Wakart yang mencakup titik aman dan masalah khusus ini.
Penghapusan Safepoint dalam loop yang dihitung sangat lama diketahui menjadi masalah. Bug
JDK-5014723
(terima kasih Vladimir Ivanov ) mengatasi masalah ini.Solusi tersedia hingga bug akhirnya diperbaiki.
-XX:+UseCountedLoopSafepoints
(ini akan menyebabkan penalti performa secara keseluruhan dan dapat menyebabkan JVM crashJDK-8161147
). Setelah menggunakannya,C2
kompilator terus menyimpan titik aman di lompatan belakang dan jeda asli menghilang sepenuhnya.Anda dapat secara eksplisit menonaktifkan kompilasi metode bermasalah dengan menggunakan
-XX:CompileCommand='exclude,binary/class/Name,methodName'
Atau Anda dapat menulis ulang kode Anda dengan menambahkan titik aman secara manual. Misalnya
Thread.yield()
panggilan di akhir siklus atau bahkan berubahint i
menjadilong i
(terima kasih, Nitsan Wakart ) juga akan memperbaiki jeda.sumber
-XX:+UseCountedLoopSafepoints
dalam produksi, karena dapat merusak JVM . Solusi terbaik sejauh ini adalah membagi loop panjang secara manual menjadi yang lebih pendek.c2
menghapus titik aman! tetapi satu hal lagi yang tidak saya dapatkan adalah apa yang terjadi selanjutnya. Sejauh yang saya lihat tidak ada titik aman yang tersisa setelah loop membuka gulungan (?) dan sepertinya tidak ada cara untuk melakukan stw. jadi ada semacam waktu tunggu yang terjadi dan de-optimasi terjadi?i
tidak pernah 0, jadi loop secara spekulatif diubah menjadi sesuatu sepertifor (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();
Ie loop terhitung hingga reguler. Setelahi
membungkus kembali ke 0, perangkap yang tidak biasa diambil, metode ini tidak dioptimalkan dan dilanjutkan di penerjemah. Selama kompilasi ulang dengan pengetahuan baru, JIT mengenali loop tak terbatas dan menghentikan kompilasi. Metode lainnya dijalankan di interpreter dengan titik aman yang tepat.Singkatnya, loop yang Anda miliki tidak memiliki titik aman di dalamnya kecuali jika
i == 0
sudah tercapai. Ketika metode ini dikompilasi dan memicu kode untuk diganti, metode ini perlu membawa semua utas ke titik aman, tetapi ini membutuhkan waktu yang sangat lama, mengunci tidak hanya utas yang menjalankan kode tetapi semua utas di JVM.Saya menambahkan opsi baris perintah berikut.
Saya juga memodifikasi kode untuk menggunakan floating point yang tampaknya membutuhkan waktu lebih lama.
Dan yang saya lihat di output adalah
Catatan: untuk kode yang akan diganti, utas harus dihentikan pada titik aman. Namun tampaknya di sini titik aman seperti itu sangat jarang dicapai (mungkin hanya saat
i == 0
Mengubah tugas keSaya melihat penundaan serupa.
Menambahkan kode ke loop dengan hati-hati Anda mendapatkan penundaan yang lebih lama.
mendapat
Namun, ubah kode untuk menggunakan metode asli yang selalu memiliki titik aman (jika tidak intrinsik)
cetakan
Catatan: menambahkan
if (Thread.currentThread().isInterrupted()) { ... }
ke perulangan menambahkan titik aman.Catatan: Ini terjadi pada mesin 16 inti sehingga tidak ada kekurangan sumber daya CPU.
sumber
Menemukan jawaban mengapa . Mereka disebut titik aman, dan paling dikenal sebagai Stop-The-World yang terjadi karena GC.
Lihat artikel ini: Logging stop-the-world berhenti di JVM
Membaca Glosarium Istilah HotSpot , mendefinisikan ini:
Berjalan dengan flag yang disebutkan di atas, saya mendapatkan output ini:
Perhatikan peristiwa STW ketiga:
Total waktu berhenti: 10,7951187 detik
Menghentikan thread yang dibutuhkan: 10,7950774 detik
JIT sendiri hampir tidak memakan waktu, tetapi setelah JVM memutuskan untuk melakukan kompilasi JIT, JIT memasuki mode STW, namun karena kode yang akan dikompilasi (loop tak terbatas) tidak memiliki situs panggilan , tidak ada titik aman yang tercapai.
STW berakhir ketika JIT akhirnya menyerah menunggu dan menyimpulkan kode berada dalam loop tak terbatas.
sumber
Setelah mengikuti utas komentar dan beberapa pengujian saya sendiri, saya yakin bahwa jeda ini disebabkan oleh kompiler JIT. Mengapa kompiler JIT membutuhkan waktu yang lama berada di luar kemampuan saya untuk melakukan debug.
Namun, karena Anda hanya menanyakan cara mencegahnya, saya punya solusi:
Tarik loop tak terbatas Anda ke metode yang dapat dikecualikan dari kompiler JIT
Jalankan program Anda dengan argumen VM ini:
-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (ganti PACKAGE dengan informasi paket Anda)
Anda harus mendapatkan pesan seperti ini untuk menunjukkan kapan metode tersebut akan dikompilasi JIT:
### Excludes compile: static blocking.TestBlockingThread :: infLoop
Anda mungkin memperhatikan bahwa saya memasukkan kelas ke dalam paket yang disebut pemblokiran
sumber
i == 0
while
lingkaran bukanlah titik aman?if (i != 0) { ... } else { safepoint(); }
tetapi ini sangat jarang. yaitu. jika Anda keluar / memutus perulangan, Anda mendapatkan banyak waktu yang sama.