การซิงโครไนซ์ฟิลด์ที่ไม่ใช่ขั้นสุดท้าย


91

คำเตือนจะแสดงทุกครั้งที่ฉันซิงโครไนซ์ในฟิลด์คลาสที่ไม่ใช่คลาสสุดท้าย นี่คือรหัส:

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

ดังนั้นฉันจึงเปลี่ยนการเข้ารหัสด้วยวิธีต่อไปนี้:

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

ฉันไม่แน่ใจว่าโค้ดด้านบนเป็นวิธีที่เหมาะสมในการซิงโครไนซ์บนฟิลด์คลาสที่ไม่ใช่คลาสสุดท้าย ฉันจะซิงโครไนซ์ฟิลด์ที่ไม่ใช่ข้อมูลสุดท้ายได้อย่างไร

คำตอบ:


128

ก่อนอื่นขอแนะนำให้คุณพยายามจัดการกับปัญหาการเกิดพร้อมกันในระดับที่สูงขึ้นของนามธรรมนั่นคือการแก้ปัญหาโดยใช้คลาสจากjava.util.concurrentเช่น ExecutorServices, Callables, Futures เป็นต้น

ดังที่กล่าวมาไม่มีอะไรผิดปกติกับการซิงโครไนซ์ในฟิลด์ที่ไม่ใช่ขั้นสุดท้ายต่อ se คุณเพียงแค่ต้องจำไว้ว่าหากการอ้างอิงอ็อบเจ็กต์เปลี่ยนไปส่วนเดียวกันของโค้ดอาจทำงานควบคู่กันได้ กล่าวคือถ้าเธรดหนึ่งรันโค้ดในบล็อกที่ซิงโครไนซ์และมีคนเรียกใช้setO(...)เธรดอื่นสามารถรันบล็อกที่ซิงโครไนซ์เดียวกันบนอินสแตนซ์เดียวกันพร้อมกันได้

ซิงโครไนซ์กับอ็อบเจ็กต์ที่คุณต้องการการเข้าถึงแบบเอกสิทธิ์เฉพาะบุคคล (หรือดีกว่าก็คืออ็อบเจ็กต์ที่มีไว้เพื่อปกป้องมัน)


1
ฉันกำลังบอกว่าหากคุณซิงโครไนซ์ในฟิลด์ที่ไม่ใช่ขั้นสุดท้ายคุณควรตระหนักถึงความจริงที่ว่าข้อมูลโค้ดทำงานด้วยการเข้าถึงพิเศษไปยังอ็อบเจ็กต์ที่oอ้างถึงในเวลาที่ถึงบล็อกซิงโครไนซ์ หากออบเจ็กต์ที่oอ้างถึงการเปลี่ยนแปลงเธรดอื่นสามารถเข้ามาและดำเนินการบล็อกโค้ดที่ซิงโครไนซ์ได้
aioobe

43
ฉันไม่เห็นด้วยกับกฎง่ายๆของคุณ - ฉันชอบที่จะซิงโครไนซ์กับวัตถุที่มีจุดประสงค์เพียงอย่างเดียวเพื่อป้องกันสถานะอื่น หากคุณไม่เคยทำอะไรกับวัตถุอื่นนอกจากการล็อกไว้คุณจะรู้แน่นอนว่าไม่มีรหัสอื่นใดสามารถล็อคได้ หากคุณล็อกวัตถุ "ของจริง" ซึ่งมีวิธีการที่คุณเรียกใช้วัตถุนั้นก็สามารถซิงโครไนซ์ด้วยตัวมันเองได้เช่นกันซึ่งจะทำให้เหตุผลเกี่ยวกับการล็อกยากขึ้น
Jon Skeet

9
อย่างที่ฉันพูดในคำตอบของฉันฉันคิดว่าฉันจำเป็นต้องมีเหตุผลอย่างรอบคอบสำหรับฉันทำไมคุณถึงต้องการทำสิ่งนั้น และฉันไม่แนะนำให้ซิงโครไนซ์thisด้วยเช่นกัน - ฉันขอแนะนำให้สร้างตัวแปรสุดท้ายในคลาสเพื่อจุดประสงค์ในการล็อกเท่านั้นซึ่งจะหยุดไม่ให้คนอื่นล็อกอ็อบเจ็กต์เดียวกัน
Jon Skeet

1
นั่นเป็นอีกจุดที่ดีและฉันเห็นด้วย การล็อกตัวแปรที่ไม่ใช่ขั้นสุดท้ายจำเป็นต้องมีเหตุผลอย่างรอบคอบ
aioobe

ฉันไม่แน่ใจเกี่ยวกับปัญหาการมองเห็นหน่วยความจำเกี่ยวกับการเปลี่ยนวัตถุที่ใช้สำหรับการซิงค์ ฉันคิดว่าคุณจะประสบปัญหาใหญ่ในการเปลี่ยนวัตถุและจากนั้นก็อาศัยโค้ดที่เห็นว่าการเปลี่ยนแปลงนั้นถูกต้องเพื่อให้ "ส่วนเดียวกันของโค้ดอาจทำงานควบคู่กัน" ฉันไม่แน่ใจว่ามีอะไรบ้างที่ guaratees จะถูกขยายโดยโมเดลหน่วยความจำเพื่อการมองเห็นหน่วยความจำของฟิลด์ที่ใช้ในการล็อกซึ่งต่างจากตัวแปรที่เข้าถึงภายในบล็อกการซิงค์ หลักการทั่วไปของฉันคือถ้าคุณซิงโครไนซ์บางสิ่งบางอย่างมันควรจะเป็นที่สิ้นสุด
Mike Q

48

จริงๆมันไม่ได้เป็นความคิดที่ดี - เพราะบล็อกตรงกันของคุณจะไม่ได้จริงๆตรงกันในลักษณะที่สอดคล้องกัน

สมมติว่าบล็อกที่ซิงโครไนซ์มีจุดมุ่งหมายเพื่อให้แน่ใจว่าเธรดเดียวเท่านั้นที่เข้าถึงข้อมูลที่แชร์บางส่วนในแต่ละครั้งให้พิจารณา:

  • เธรด 1 เข้าสู่บล็อกที่ซิงโครไนซ์ เย้ - มีสิทธิ์เข้าถึงข้อมูลที่แชร์โดยเฉพาะ ...
  • เธรด 2 เรียก setO ()
  • เธรด 3 (หรือ 2 ... ) เข้าสู่บล็อกที่ซิงโครไนซ์ เอิ๊ก! คิดว่ามีสิทธิ์เข้าถึงข้อมูลที่แชร์ แต่เธรด 1 ยังคงดำเนินต่อไป ...

ทำไมคุณถึงต้องการให้สิ่งนี้เกิดขึ้น? อาจมีสถานการณ์พิเศษบางอย่างที่สมเหตุสมผล ... แต่คุณต้องนำเสนอกรณีการใช้งานที่เฉพาะเจาะจงให้ฉัน (พร้อมกับวิธีบรรเทาสถานการณ์ที่ฉันให้ไว้ข้างต้น) ก่อนที่ฉันจะพอใจ มัน.


2
@aioobe: แต่เธรด 1 ยังคงสามารถเรียกใช้โค้ดบางตัวที่กำลังกลายพันธุ์รายการ (และมักอ้างถึงo) - และส่วนหนึ่งในการดำเนินการเริ่มกลายพันธุ์รายการอื่น วิธีที่จะเป็นความคิดที่ดี? ฉันคิดว่าเราไม่เห็นด้วยโดยพื้นฐานแล้วว่าควรล็อควัตถุที่คุณสัมผัสด้วยวิธีอื่นหรือไม่ ฉันค่อนข้างจะสามารถให้เหตุผลเกี่ยวกับรหัสของฉันได้โดยที่ไม่รู้ว่ารหัสอื่นทำอะไรในแง่ของการล็อก
Jon Skeet

2
@Felype: ดูเหมือนว่าคุณควรถามคำถามที่มีรายละเอียดมากกว่านี้เป็นคำถามแยกต่างหาก - แต่ใช่ฉันมักจะสร้างวัตถุแยกต่างหากเหมือนกับล็อค
Jon Skeet

3
@VitBernatik: ไม่ถ้าเธรด X เริ่มแก้ไขการกำหนดค่าเธรด Y จะเปลี่ยนค่าของตัวแปรที่ซิงโครไนซ์จากนั้นเธรด Z จะเริ่มแก้ไขการกำหนดค่าจากนั้นทั้ง X และ Z จะแก้ไขการกำหนดค่าพร้อมกันซึ่งไม่ดี .
Jon Skeet

1
ในระยะสั้นจะปลอดภัยถ้าเราเสมอประกาศล็อคเช่นวัตถุสุดท้ายถูกต้องหรือไม่
St.Antario

2
@LinkTheProgrammer: "วิธีการซิงโครไนซ์จะซิงโครไนซ์ทุกออบเจ็กต์เดียวในอินสแตนซ์" - ไม่เลย นั่นไม่เป็นความจริงและคุณควรทบทวนความเข้าใจเกี่ยวกับการซิงโครไนซ์
Jon Skeet

13

ฉันเห็นด้วยกับหนึ่งในความคิดเห็นของ John: คุณต้องใช้ตัวล็อกสุดท้ายเสมอในขณะที่เข้าถึงตัวแปรที่ไม่ใช่ขั้นสุดท้ายเพื่อป้องกันความไม่สอดคล้องกันในกรณีที่มีการเปลี่ยนแปลงการอ้างอิงของตัวแปร ดังนั้นไม่ว่าในกรณีใด ๆ และตามกฎข้อแรก:

กฎข้อที่ 1: หากฟิลด์ไม่เป็นที่สิ้นสุดให้ใช้ (ส่วนตัว) ดัมมี่ล็อกสุดท้าย (ส่วนตัว) เสมอ

เหตุผล # 1: คุณล็อคและเปลี่ยนการอ้างอิงของตัวแปรด้วยตัวเอง เธรดอื่นที่รออยู่นอกล็อกที่ซิงโครไนซ์จะสามารถเข้าสู่บล็อกที่มีการป้องกันได้

เหตุผล # 2: คุณล็อคไว้และเธรดอื่นเปลี่ยนการอ้างอิงของตัวแปร ผลลัพธ์จะเหมือนกัน: เธรดอื่นสามารถเข้าสู่บล็อกที่มีการป้องกันได้

แต่เมื่อใช้ดัมมี่ล็อคขั้นสุดท้ายมีปัญหาอีกอย่าง : คุณอาจได้รับข้อมูลที่ไม่ถูกต้องเนื่องจากอ็อบเจ็กต์ที่ไม่ใช่ขั้นสุดท้ายของคุณจะซิงโครไนซ์กับ RAM เมื่อเรียกซิงโครไนซ์ (วัตถุ) เท่านั้น ดังนั้นตามกฎข้อที่สอง:

กฎ # 2: เมื่อล็อควัตถุที่ไม่ใช่ขั้นสุดท้ายคุณจำเป็นต้องทำทั้งสองอย่างเสมอ: การใช้ดัมมี่ล็อคสุดท้ายและการล็อคของวัตถุที่ไม่ใช่ขั้นสุดท้ายเพื่อประโยชน์ในการซิงโครไนซ์ RAM (ทางเลือกเดียวคือการประกาศเขตข้อมูลทั้งหมดของวัตถุว่ามีความผันผวน!)

ล็อกเหล่านี้เรียกอีกอย่างว่า "ล็อกที่ซ้อนกัน" โปรดทราบว่าคุณต้องเรียกพวกเขาในลำดับเดียวกันเสมอมิฉะนั้นคุณจะได้รับล็อกตาย :

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

อย่างที่คุณเห็นฉันเขียนสองล็อคไว้ในบรรทัดเดียวกันโดยตรงเพราะมันอยู่ด้วยกันเสมอ ด้วยวิธีนี้คุณสามารถล็อค 10 รัง:

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

โปรดทราบว่ารหัสนี้จะไม่แตกหากคุณเพิ่งได้รับการล็อกด้านในเช่นเดียวsynchronized (LOCK3)กับเธรดอื่น แต่มันจะพังถ้าคุณโทรในเธรดอื่นสิ่งนี้:

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

มีเพียงวิธีแก้ปัญหาเดียวสำหรับการล็อกที่ซ้อนกันดังกล่าวในขณะที่จัดการฟิลด์ที่ไม่ใช่ขั้นสุดท้าย:

กฎ # 2 - ทางเลือก: ประกาศทุกฟิลด์ของวัตถุว่าระเหยได้ (ฉันจะไม่พูดถึงข้อเสียของการทำเช่นนี้เช่นการป้องกันการจัดเก็บใด ๆ ในแคชระดับ x แม้กระทั่งสำหรับการอ่าน aso ก็ตาม)

ดังนั้น aioobe จึงค่อนข้างถูกต้อง: เพียงใช้ java.util.concurrent หรือเริ่มเข้าใจทุกอย่างเกี่ยวกับการซิงโครไนซ์และทำด้วยตัวเองด้วยการล็อกแบบซ้อน ;)

สำหรับรายละเอียดเพิ่มเติมว่าเหตุใดการซิงโครไนซ์ในฟิลด์ที่ไม่ใช่ขั้นสุดท้ายจึงแตกดูในกรณีทดสอบของฉัน: https://stackoverflow.com/a/21460055/2012947

และสำหรับรายละเอียดเพิ่มเติมว่าทำไมคุณต้องซิงโครไนซ์เลยเนื่องจาก RAM และแคชดูได้ที่นี่: https://stackoverflow.com/a/21409975/2012947


1
ผมคิดว่าคุณต้องห่อหมาของoมีตรงกัน (LOCK) เพื่อสร้าง "เกิดขึ้นมาก่อน" oความสัมพันธ์ระหว่างการตั้งค่าและวัตถุอ่าน ฉันกำลังพูดถึงเรื่องนี้ในคำถามที่คล้ายกันของฉัน: stackoverflow.com/questions/32852464/…
Petrakeas

ฉันใช้ dataObject เพื่อซิงโครไนซ์การเข้าถึงสมาชิก dataObject นั้นผิดอย่างไร? ถ้า dataObject เริ่มชี้ไปที่อื่นฉันต้องการให้มันซิงโครไนซ์กับข้อมูลใหม่เพื่อป้องกันไม่ให้เธรดพร้อมกันแก้ไข มีปัญหาอะไรไหม
Harmen

2

ฉันไม่เห็นคำตอบที่ถูกต้องที่นี่นั่นคือมันเป็นเรื่องที่ดีที่จะทำ

ฉันไม่แน่ใจด้วยซ้ำว่าทำไมมันถึงเป็นคำเตือนไม่มีอะไรผิดปกติ JVM ที่ทำให้แน่ใจว่าคุณจะได้รับบางส่วนกลับวัตถุที่ถูกต้อง (หรือ null) เมื่อคุณอ่านค่าและคุณสามารถประสานได้ที่ใด ๆวัตถุ

ถ้าคุณวางแผนที่จริงการเปลี่ยนแปลงล็อคในขณะที่มันในการใช้งาน (เมื่อเทียบกับเช่นเปลี่ยนจากวิธี init แล้วก่อนที่คุณจะเริ่มใช้งานได้) volatileคุณจะต้องทำให้ตัวแปรที่คุณวางแผนที่จะเปลี่ยนแปลง จากนั้นสิ่งที่คุณต้องทำคือการซิงโครไนซ์ทั้งวัตถุเก่าและวัตถุใหม่และคุณสามารถเปลี่ยนค่าได้อย่างปลอดภัย

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

ที่นั่น ไม่ซับซ้อนการเขียนโค้ดด้วยการล็อก (mutexes) นั้นค่อนข้างง่าย การเขียนโค้ดโดยไม่มีพวกเขา (ล็อครหัสฟรี) เป็นสิ่งที่ยาก


ซึ่งอาจไม่ได้ผล พูดว่า o เริ่มต้นด้วยการอ้างอิงถึง O1 จากนั้นเธรด T1 จะล็อก o (= O1) และ O2 และตั้งค่า o เป็น O2 ในเวลาเดียวกัน Thread T2 จะล็อก O1 และรอให้ T1 ปลดล็อก เมื่อได้รับล็อค O1 มันจะตั้งค่า o เป็น O3 ในสถานการณ์สมมตินี้ระหว่าง T1 ปล่อย O1 และ T2 ล็อก O1 O1 กลายเป็นไม่ถูกต้องสำหรับการล็อกผ่าน o ในขณะนี้เธรดอื่นอาจใช้ o (= O2) สำหรับการล็อกและดำเนินการต่อโดยไม่หยุดชะงักในการแข่งขันกับ T2
GPS

2

แก้ไข: ดังนั้นวิธีแก้ปัญหานี้ (ตามที่แนะนำโดย Jon Skeet) อาจมีปัญหาเกี่ยวกับความเป็นอะตอมของการใช้ "ซิงโครไนซ์ (วัตถุ) {}" ในขณะที่การอ้างอิงวัตถุกำลังเปลี่ยนไป ฉันถามแยกต่างหากและตามที่คุณ erickson ไม่เธรดปลอดภัย - โปรดดู: กำลังเข้าสู่อะตอมบล็อกที่ตรงกันหรือไม่ . งั้นเอามาเป็นตัวอย่างว่าจะไม่ทำ - มีลิงค์ทำไม;)

ดูรหัสว่ามันจะทำงานอย่างไรหากซิงโครไนซ์ () จะเป็นอะตอม:

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}

1
นี่อาจจะโอเค - แต่ฉันไม่รู้ว่ามีการรับประกันในโมเดลหน่วยความจำหรือไม่ว่าค่าที่คุณซิงโครไนซ์เป็นค่าที่เขียนล่าสุด - ฉันไม่คิดว่าจะมีการรับประกันว่าจะ "อ่านและซิงโครไนซ์" แบบอะตอม โดยส่วนตัวแล้วฉันพยายามหลีกเลี่ยงการซิงโครไนซ์บนจอภาพที่มีการใช้งานอื่นอยู่แล้วเพื่อความเรียบง่าย (ด้วยการมีฟิลด์แยกต่างหากรหัสจะถูกต้องอย่างชัดเจนแทนที่จะต้องให้เหตุผลอย่างรอบคอบ)
Jon Skeet

@ จอน. ขอบคุณสำหรับคำตอบ! ฉันได้ยินความกังวลของคุณ ฉันเห็นด้วยสำหรับกรณีนี้การล็อกภายนอกจะหลีกเลี่ยงคำถามเรื่อง "ปรมาณูที่ซิงโครไนซ์" ดังนั้นจะดีกว่า แม้ว่าอาจมีบางกรณีที่คุณต้องการแนะนำในการกำหนดค่ารันไทม์เพิ่มเติมและแชร์การกำหนดค่าที่แตกต่างกันสำหรับกลุ่มเธรดที่แตกต่างกัน (แม้ว่าจะไม่ใช่กรณีของฉันก็ตาม) แล้ววิธีนี้ก็น่าสนใจ ฉันโพสต์คำถามstackoverflow.com/questions/29217266/…ของการซิงโครไนซ์ () atomicity - ดังนั้นเราจะดูว่าสามารถใช้งานได้หรือไม่ (และมีคนตอบกลับ)
Vit Bernatik

2

AtomicReferenceเหมาะกับความต้องการของคุณ

จากเอกสาร java เกี่ยวกับ แพ็คเกจอะตอม :

ชุดเครื่องมือขนาดเล็กของคลาสที่รองรับการเขียนโปรแกรมแบบไม่ล็อคเธรดปลอดภัยในตัวแปรเดียว โดยพื้นฐานแล้วคลาสในแพ็คเกจนี้จะขยายความคิดของค่าระเหยฟิลด์และองค์ประกอบอาร์เรย์ไปยังองค์ประกอบที่ให้การดำเนินการอัปเดตเงื่อนไขอะตอมของแบบฟอร์ม:

boolean compareAndSet(expectedValue, updateValue);

โค้ดตัวอย่าง:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

ในตัวอย่างข้างต้นคุณแทนที่Stringด้วยของคุณเองObject

คำถาม SE ที่เกี่ยวข้อง:

เมื่อใดควรใช้ AtomicReference ใน Java


1

หากoไม่เคยเปลี่ยนแปลงตลอดอายุการใช้งานของอินสแตนซ์Xเวอร์ชันที่สองจะมีสไตล์ที่ดีกว่าโดยไม่คำนึงว่าจะเกี่ยวข้องกับการซิงโครไนซ์หรือไม่

ตอนนี้ไม่ว่าจะมีอะไรผิดปกติกับเวอร์ชันแรกเป็นไปไม่ได้ที่จะตอบโดยไม่รู้ว่ามีอะไรเกิดขึ้นอีกในชั้นเรียนนั้น ฉันมักจะเห็นด้วยกับคอมไพเลอร์ว่ามันดูผิดพลาดได้ง่าย (ฉันจะไม่พูดซ้ำในสิ่งที่คนอื่นพูด)


1

เพียงแค่เพิ่มสองเซ็นต์ของฉัน: ฉันมีคำเตือนนี้เมื่อฉันใช้ส่วนประกอบที่สร้างอินสแตนซ์ผ่านตัวออกแบบดังนั้นฟิลด์จึงไม่สามารถเป็นขั้นสุดท้ายได้เนื่องจากตัวสร้างไม่สามารถรับพารามิเตอร์ได้ กล่าวอีกนัยหนึ่งฉันมีฟิลด์กึ่งสุดท้ายโดยไม่มีคีย์เวิร์ดสุดท้าย

ฉันคิดว่านั่นเป็นเหตุผลว่าทำไมจึงเป็นเพียงคำเตือน: คุณอาจทำอะไรผิดพลาด แต่มันก็อาจจะถูกเช่นกัน

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