เหตุใด BufferedInputStream จึงคัดลอกฟิลด์ไปยังตัวแปรโลคัลแทนที่จะใช้ฟิลด์โดยตรง


107

เมื่อฉันอ่านซอร์สโค้ดจากjava.io.BufferedInputStream.getInIfOpen()ฉันก็งงว่าทำไมมันถึงเขียนโค้ดแบบนี้:

/**
 * 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;
}

เหตุใดจึงใช้นามแฝงแทนการใช้ตัวแปรฟิลด์inโดยตรงเช่นด้านล่าง:

/**
 * 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;
}

มีใครสามารถให้คำอธิบายที่สมเหตุสมผลได้หรือไม่?


ในEclipseคุณไม่สามารถหยุดดีบักเกอร์ชั่วคราวในifคำสั่งได้ อาจเป็นเหตุผลสำหรับตัวแปรนามแฝงนั้น แค่อยากจะโยนมันออกไป แน่นอนฉันคาดเดา
Debosmit Ray

@DebosmitRay: หยุดifคำสั่งไม่ได้จริงๆเหรอ?
rkosegi

@rkosegi ในรุ่นของฉัน Eclipse ปัญหาคล้ายกับคนนี้ อาจไม่ใช่เหตุการณ์ที่เกิดขึ้นบ่อยนัก และอย่างไรก็ตามฉันไม่ได้หมายถึงมันในเรื่องเบา ๆ (เห็นได้ชัดว่าเป็นเรื่องตลกที่ไม่ดี) :)
Debosmit Ray

คำตอบ:


119

หากคุณดูโค้ดนี้จากบริบทไม่มีคำอธิบายที่ดีสำหรับ "นามแฝง" นั้น เป็นเพียงรหัสซ้ำซ้อนหรือรูปแบบรหัสที่ไม่ดี

แต่บริบทนั้นBufferedInputStreamเป็นคลาสที่สามารถแบ่งเป็นคลาสย่อยได้และจำเป็นต้องทำงานในบริบทที่มีหลายเธรด

เบาะแสคือการที่inจะมีการประกาศในมีFilterInputStream protected volatileนั่นหมายความว่ามีโอกาสที่คลาสย่อยสามารถเข้าถึงและมอบหมายnullให้inได้ ด้วยความเป็นไปได้ดังกล่าวจึงมี "นามแฝง" อยู่เพื่อป้องกันสภาพการแข่งขัน

พิจารณารหัสที่ไม่มี "นามแฝง"

private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}
  1. เธรดการโทร getInIfOpen()
  2. ด้ายประเมินin == nullและเห็นว่าไม่innull
  3. ด้ายกำหนด B ไปnullin
  4. return inด้ายรัน ซึ่งผลตอบแทนnullเพราะaเป็นกvolatile.

"นามแฝง" ป้องกันสิ่งนี้ ตอนนี้inอ่านเพียงครั้งเดียวโดยเธรด A หากเธรด B กำหนดnullหลังเธรด A inก็ไม่สำคัญ เธรด A จะโยนข้อยกเว้นหรือส่งคืนค่าที่ไม่ใช่ค่าว่าง (รับประกัน)


11
ซึ่งแสดงให้เห็นว่าเหตุใดprotectedตัวแปรจึงชั่วร้ายในบริบทแบบมัลติเธรด
Mick Mnemonic

2
มันไม่แน่นอน อย่างไรก็ตาม AFAIK คลาสเหล่านี้กลับไปที่ Java 1.0 นี่เป็นเพียงอีกตัวอย่างหนึ่งของการตัดสินใจในการออกแบบที่ไม่ดีซึ่งไม่สามารถแก้ไขได้เพราะกลัวว่าจะทำลายรหัสลูกค้า
Stephen C

2
@StephenC ขอบคุณสำหรับคำอธิบายโดยละเอียด +1 นั่นหมายความว่าเราไม่ควรใช้protectedตัวแปรในโค้ดของเราหากเป็นแบบมัลติเธรด?
Madhusudana Reddy Sunnapu

3
@MadhusudanaReddySunnapu บทเรียนโดยรวมคือในสภาพแวดล้อมที่หลายเธรดอาจเข้าถึงสถานะเดียวกันคุณต้องควบคุมการเข้าถึงนั้นด้วยวิธีใดวิธีหนึ่ง นั่นอาจเป็นตัวแปรส่วนตัวที่สามารถเข้าถึงได้ผ่าน setter เท่านั้นมันอาจเป็นยามท้องถิ่นเช่นนี้ได้โดยการทำให้ตัวแปรเขียนครั้งเดียวด้วยวิธีที่ปลอดภัย
Chris Hayes

3
@sam - 1) ไม่จำเป็นต้องอธิบายเงื่อนไขและสถานะการแข่งขันทั้งหมด จุดมุ่งหมายของคำตอบคือการชี้ให้เห็นว่าเหตุใดรหัสที่ดูเหมือนอธิบายไม่ได้นี้จึงมีความจำเป็น 2) อย่างไร?
Stephen C

20

เนื่องจากคลาสBufferedInputStreamนี้ออกแบบมาสำหรับการใช้งานแบบมัลติเธรด

ที่นี่คุณจะเห็นการประกาศinซึ่งวางอยู่ในคลาสพาเรนต์FilterInputStream:

protected volatile InputStream in;

เนื่องจากเป็นprotectedเช่นนั้นค่าของมันสามารถเปลี่ยนแปลงได้โดยคลาสย่อยใด ๆ ของFilterInputStreamรวมถึงBufferedInputStreamและคลาสย่อยของมัน นอกจากนี้ยังมีการประกาศvolatileซึ่งหมายความว่าหากเธรดใด ๆ เปลี่ยนแปลงค่าของตัวแปรการเปลี่ยนแปลงนี้จะปรากฏในเธรดอื่น ๆ ทั้งหมดทันที ชุดค่าผสมนี้ไม่ดีเนื่องจากหมายความว่าชั้นเรียนBufferedInputStreamไม่มีทางควบคุมหรือรู้ว่าเมื่อใดinมีการเปลี่ยนแปลง ดังนั้นค่ายังสามารถเปลี่ยนแปลงได้ระหว่างการตรวจสอบค่าว่างและคำสั่งส่งคืนในBufferedInputStream::getInIfOpenซึ่งทำให้การตรวจสอบค่าว่างเปล่าอย่างมีประสิทธิภาพ การอ่านค่าinเพียงครั้งเดียวเพื่อแคชในตัวแปรโลคัลinputเมธอดBufferedInputStream::getInIfOpenนี้ปลอดภัยต่อการเปลี่ยนแปลงจากเธรดอื่นเนื่องจากตัวแปรโลคัลจะเป็นของเธรดเดียวเสมอ

มีตัวอย่างBufferedInputStream::closeซึ่งตั้งinค่าเป็นโมฆะ:

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()
    }
}

หากBufferedInputStream::closeถูกเรียกโดยเธรดอื่นในขณะที่BufferedInputStream::getInIfOpenดำเนินการสิ่งนี้จะส่งผลให้เกิดสภาวะการแข่งขันที่อธิบายไว้ข้างต้น


ฉันยอมรับตราบเท่าที่เราเห็นสิ่งที่ชอบcompareAndSet(), CASฯลฯ ในรหัสและในการแสดงความคิดเห็น ฉันยังค้นหาBufferedInputStreamโค้ดและพบsynchronizedวิธีการมากมาย ดังนั้นจึงมีไว้สำหรับการใช้งานแบบมัลติเธรดแม้ว่าฉันแน่ใจว่าไม่เคยใช้แบบนั้น อย่างไรก็ตามฉันคิดว่าคำตอบของคุณถูกต้อง!
sparc_spread

สิ่งนี้อาจเหมาะสมเพราะgetInIfOpen()เรียกจากpublic synchronizedวิธีการBufferedInputStreamเท่านั้น
Mick Mnemonic

6

นี่เป็นรหัสสั้น ๆ แต่ในทางทฤษฎีในสภาพแวดล้อมแบบมัลติเธรดinอาจเปลี่ยนแปลงทันทีหลังจากการเปรียบเทียบดังนั้นวิธีการสามารถส่งคืนสิ่งที่ไม่ได้ตรวจสอบ (สามารถคืนค่าnullได้ดังนั้นจึงทำสิ่งที่แน่นอนตามที่ตั้งใจไว้ ป้องกัน).


ฉันถูกต้องหรือไม่ถ้าฉันบอกว่าการอ้างอิงin อาจเปลี่ยนแปลงระหว่างเวลาที่คุณเรียกใช้เมธอดและการส่งคืนค่า (ในสภาพแวดล้อมแบบมัลติเธรด)
Debosmit Ray

ใช่คุณสามารถพูดได้ ในที่สุดความเป็นไปได้จะขึ้นอยู่กับกรณีที่เป็นรูปธรรม (สิ่งที่เรารู้ก็คือinสามารถเปลี่ยนแปลงได้ตลอดเวลา)
acdcjunior

4

ฉันเชื่อว่าการจับตัวแปรคลาสinไปยังตัวแปรโลคัลinputคือการป้องกันพฤติกรรมที่ไม่สอดคล้องกันหากinเธรดอื่นเปลี่ยนแปลงในขณะที่getInIfOpen()กำลังทำงาน

ขอให้สังเกตว่าเจ้าของเป็นระดับผู้ปกครองและไม่ได้ทำเครื่องหมายว่าinfinal

รูปแบบนี้จำลองแบบในส่วนอื่น ๆ ของคลาสและดูเหมือนว่าจะเป็นการเข้ารหัสเชิงป้องกันที่สมเหตุสมผล

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.