Đồng bộ hóa trường không phải cuối cùng


91

Cảnh báo sẽ hiển thị mỗi khi tôi đồng bộ hóa trên một trường lớp không phải cuối cùng. Đây là mã:

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  
     {  
     }  
   }  
 } 

vì vậy tôi đã thay đổi mã theo cách sau:

 public class X  
 {  

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

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

Tôi không chắc đoạn mã trên có phải là cách thích hợp để đồng bộ hóa trên trường lớp không phải cuối cùng hay không. Làm cách nào để đồng bộ hóa trường không phải là trường cuối cùng?

Câu trả lời:


127

Trước hết, tôi khuyến khích bạn thực sự cố gắng giải quyết các vấn đề đồng thời ở mức độ trừu tượng cao hơn, tức là giải quyết nó bằng cách sử dụng các lớp từ java.util.concurrent như ExecutorServices, Callables, Futures, v.v.

Điều đó đang được nói, không có gì sai khi đồng bộ hóa trên một trường không phải là trường cuối cùng. Bạn chỉ cần lưu ý rằng nếu tham chiếu đối tượng thay đổi, cùng một đoạn mã có thể được chạy song song . Tức là, nếu một luồng chạy mã trong khối được đồng bộ hóa và ai đó gọi setO(...), một luồng khác có thể chạy đồng thời cùng một khối được đồng bộ hóa trên cùng một phiên bản .

Đồng bộ hóa trên đối tượng mà bạn cần quyền truy cập độc quyền (hoặc tốt hơn là một đối tượng dành riêng để bảo vệ nó).


1
Tôi đang nói rằng, nếu bạn đồng bộ hóa trên một trường không phải là trường cuối cùng, bạn nên biết thực tế là đoạn mã chạy với quyền truy cập độc quyền vào đối tượng ođược đề cập tại thời điểm đạt đến khối được đồng bộ hóa. Nếu đối tượng ođề cập đến các thay đổi, một luồng khác có thể đến và thực thi khối mã được đồng bộ hóa.
aioobe

42
Tôi không đồng ý với quy tắc ngón tay cái của bạn - Tôi thích đồng bộ hóa trên một đối tượng có mục đích duy nhất là bảo vệ trạng thái khác. Nếu bạn không bao giờ làm bất cứ điều gì với một đối tượng ngoài việc khóa nó, bạn biết chắc rằng không có mã nào khác có thể khóa nó. Nếu bạn khóa một đối tượng "thực" có các phương thức mà bạn gọi sau đó, đối tượng đó cũng có thể đồng bộ hóa trên chính nó, điều này khiến việc lý giải về việc khóa trở nên khó khăn hơn.
Jon Skeet

9
Như tôi đã nói trong câu trả lời của mình, tôi nghĩ rằng tôi cần phải giải thích rất cẩn thận cho tôi, tại sao bạn lại muốn làm điều đó. Và tôi cũng không khuyên bạn nên bật đồng bộ hóa this- tôi khuyên bạn nên tạo một biến cuối cùng trong lớp chỉ cho mục đích khóa , điều này ngăn không cho bất kỳ ai khác khóa trên cùng một đối tượng.
Jon Skeet

1
Đó là một điểm tốt khác, và tôi đồng ý; khóa trên một biến không phải là cuối cùng chắc chắn cần được biện minh cẩn thận.
aioobe

Tôi không chắc về các vấn đề hiển thị bộ nhớ xung quanh việc thay đổi một đối tượng được sử dụng để đồng bộ hóa. Tôi nghĩ rằng bạn sẽ gặp rắc rối lớn khi thay đổi một đối tượng và sau đó dựa vào mã thấy sự thay đổi đó chính xác để "cùng một đoạn mã có thể được chạy song song". Tôi không chắc điều gì, nếu có, các giám sát viên được mô hình bộ nhớ mở rộng khả năng hiển thị trong bộ nhớ của các trường được sử dụng để khóa, trái ngược với các biến được truy cập trong khối đồng bộ. Quy tắc ngón tay cái của tôi là, nếu bạn đồng bộ hóa trên một cái gì đó, nó sẽ là cuối cùng.
Mike Q

47

Đó thực sự không phải là một ý tưởng hay - bởi vì các khối được đồng bộ hóa của bạn không còn thực sự được đồng bộ hóa một cách nhất quán nữa.

Giả sử các khối được đồng bộ hóa có nghĩa là đảm bảo rằng chỉ một luồng truy cập một số dữ liệu được chia sẻ tại một thời điểm, hãy xem xét:

  • Luồng 1 đi vào khối đồng bộ. Yay - nó có quyền truy cập độc quyền vào dữ liệu được chia sẻ ...
  • Luồng 2 cuộc gọi setO ()
  • Luồng 3 (hoặc vẫn là 2 ...) đi vào khối được đồng bộ hóa. Eek! Nó nghĩ rằng nó có quyền truy cập độc quyền vào dữ liệu được chia sẻ, nhưng luồng 1 vẫn đang tiếp tục với nó ...

Tại sao bạn muốn điều này xảy ra? Có thể có một số tình huống rất đặc biệt mà nó có ý nghĩa ... nhưng bạn phải trình bày cho tôi một trường hợp sử dụng cụ thể (cùng với các cách giảm thiểu tình huống mà tôi đã đưa ra ở trên) trước khi tôi hài lòng với nó.


2
@aioobe: Nhưng sau đó luồng 1 vẫn có thể chạy một số mã làm thay đổi danh sách (và thường được đề cập đến o) - và một phần trong quá trình thực thi của nó bắt đầu thay đổi một danh sách khác. Làm thế nào đó sẽ là một ý tưởng tốt? Tôi nghĩ về cơ bản chúng ta không đồng ý về việc khóa các đồ vật bạn chạm vào theo những cách khác có nên hay không. Tôi thà có thể suy luận về mã của mình mà không cần bất kỳ kiến ​​thức nào về những gì mã khác làm về mặt khóa.
Jon Skeet

2
@Felype: Có vẻ như bạn nên hỏi một câu hỏi chi tiết hơn như một câu hỏi riêng biệt - nhưng có, tôi thường tạo các đối tượng riêng biệt giống như ổ khóa.
Jon Skeet

3
@VitBernatik: Không. Nếu luồng X bắt đầu sửa đổi cấu hình, luồng Y thay đổi giá trị của biến đang được đồng bộ hóa, thì luồng Z bắt đầu sửa đổi cấu hình, thì cả X và Z sẽ sửa đổi cấu hình cùng một lúc, điều này thật tệ .
Jon Skeet

1
Tóm lại, sẽ an toàn hơn nếu chúng ta luôn khai báo các đối tượng khóa như vậy sau cùng, đúng không?
St.Antario

2
@LinkTheProgrammer: "Một phương thức được đồng bộ hóa sẽ đồng bộ hóa mọi đối tượng trong trường hợp" - không có đâu. Điều đó chỉ đơn giản là không đúng, và bạn nên xem xét lại hiểu biết của mình về đồng bộ hóa.
Jon Skeet

12

Tôi đồng ý với một trong những nhận xét của John: Bạn phải luôn sử dụng một giả khóa cuối cùng trong khi truy cập một biến không phải là cuối cùng để ngăn chặn sự mâu thuẫn trong trường hợp thay đổi tham chiếu của biến. Vì vậy, trong mọi trường hợp và theo nguyên tắc đầu tiên:

Quy tắc # 1: Nếu một trường không phải là trường cuối cùng, hãy luôn sử dụng giả khóa cuối cùng (riêng tư).

Lý do số 1: Bạn giữ khóa và tự mình thay đổi tham chiếu của biến. Một luồng khác đang đợi bên ngoài khóa được đồng bộ hóa sẽ có thể vào khối được bảo vệ.

Lý do # 2: Bạn giữ khóa và một luồng khác thay đổi tham chiếu của biến. Kết quả là như nhau: Một luồng khác có thể vào khối được bảo vệ.

Nhưng khi sử dụng giả khóa cuối cùng, có một vấn đề khác : Bạn có thể nhận được dữ liệu sai, vì đối tượng không phải cuối cùng của bạn sẽ chỉ được đồng bộ hóa với RAM khi gọi đồng bộ hóa (đối tượng). Vì vậy, như một quy tắc ngón tay cái thứ hai:

Quy tắc # 2: Khi khóa một đối tượng không phải là cuối cùng, bạn luôn cần thực hiện cả hai: Sử dụng giả khóa cuối cùng và khóa của đối tượng không phải cuối cùng để đồng bộ hóa RAM. (Cách thay thế duy nhất sẽ là khai báo tất cả các trường của đối tượng là dễ bay hơi!)

Những ổ khóa này còn được gọi là "ổ khóa lồng nhau". Lưu ý rằng bạn phải gọi chúng luôn theo cùng một thứ tự, nếu không bạn sẽ bị khóa chết :

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...
        }
        }  
    }  
} 

Như bạn có thể thấy, tôi viết hai ổ khóa trực tiếp trên cùng một dòng, bởi vì chúng luôn thuộc về nhau. Như thế này, bạn thậm chí có thể làm 10 khóa lồng nhau:

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

Lưu ý rằng mã này sẽ không bị phá vỡ nếu bạn chỉ có một khóa bên trong giống như synchronized (LOCK3)một chuỗi khác. Nhưng nó sẽ bị hỏng nếu bạn gọi trong một chuỗi khác như thế này:

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

Chỉ có một cách giải quyết xung quanh các khóa lồng nhau như vậy trong khi xử lý các trường không phải cuối cùng:

Quy tắc số 2 - Thay thế: Khai báo tất cả các trường của đối tượng là biến động. (Tôi sẽ không nói ở đây về những bất lợi của việc này, ví dụ như ngăn chặn bất kỳ bộ nhớ nào trong bộ nhớ đệm cấp x ngay cả đối với các lần đọc, aso.)

Vì vậy, do đó aioobe khá đúng: Chỉ cần sử dụng java.util.concurrent. Hoặc bắt đầu hiểu mọi thứ về đồng bộ hóa và tự mình làm điều đó với các ổ khóa lồng nhau. ;)

Để biết thêm chi tiết tại sao đồng bộ hóa trên các trường không phải cuối cùng bị ngắt, hãy xem trường hợp thử nghiệm của tôi: https://stackoverflow.com/a/21460055/2012947

Và để biết thêm chi tiết tại sao bạn cần đồng bộ hóa do RAM và bộ nhớ đệm, hãy xem tại đây: https://stackoverflow.com/a/21409975/2012947


1
Tôi nghĩ rằng bạn phải bọc setter của obằng một đồng bộ hóa (LOCK) để thiết lập mối quan hệ "xảy ra trước" giữa thiết lập và đối tượng đọc o. Tôi đang thảo luận về vấn đề này trong một câu hỏi tương tự của tôi: stackoverflow.com/questions/32852464/…
Petrakeas 29/09/15

Tôi sử dụng dataObject để đồng bộ hóa quyền truy cập vào các thành viên dataObject. Làm thế nào là sai? Nếu dataObject bắt đầu trỏ đến một nơi khác, tôi muốn nó được đồng bộ hóa trên dữ liệu mới để ngăn các luồng đồng thời sửa đổi nó. Bất kỳ vấn đề với điều đó?
Harmen

2

Tôi thực sự không thấy câu trả lời chính xác ở đây, đó là, Làm điều đó là hoàn toàn ổn.

Tôi thậm chí không chắc tại sao đó là một cảnh báo, không có gì sai với nó. JVM đảm bảo rằng bạn lấy lại một số đối tượng hợp lệ (hoặc null) khi bạn đọc một giá trị và bạn có thể đồng bộ hóa trên bất kỳ đối tượng nào .

Nếu bạn dự định thực sự thay đổi khóa trong khi nó đang được sử dụng (thay vì thay đổi nó từ một phương thức init, trước khi bạn bắt đầu sử dụng nó), bạn phải tạo biến mà bạn định thay đổi volatile. Sau đó, tất cả những gì bạn cần làm là đồng bộ hóa trên cả đối tượng cũ và đối tượng mới và bạn có thể thay đổi giá trị một cách an toàn

public volatile Object lock;

...

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

Đây. Nó không phức tạp, viết mã bằng ổ khóa (mutexes) trên thực tế khá dễ dàng. Viết mã mà không có chúng (khóa mã miễn phí) là điều khó khăn.


Điều này có thể không hoạt động. Giả sử o bắt đầu là tham chiếu đến O1, sau đó luồng T1 khóa o (= O1) và O2 và đặt o thành O2. Đồng thời Thread T2 khóa O1 và đợi T1 mở khóa. Khi nó nhận được khóa O1, nó sẽ đặt o thành O3. Trong trường hợp này, giữa T1 giải phóng O1 và T2 khóa O1, O1 trở nên không hợp lệ để khóa qua o. Tại thời điểm này, một luồng khác có thể sử dụng o (= O2) để khóa và tiếp tục không bị gián đoạn trong cuộc đua với T2.
GPS

2

CHỈNH SỬA: Vì vậy, giải pháp này (theo đề xuất của Jon Skeet) có thể có vấn đề với tính nguyên tử của việc triển khai "đồng bộ hóa (đối tượng) {}" trong khi tham chiếu đối tượng đang thay đổi. Tôi đã hỏi riêng và theo ông erickson nó không phải là luồng an toàn - hãy xem: Nhập nguyên tử khối đồng bộ hóa? . Vì vậy, hãy lấy nó làm ví dụ về cách KHÔNG làm điều đó - với các liên kết tại sao;)

Xem mã sẽ hoạt động như thế nào nếu sync () sẽ là nguyên tử:

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
Điều này thể ổn - nhưng tôi không biết liệu có bất kỳ đảm bảo nào trong mô hình bộ nhớ rằng giá trị mà bạn đồng bộ hóa là giá trị được viết gần đây nhất hay không - Tôi không nghĩ có bất kỳ đảm bảo nào về việc "đọc và đồng bộ hóa" về mặt nguyên tử. Cá nhân tôi cố gắng tránh đồng bộ hóa trên các màn hình có các mục đích sử dụng khác, vì đơn giản. (Bởi có một lĩnh vực riêng biệt, mã trở nên rõ ràng đúng thay vì phải suy luận về nó một cách cẩn thận.)
Jon Skeet

@Jon. Thx cho câu trả lời! Tôi nghe thấy sự lo lắng của bạn. Tôi đồng ý cho trường hợp này, khóa bên ngoài sẽ tránh được câu hỏi về "tính nguyên tử đồng bộ". Như vậy sẽ là tốt hơn. Mặc dù có thể có trường hợp bạn muốn giới thiệu thêm cấu hình trong thời gian chạy và chia sẻ cấu hình khác nhau cho các nhóm luồng khác nhau (mặc dù đó không phải là trường hợp của tôi). Và sau đó giải pháp này có thể trở nên thú vị. Tôi đăng câu hỏi stackoverflow.com/questions/29217266/... của đồng bộ số nguyên tử () - vì vậy chúng tôi sẽ xem nếu nó có thể được sử dụng (và là người trả lời)
Vit Bernatik

2

AtomicReference phù hợp với yêu cầu của bạn.

Từ tài liệu java về gói nguyên tử :

Một bộ công cụ nhỏ gồm các lớp hỗ trợ lập trình an toàn luồng không khóa trên các biến đơn. Về bản chất, các lớp trong gói này mở rộng khái niệm về giá trị dễ bay hơi, trường và phần tử mảng cho những phần tử cũng cung cấp hoạt động cập nhật có điều kiện nguyên tử của biểu mẫu:

boolean compareAndSet(expectedValue, updateValue);

Mã mẫu:

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);

Trong ví dụ trên, bạn thay thế StringbằngObject

Câu hỏi SE có liên quan:

Khi nào sử dụng AtomicReference trong Java?


1

Nếu okhông bao giờ thay đổi trong suốt thời gian tồn tại của một Xphiên bản, thì phiên bản thứ hai sẽ có kiểu dáng tốt hơn bất kể có tham gia đồng bộ hóa hay không.

Bây giờ, liệu có gì sai với phiên bản đầu tiên hay không thì không thể trả lời nếu không biết những gì khác đang diễn ra trong lớp đó. Tôi có xu hướng đồng ý với trình biên dịch rằng nó trông dễ bị lỗi (tôi sẽ không lặp lại những gì những người khác đã nói).


1

Chỉ cần thêm hai xu của tôi: Tôi đã có cảnh báo này khi tôi sử dụng thành phần được khởi tạo thông qua trình thiết kế, vì vậy trường của nó thực sự không thể là cuối cùng, vì hàm tạo không thể nhận tham số. Nói cách khác, tôi có trường gần như cuối cùng mà không có từ khóa cuối cùng.

Tôi nghĩ đó là lý do tại sao nó chỉ là cảnh báo: bạn có thể đang làm điều gì đó sai, nhưng nó cũng có thể đúng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.