Saya menemukan situasi yang aneh di mana menggunakan aliran paralel dengan lambda di penginisialisasi statis membutuhkan waktu yang tampaknya selamanya tanpa pemanfaatan CPU. Berikut kodenya:
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
Ini tampaknya mereproduksi kasus uji minimum untuk perilaku ini. Jika saya:
- letakkan blok di metode utama alih-alih penginisialisasi statis,
- hapus paralelisasi, atau
- hapus lambda,
kode tersebut langsung selesai. Adakah yang bisa menjelaskan perilaku ini? Apakah ini bug atau ini dimaksudkan?
Saya menggunakan OpenJDK versi 1.8.0_66-internal.
i -> i
bukan referensi metode itustatic method
diimplementasikan di kelas Deadlock. Jika digantii -> i
denganFunction.identity()
kode ini seharusnya baik-baik saja.Jawaban:
Saya menemukan laporan bug dari kasus yang sangat mirip ( JDK-8143380 ) yang ditutup sebagai "Bukan Masalah" oleh Stuart Marks:
Saya dapat menemukan laporan bug lain dari itu ( JDK-8136753 ), juga ditutup sebagai "Bukan Masalah" oleh Stuart Marks:
Perhatikan bahwa FindBugs memiliki masalah terbuka untuk menambahkan peringatan untuk situasi ini.
sumber
this
melarikan diri selama konstruksi objek. Aturan dasarnya adalah, jangan gunakan operasi multi-utas di penginisialisasi. Saya rasa ini tidak sulit untuk dipahami. Contoh Anda mendaftarkan fungsi yang diimplementasikan lambda ke dalam registri adalah hal yang berbeda, itu tidak membuat kebuntuan kecuali Anda akan menunggu salah satu utas latar belakang yang diblokir ini. Namun demikian, saya sangat tidak menyarankan untuk melakukan operasi seperti itu di penginisialisasi kelas. Bukan untuk apa mereka dimaksudkan.Bagi mereka yang bertanya-tanya di mana utas lain yang mereferensikan
Deadlock
kelas itu sendiri, lambda Java berperilaku seperti yang Anda tulis ini:public class Deadlock { public static int lambda1(int i) { return i; } static { IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() { @Override public int applyAsInt(int operand) { return lambda1(operand); } }).count(); System.out.println("done"); } public static void main(final String[] args) {} }
Dengan kelas anonim biasa tidak ada kebuntuan:
public class Deadlock { static { IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() { @Override public int applyAsInt(int operand) { return operand; } }).count(); System.out.println("done"); } public static void main(final String[] args) {} }
sumber
lambda1
contoh ini). Menempatkan setiap lambda ke dalam kelasnya sendiri akan jauh lebih mahal.i -> i
; mereka tidak akan menjadi norma. Ekspresi Lambda dapat menggunakan semua anggota kelas sekitarnya, termasukprivate
satu, dan itu membuat kelas pendefinisian itu sendiri menjadi tempat alami mereka. Membiarkan semua kasus penggunaan ini menderita akibat implementasi yang dioptimalkan untuk kasus khusus penginisialisasi kelas dengan penggunaan multi-threaded ekspresi lambda sepele, tidak menggunakan anggota kelas yang menentukan, bukanlah opsi yang dapat dijalankan.Ada penjelasan yang sangat bagus tentang masalah ini oleh Andrei Pangin , tertanggal 07 Apr 2015. Ini tersedia di sini , tetapi ditulis dalam bahasa Rusia (saya sarankan untuk meninjau contoh kode juga - mereka internasional). Masalah umum adalah kunci selama inisialisasi kelas.
Berikut beberapa kutipan dari artikel tersebut:
Menurut JLS , setiap kelas memiliki kunci inisialisasi unik yang ditangkap selama inisialisasi. Saat thread lain mencoba mengakses kelas ini selama inisialisasi, thread tersebut akan diblokir di kunci sampai inisialisasi selesai. Ketika kelas diinisialisasi secara bersamaan, mungkin saja terjadi kebuntuan.
Saya menulis program sederhana yang menghitung jumlah bilangan bulat, apa yang harus dicetak?
public class StreamSum { static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt(); public static void main(String[] args) { System.out.println(SUM); } }
Sekarang hapus
parallel()
atau ganti lambda denganInteger::sum
panggilan - apa yang akan berubah?Di sini kita melihat kebuntuan lagi [ada beberapa contoh kebuntuan di penginisialisasi kelas sebelumnya di artikel]. Karena
parallel()
operasi aliran dijalankan di kumpulan utas terpisah. Utas ini mencoba menjalankan lambda body, yang ditulis dalam bytecode sebagaiprivate static
metode di dalamStreamSum
kelas. Tetapi metode ini tidak dapat dijalankan sebelum penyelesaian penginisialisasi statis kelas, yang menunggu hasil penyelesaian streaming.Apa yang lebih mengejutkan: kode ini bekerja secara berbeda di lingkungan yang berbeda. Ini akan bekerja dengan benar pada satu mesin CPU dan kemungkinan besar akan digantung pada mesin multi CPU. Perbedaan ini berasal dari implementasi kumpulan Fork-Join. Anda dapat memverifikasi sendiri dengan mengubah parameter
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
sumber