ทำไมต้องรอ () อยู่ในบล็อกที่ซิงโครไนซ์เสมอ


257

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

อะไรคือความเสียหายที่อาจเกิดขึ้นหากเป็นไปได้ที่จะเรียกใช้wait()นอกบล็อกที่ซิงโครไนซ์โดยรักษาความหมายไว้ - ระงับเธรดผู้โทร?

คำตอบ:


232

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

wait()ความหมายที่คุณไม่เคยเป็นเพียงแค่ คุณจำเป็นต้องมีเงื่อนไขบางอย่างที่จะ satsified และถ้าไม่ได้คุณรอจนกว่ามันจะเป็น ดังนั้นสิ่งที่คุณทำจริงๆคือ

if(!condition){
    wait();
}

แต่เงื่อนไขจะถูกกำหนดโดยเธรดแยกต่างหากดังนั้นเพื่อให้งานนี้ถูกต้องคุณต้องซิงโครไนซ์

มีสิ่งผิดปกติเพิ่มขึ้นอีกสองสามข้อเนื่องจากการที่เธรดหยุดรอไม่ได้หมายความว่าคุณกำลังมองหาสิ่งที่เป็นจริง:

  • คุณสามารถรับสัญญาณเตือนปลอม (ซึ่งหมายความว่าเธรดสามารถตื่นจากการรอโดยไม่ได้รับการแจ้งเตือน) หรือ

  • เงื่อนไขสามารถถูกตั้งค่าได้ แต่เธรดที่สามสร้างเงื่อนไขที่ผิดพลาดอีกครั้งโดยที่เธรดที่รอคอยกลับมาทำงานอีกครั้ง (และเรียกคืนจอภาพอีกครั้ง)

ในการจัดการกับกรณีเหล่านี้สิ่งที่คุณต้องการจริงๆคือการเปลี่ยนแปลงของสิ่งนี้เสมอ :

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

ยังดีกว่าอย่ายุ่งกับการซิงโครไนซ์ดั้งเดิมและทำงานกับ abstractions ที่เสนอในjava.util.concurrentแพ็คเกจ


3
มีการอภิปรายรายละเอียดที่นี่เช่นกันโดยพูดถึงสิ่งเดียวกัน coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/?hl=th

1
btw ถ้าคุณไม่ละเว้นการตั้งค่าสถานะถูกขัดจังหวะลูปจะตรวจสอบThread.interrupted()เช่นกัน
bestsss

2
ฉันยังสามารถทำสิ่งต่าง ๆ เช่น: ในขณะที่ (! เงื่อนไข) {ทำข้อมูลให้ตรงกัน (นี้) {รอ ();}} ซึ่งหมายความว่ายังคงมีการแข่งขันระหว่างการตรวจสอบสภาพและรอแม้ว่าการรอ () จะเรียกอย่างถูกต้องในบล็อกที่ซิงโครไนซ์ ดังนั้นมีเหตุผลอื่น ๆ ที่อยู่เบื้องหลังข้อ จำกัด นี้อาจเป็นเพราะวิธีการใช้งานใน Java?
shrini1000

9
อีกสถานการณ์ที่น่ารังเกียจ: เงื่อนไขเป็นเท็จเรากำลังจะเข้าสู่การรอ () จากนั้นเธรดอื่นจะเปลี่ยนเงื่อนไขและเรียกใช้แจ้งเตือน () เนื่องจากเรายังไม่รอ () เราจะพลาดการแจ้งเตือนนี้ () ในคำอื่น ๆ การทดสอบและการรอคอยเช่นเดียวกับการเปลี่ยนแปลงและการแจ้งจะต้องเป็นอะตอม

1
@Nullpointer: หากเป็นประเภท aa ที่สามารถเขียนเป็นอะตอม (เช่นบูลีนโดยนัยโดยใช้โดยตรงในประโยคข้อ) และไม่มีการพึ่งพาซึ่งกันและกันกับข้อมูลที่ใช้ร่วมกันอื่น ๆ คุณสามารถหลีกเลี่ยงการประกาศความผันผวน แต่คุณต้องใช้วิธีนั้นหรือทำการซิงโครไนซ์เพื่อให้แน่ใจว่าการอัพเดทจะปรากฏในเธรดอื่นโดยทันที
Michael Borgwardt

283

อะไรคือความเสียหายที่อาจเกิดขึ้นหากเป็นไปได้ที่จะเรียกใช้wait()นอกบล็อกที่ซิงโครไนซ์โดยรักษาความหมายไว้ - ระงับเธรดผู้โทร?

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

สมมติว่าเราใช้คิวการบล็อก (ฉันรู้ว่ามีอยู่แล้วใน API :)

ความพยายามครั้งแรก (โดยไม่มีการซิงโครไนซ์) อาจดูบางสิ่งตามบรรทัดด้านล่าง

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

นี่คือสิ่งที่อาจเกิดขึ้น:

  1. ด้ายผู้บริโภคเรียกร้องและเห็นว่าtake()buffer.isEmpty()

  2. ก่อนที่เธรดผู้บริโภคจะดำเนินการต่อไปwait()เธรดผู้ผลิตจะเข้ามาและเรียกใช้งานแบบเต็มgive()นั่นคือbuffer.add(data); notify();

  3. ด้ายของผู้บริโภคในขณะนี้จะเรียกwait()(และพลาดnotify()ที่ถูกเรียกว่าเพียงแค่)

  4. หากโชคร้ายเธรดผู้ผลิตจะไม่ผลิตมากขึ้นgive()เนื่องจากข้อเท็จจริงที่ว่าเธรดผู้บริโภคไม่เคยตื่นขึ้นมาและเรามีระบบล็อคแบบตาย

เมื่อคุณเข้าใจปัญหาแล้ววิธีแก้ไขก็ชัดเจน: ใช้synchronizedเพื่อให้แน่ใจว่าnotifyจะไม่ถูกเรียกระหว่างisEmptyและwaitและ

โดยไม่ต้องลงรายละเอียด: ปัญหาการประสานนี้เป็นสากล ดังที่ Michael Borgwardt ชี้ให้เห็นการรอ / แจ้งนั้นเป็นเรื่องเกี่ยวกับการสื่อสารระหว่างเธรดดังนั้นคุณจะต้องจบลงด้วยสภาพการแข่งขันคล้ายกับที่อธิบายไว้ข้างต้น นี่คือเหตุผลที่มีการบังคับใช้กฎ "รอเฉพาะภายในที่ซิงโครไนซ์" เท่านั้น


ย่อหน้าจากลิงก์ที่โพสต์โดย @Willieสรุปว่าค่อนข้างดี:

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

buffer.isEmpty()คำกริยาที่ผู้ผลิตและผู้บริโภคจำเป็นต้องตกลงอยู่ในตัวอย่างข้างต้น และข้อตกลงดังกล่าวได้รับการแก้ไขโดยการทำให้มั่นใจว่าการรอและการแจ้งเตือนนั้นดำเนินการเป็นsynchronizedบล็อก


โพสต์นี้ถูกเขียนใหม่เป็นบทความที่นี่: Java: เหตุใดจึงต้องรอในบล็อกที่ซิงค์


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

ที่น่าสนใจ แต่โปรดทราบว่าการโทรไปที่ซิงโครไนซ์จริงๆแล้วจะไม่สามารถแก้ปัญหาดังกล่าวได้เนื่องจากลักษณะของ "ไม่น่าเชื่อถือ" ของการรอ () และแจ้ง () อ่านเพิ่มเติมได้ที่นี่: stackoverflow.com/questions/21439355/… . เหตุผลที่จำเป็นต้องมีการซิงโครไนซ์อยู่ในสถาปัตยกรรมฮาร์ดแวร์ (ดูคำตอบของฉันด้านล่าง)
มาร์คัส

แต่ถ้าเพิ่มreturn buffer.remove();ในขณะที่บล็อก แต่หลังจากwait();นั้นมันใช้งานได้?
BobJiang

@ BobJiang, ไม่, เธรดสามารถถูกปลุกด้วยเหตุผลอื่นที่ไม่ใช่คนที่เรียกให้ กล่าวอีกนัยหนึ่งบัฟเฟอร์อาจว่างเปล่าแม้หลังจากwaitส่งคืนแล้ว
aioobe

ฉันมีเพียงแค่Thread.currentThread().wait();ในฟังก์ชั่นที่ล้อมรอบด้วยลองจับmain InterruptedExceptionโดยไม่ต้องบล็อกมันทำให้ฉันยกเว้นเดียวกันsynchronized IllegalMonitorStateExceptionอะไรทำให้มันถึงสถานะที่ผิดกฎหมายในตอนนี้? มันทำงานภายในsynchronizedบล็อกแม้ว่า
Shashwat

12

@Rollerball ถูกต้อง wait()เรียกว่าเพื่อให้ด้ายสามารถรอให้สภาพบางอย่างที่จะเกิดขึ้นเมื่อนี้wait()โทรที่เกิดขึ้นด้ายถูกบังคับให้ขึ้นล็อค
ในการยอมแพ้คุณต้องเป็นเจ้าของมันก่อน เธรดต้องเป็นเจ้าของล็อคก่อน ดังนั้นความต้องการที่จะเรียกมันว่าภายในsynchronizedเมธอด / บล็อก

ใช่ฉันเห็นด้วยกับคำตอบทั้งหมดข้างต้นเกี่ยวกับความเสียหาย / ความไม่สอดคล้องที่อาจเกิดขึ้นหากคุณไม่ได้ตรวจสอบสภาพภายในsynchronizedวิธี / บล็อก อย่างไรก็ตามเนื่องจาก @ shrini1000 ชี้ให้เห็นว่าการโทรwait()ภายในบล็อกที่ซิงโครไนซ์จะช่วยป้องกันความไม่สอดคล้องนี้

นี่คือการอ่านที่ดี ..


5
@Popeye อธิบาย 'ถูกต้อง' อย่างเหมาะสม ความคิดเห็นของคุณไม่มีประโยชน์กับใครเลย
มาร์ควิสแห่ง Lorne

4

ปัญหาที่อาจเกิดขึ้นหากคุณไม่ซิงโครไนซ์มาก่อนwait()ดังต่อไปนี้:

  1. หากเธรดที่ 1 เข้าสู่makeChangeOnX()และตรวจสอบเงื่อนไข while และเป็นtrue( x.metCondition()ส่งคืนfalseหมายความว่าx.conditionมีค่าfalse) ดังนั้นมันจะเข้าสู่ภายใน แล้วก็ก่อนที่จะwait()วิธีหัวข้ออื่นไปsetConditionToTrue()และกำหนดx.conditionไปและtruenotifyAll()
  2. จากนั้นหลังจากนั้นเธรดที่ 1 จะเข้าสู่wait()วิธีการของเขา(ไม่ได้รับผลกระทบจากเหตุการณ์notifyAll()ที่เกิดขึ้นเมื่อไม่นานมานี้) ในกรณีนี้เธรดที่ 1 จะรอให้เธรดอื่นดำเนินการอยู่setConditionToTrue()แต่นั่นอาจไม่เกิดขึ้นอีก

แต่ถ้าคุณใส่synchronizedก่อนหน้าวิธีการที่เปลี่ยนสถานะวัตถุสิ่งนี้จะไม่เกิดขึ้น

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

เราทุกคนรู้ว่าวิธีการรอ (), แจ้งเตือน () และ alertAll () ใช้สำหรับการสื่อสารระหว่างเธรด เพื่อที่จะกำจัดสัญญาณที่ไม่ได้รับและปัญหาการตื่นขึ้นโดยไม่ตั้งใจเธรดที่รอจะรออยู่เสมอในบางสภาวะ เช่น-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

จากนั้นการแจ้งชุดเธรดคือตัวแปรที่แจ้งถึงจริงและแจ้งให้ทราบ

ทุกเธรดมีแคชในตัวเครื่องเพื่อให้การเปลี่ยนแปลงทั้งหมดได้รับการเขียนครั้งแรกจากนั้นเลื่อนไปยังหน่วยความจำหลักทีละน้อย

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

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

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

ขอบคุณหวังว่ามันชัดเจน


1

โดยทั่วไปเกี่ยวข้องกับสถาปัตยกรรมฮาร์ดแวร์ (เช่นRAMและแคช )

หากคุณไม่ได้ใช้synchronizedร่วมกับwait()หรือnotify()เธรดอื่นสามารถเข้าสู่บล็อกเดียวกันแทนที่จะรอให้จอภาพเข้าสู่ ยิ่งไปกว่านั้นเมื่อเช่นการเข้าถึงอาเรย์ที่ไม่มีบล็อกที่ซิงโครไนซ์เธรดอื่นอาจไม่เห็นการเปลี่ยนแปลงของมัน ... อันที่จริงเธรดอื่นจะไม่เห็นการเปลี่ยนแปลงใด ๆเมื่อมันมีสำเนาของอาเรย์ในแคชระดับ x แล้ว aka แคชระดับ 1 / 2nd / 3rd) ของเธรดการจัดการ CPU core

แต่บล็อกที่ซิงโครไนซ์เป็นเพียงด้านเดียวของเหรียญ: ถ้าคุณเข้าถึงวัตถุภายในบริบทที่ซิงโครไนซ์จากบริบทที่ไม่ซิงโครไนซ์วัตถุนั้นจะไม่ถูกซิงโครไนซ์แม้จะอยู่ในบล็อกที่ซิงโครไนซ์ วัตถุในแคชของมัน ฉันเขียนเกี่ยวกับปัญหานี้ที่นี่: https://stackoverflow.com/a/21462631และเมื่อการล็อกมีวัตถุที่ไม่สิ้นสุดการอ้างอิงของวัตถุจะยังคงสามารถเปลี่ยนแปลงได้โดยเธรดอื่นหรือไม่

นอกจากนี้ฉันเชื่อว่าแคชระดับ x มีหน้าที่รับผิดชอบข้อผิดพลาดรันไทม์ที่ไม่สามารถทำซ้ำได้ส่วนใหญ่ นั่นเป็นเพราะนักพัฒนามักจะไม่เรียนรู้สิ่งต่าง ๆ ในระดับต่ำเช่นวิธีการทำงานของ CPU หรือลำดับชั้นของหน่วยความจำมีผลต่อการทำงานของแอปพลิเคชัน: http://en.wikipedia.org/wiki/Memory_hierarchy

ยังคงเป็นปริศนาว่าทำไมคลาสการเขียนโปรแกรมไม่เริ่มต้นด้วยลำดับชั้นหน่วยความจำและสถาปัตยกรรม CPU ก่อน "Hello world" จะไม่ช่วยที่นี่ ;)


1
เพิ่งค้นพบเว็บไซต์ที่อธิบายได้อย่างสมบูรณ์แบบและเจาะลึก: javamex.com/tutorials/…
Marcus

อืม .. ไม่แน่ใจว่าฉันติดตาม หากการแคชเป็นเหตุผลเดียวในการรอและแจ้งเตือนภายในการซิงโครไนซ์เหตุใดการซิงโครไนซ์จึงอยู่ในการดำเนินการรอ / แจ้งเตือน
aioobe

เป็นคำถามที่ดีเนื่องจากวิธีการซิงโครไนซ์รอ / แจ้งเป็นอย่างดี ... บางทีนักพัฒนา Java ในอดีตของ Sun อาจจะรู้คำตอบ? ลองเข้าไปดูลิงค์ด้านบนหรืออาจจะช่วยคุณได้: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

เหตุผลอาจเป็น: ในวันแรก ๆ ของ Java ไม่มีข้อผิดพลาดในการคอมไพล์เมื่อไม่เรียกการซิงโครไนซ์ก่อนทำการดำเนินการมัลติเธรดเหล่านี้ แต่มีข้อผิดพลาดรันไทม์เท่านั้น (เช่นcoderanch.com/t/239491/java-programmer-SCJP/certification/ … ) บางทีพวกเขาคิดว่า @SUN จริง ๆ ว่าเมื่อโปรแกรมเมอร์ได้รับข้อผิดพลาดเหล่านี้พวกเขากำลังได้รับการติดต่อซึ่งอาจทำให้พวกเขามีโอกาสขายเซิร์ฟเวอร์ของพวกเขามากขึ้น มันเปลี่ยนไปเมื่อไหร่ อาจจะเป็น Java 5.0 หรือ 6.0 แต่จริงๆแล้วฉันจำไม่ได้ว่าเป็นคนซื่อสัตย์ ...
Marcus

TBH ผมเห็นปัญหาน้อย ๆ กับคำตอบของคุณ 1) ประโยคที่สองของคุณไม่ได้ทำให้รู้สึก: มันไม่สำคัญซึ่งวัตถุด้ายมีล็อค ไม่ว่าวัตถุสองเธรดที่ซิงโครไนซ์กันจะเห็นการเปลี่ยนแปลงทั้งหมด 2) คุณพูดอีกกระทู้"จะไม่"เห็นการเปลี่ยนแปลงใด ๆ นี้ควรจะเป็น"อาจจะไม่ได้" 3) ฉันไม่รู้ว่าทำไมคุณถึงนำแคชในระดับที่ 1/2/3 ... เรื่องสำคัญนี่คือสิ่งที่ Java Memory Model พูดและที่ระบุใน JLS ในขณะที่สถาปัตยกรรมฮาร์ดแวร์อาจช่วยในการทำความเข้าใจว่าทำไม JLS บอกว่ามันทำอะไร แต่เป็นการพูดที่ไม่เกี่ยวข้องในบริบทนี้อย่างเคร่งครัด
aioobe

0

โดยตรงจากการกวดวิชา Oracle oracle นี้ :

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


จากคำถามที่ผู้เขียนทำดูเหมือนว่าผู้เขียนคำถามมีความเข้าใจที่ชัดเจนของสิ่งที่ฉันยกมาจากการกวดวิชาและยิ่งกว่านั้นคำตอบของฉันอธิบาย "ทำไม"
Rollerball

0

เมื่อคุณเรียกใช้การแจ้งเตือน () จากวัตถุ t, java จะแจ้งเมธอด t.wait () เฉพาะ แต่จาวาค้นหาและแจ้งวิธีการรอได้อย่างไร

java มองเฉพาะในบล็อกซิงโครไนซ์ของรหัสซึ่งถูกล็อคโดยวัตถุ t java ไม่สามารถค้นหารหัสทั้งหมดเพื่อแจ้งเตือนเฉพาะ t.wait ()


0

ตามเอกสาร:

เธรดปัจจุบันต้องเป็นเจ้าของจอภาพของวัตถุนี้ เธรดปล่อยความเป็นเจ้าของของจอภาพนี้

wait()วิธีการก็หมายความว่ามันปล่อยล็อคบนวัตถุ ดังนั้นวัตถุจะถูกล็อคเฉพาะภายในบล็อก / วิธีการทำข้อมูลให้ตรงกัน หากเธรดอยู่นอกบล็อกการซิงค์หมายความว่าไม่ได้ล็อคถ้าไม่ล็อคคุณจะปล่อยสิ่งใดบนวัตถุ


0

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

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

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.