Mengapa BufferedInputStream menyalin bidang ke variabel lokal daripada menggunakan bidang secara langsung

107

Saat saya membaca source code dari java.io.BufferedInputStream.getInIfOpen(), saya bingung kenapa menulis kode seperti ini:

/**
 * Check to make sure that underlying input stream has not been
 * nulled out due to close; if not return it;
 */
private InputStream getInIfOpen() throws IOException {
    InputStream input = in;
    if (input == null)
        throw new IOException("Stream closed");
    return input;
}

Mengapa menggunakan alias daripada menggunakan variabel bidang insecara langsung seperti di bawah ini:

/**
 * Check to make sure that underlying input stream has not been
 * nulled out due to close; if not return it;
 */
private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}

Bisakah seseorang memberikan penjelasan yang masuk akal?

Santo
sumber
Di Eclipse, Anda tidak dapat menjeda debugger pada ifpernyataan. Mungkin menjadi alasan variabel alias itu. Hanya ingin membuangnya ke sana. Saya berspekulasi, tentu saja.
Debosmit Ray
@DebosmitRay: Benar-benar tidak bisa berhenti pada ifpernyataan?
rkosegi
@rkosegi Pada versi Eclipse saya, masalahnya mirip dengan yang ini . Mungkin bukan kejadian yang sangat umum. Dan bagaimanapun, saya tidak bermaksud begitu saja (jelas lelucon yang buruk). :)
Debosmit Ray

Jawaban:

119

Jika Anda melihat kode ini di luar konteks, tidak ada penjelasan yang baik untuk "alias" itu. Ini hanyalah kode yang berlebihan atau gaya kode yang buruk.

Tapi konteksnya adalah itu BufferedInputStreamadalah kelas yang bisa disubkelas, dan itu perlu bekerja dalam konteks multi-utas.

Petunjuknya adalah yang indideklarasikan dalam FilterInputStreamadalah protected volatile. Itu berarti bahwa ada kemungkinan bahwa subclass bisa mencapai dan menetapkan nulluntuk in. Mengingat kemungkinan itu, "alias" sebenarnya ada untuk mencegah kondisi balapan.

Pertimbangkan kode tanpa "alias"

private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}
  1. Thread A. panggilan getInIfOpen()
  2. Thread A mengevaluasi in == nulldan melihat yang intidak null.
  3. Utas B diberikan nullke in.
  4. Thread A dijalankan return in. Yang mengembalikan nullkarena aa volatile.

"Alias" mencegah ini. Sekarang indibaca hanya sekali oleh utas A. Jika utas B diberikan nullsetelah utas A memilikinya, initu tidak masalah. Thread A akan memunculkan pengecualian atau mengembalikan nilai bukan null (dijamin).

Stephen C
sumber
11
Yang menunjukkan mengapa protectedvariabel itu jahat dalam konteks multi-threaded.
Mick Mnemonic
2
Memang benar. Namun, AFAIK kelas-kelas ini kembali ke Java 1.0. Ini hanyalah contoh lain dari keputusan desain yang buruk yang tidak dapat diperbaiki karena takut melanggar kode pelanggan.
Stephen C
2
@StephenC Terima kasih atas penjelasan rinci +1. Jadi apakah itu berarti, kita tidak boleh menggunakan protectedvariabel dalam kode kita jika itu multi-threaded?
Madhusudana Reddy Sunnapu
3
@MadhusudanaReddySunnapu Pelajaran keseluruhannya adalah bahwa dalam lingkungan di mana banyak utas dapat mengakses status yang sama, Anda perlu mengontrol akses itu entah bagaimana. Itu mungkin variabel privat yang hanya dapat diakses melalui penyetel, bisa juga penjaga lokal seperti ini, bisa juga dengan membuat variabel tulis sekali dengan cara yang aman untuk thread.
Chris Hayes
3
@sam - 1) Tidak perlu menjelaskan semua kondisi dan status balapan. Tujuan dari jawaban ini adalah untuk menunjukkan mengapa kode yang tampaknya tidak dapat dijelaskan ini sebenarnya diperlukan. 2) Bagaimana bisa?
Stephen C
20

Ini karena kelas BufferedInputStreamdirancang untuk penggunaan multi-utas.

Di sini, Anda melihat deklarasi in, yang ditempatkan di kelas induk FilterInputStream:

protected volatile InputStream in;

Karena itu protected, nilainya dapat diubah oleh subkelas apa pun FilterInputStream, termasuk BufferedInputStreamdan subkelasnya. Juga, itu dideklarasikan volatile, yang berarti bahwa jika ada thread yang mengubah nilai variabel, perubahan ini akan segera terlihat di semua thread lainnya. Kombinasi ini buruk, karena itu berarti kelas BufferedInputStreamtidak memiliki cara untuk mengontrol atau mengetahui kapan indiubah. Dengan demikian, nilainya bahkan dapat diubah antara pemeriksaan null dan pernyataan return in BufferedInputStream::getInIfOpen, yang secara efektif membuat pemeriksaan null tidak berguna. Dengan membaca nilai inhanya sekali untuk menyimpannya dalam cache di variabel lokal input, metode BufferedInputStream::getInIfOpenini aman dari perubahan dari utas lain, karena variabel lokal selalu dimiliki oleh satu utas.

Ada contoh di BufferedInputStream::close, yang disetel inke nol:

public void close() throws IOException {
    byte[] buffer;
    while ( (buffer = buf) != null) {
        if (bufUpdater.compareAndSet(this, buffer, null)) {
            InputStream input = in;
            in = null;
            if (input != null)
                input.close();
            return;
        }
        // Else retry in case a new buf was CASed in fill()
    }
}

Jika BufferedInputStream::closedipanggil oleh utas lain saat BufferedInputStream::getInIfOpendijalankan, ini akan menghasilkan kondisi balapan yang dijelaskan di atas.

Stefan Dollase
sumber
Saya setuju, karena kita melihat hal-hal seperti compareAndSet(), CAS, dll dalam kode dan di komentar. Saya juga mencari BufferedInputStreamkode dan menemukan banyak synchronizedmetode. Jadi, ini ditujukan untuk penggunaan multi-threaded, meski saya yakin belum pernah menggunakannya seperti itu. Bagaimanapun, saya pikir jawaban Anda benar!
sparc_spread
Ini mungkin masuk akal karena getInIfOpen()hanya dipanggil dari public synchronizedmetode BufferedInputStream.
Mick Mnemonic
6

Ini adalah kode yang singkat, tetapi, secara teoritis, dalam lingkungan multi-utas, indapat berubah tepat setelah perbandingan, sehingga metode dapat mengembalikan sesuatu yang tidak diperiksa (dapat kembali null, sehingga melakukan hal yang tepat seperti yang dimaksudkan untuk mencegah).

acdcjunior.dll
sumber
Apakah saya benar jika saya mengatakan bahwa referensi in mungkin berubah antara waktu Anda memanggil metode dan mengembalikan nilai (dalam lingkungan multi-utas)?
Debosmit Ray
Ya, bisa dibilang begitu. Pada akhirnya kemungkinannya akan sangat bergantung pada kasus konkret (semua yang kita tahu fakta adalah bahwa hal itu indapat berubah kapan saja).
acdcjunior
4

Saya percaya menangkap variabel kelas inke variabel lokal inputadalah untuk mencegah perilaku yang tidak konsisten jika indiubah oleh utas lain saat getInIfOpen()sedang berjalan.

Perhatikan bahwa pemilik inadalah kelas induk dan tidak menandainya sebagai final.

Pola ini direplikasi di bagian lain kelas dan tampaknya merupakan pengkodean pertahanan yang wajar.

Sam
sumber