Tránh đồng bộ hóa (cái này) trong Java?


381

Bất cứ khi nào một câu hỏi xuất hiện trên SO về đồng bộ hóa Java, một số người rất muốn chỉ ra rằng synchronized(this)nên tránh. Thay vào đó, họ tuyên bố, một khóa trên một tài liệu tham khảo riêng tư sẽ được ưu tiên.

Một số lý do được đưa ra là:

Những người khác, bao gồm cả tôi, cho rằng đó synchronized(this)là một thành ngữ được sử dụng rất nhiều (cũng trong các thư viện Java), là an toàn và được hiểu rõ. Không nên tránh vì bạn có lỗi và bạn không biết gì về những gì đang diễn ra trong chương trình đa luồng của mình. Nói cách khác: nếu nó được áp dụng, sau đó sử dụng nó.

Tôi thích xem một số ví dụ trong thế giới thực (không có nội dung foobar) trong đó tránh việc khóa trên thislà thích hợp hơn khi synchronized(this)cũng sẽ thực hiện công việc.

Do đó: bạn có nên luôn luôn tránh synchronized(this)và thay thế nó bằng một khóa trên một tài liệu tham khảo riêng tư?


Một số thông tin khác (cập nhật dưới dạng câu trả lời được đưa ra):

  • chúng ta đang nói về đồng bộ hóa cá thể
  • cả ẩn ( synchronizedphương thức) và hình thức rõ ràng synchronized(this)đều được xem xét
  • nếu bạn trích dẫn Bloch hoặc các cơ quan khác về chủ đề này, đừng bỏ qua những phần bạn không thích (ví dụ: Java hiệu quả, mục về An toàn chủ đề: Thông thường, đó là khóa trên chính cá thể, nhưng vẫn có ngoại lệ.)
  • nếu bạn cần độ chi tiết trong khóa của bạn ngoài việc synchronized(this)cung cấp, thì synchronized(this)không thể áp dụng được nên đó không phải là vấn đề

4
Tôi cũng muốn chỉ ra rằng bối cảnh rất quan trọng - bit "Thông thường nó là khóa trên chính cá thể" nằm trong một phần về tài liệu về một lớp an toàn luồng có điều kiện, khi bạn đặt khóa công khai. Nói cách khác, câu đó được áp dụng khi bạn đã đưa ra quyết định này.
Jon Skeet

Trong trường hợp không có đồng bộ nội bộ và khi cần đồng bộ bên ngoài, khóa thường là bản thân, Bloch nói về cơ bản. Vậy tại sao điều này không xảy ra đối với đồng bộ nội bộ với khóa trên 'cái này'? (Tầm quan trọng của tài liệu là một vấn đề khác.)
eljenso

Có một sự đánh đổi giữa độ chi tiết mở rộng và bộ đệm CPU thêm và yêu cầu bus, vì việc khóa đối tượng bên ngoài rất có thể sẽ yêu cầu một dòng bộ đệm riêng biệt được sửa đổi và trao đổi giữa bộ đệm CPU (xem MESIF và MOESI).
ArtemGr

1
Tôi nghĩ rằng, trong thế giới lập trình phòng thủ, bạn ngăn chặn các lỗi không phải bằng thành ngữ mà bằng mã. Khi ai đó hỏi tôi một câu hỏi, "Đồng bộ hóa của bạn được tối ưu hóa như thế nào?", Tôi muốn nói "Rất" thay vì "Rất, trừ khi người khác không tuân theo thành ngữ".
Sgene9

Câu trả lời:


132

Tôi sẽ bao gồm từng điểm riêng biệt.

  1. Một số mã xấu có thể đánh cắp khóa của bạn (rất phổ biến mã này, cũng có một biến thể "vô tình")

    Tôi vô tình lo lắng hơn . Những gì nó được sử dụng là việc sử dụng thisnày là một phần của giao diện tiếp xúc với lớp của bạn và nên được ghi lại. Đôi khi khả năng của mã khác để sử dụng khóa của bạn là mong muốn. Điều này đúng với những thứ như Collections.synchronizedMap(xem javadoc).

  2. Tất cả các phương thức được đồng bộ hóa trong cùng một lớp đều sử dụng cùng một khóa chính xác, giúp giảm thông lượng

    Đây là suy nghĩ quá đơn giản; chỉ cần thoát khỏi synchronized(this)sẽ không giải quyết vấn đề. Đồng bộ hóa thích hợp cho thông lượng sẽ mất nhiều suy nghĩ hơn.

  3. Bạn (không cần thiết) tiết lộ quá nhiều thông tin

    Đây là một biến thể của # 1. Sử dụng synchronized(this)là một phần của giao diện của bạn. Nếu bạn không muốn / cần điều này tiếp xúc, đừng làm điều đó.


1. "đã đồng bộ hóa" không phải là một phần của giao diện được hiển thị của lớp bạn. 2. đồng ý 3. xem 1.
eljenso

66
Về cơ bản được đồng bộ hóa (điều này) được phơi bày vì điều đó có nghĩa là mã bên ngoài có thể ảnh hưởng đến hoạt động của lớp của bạn. Vì vậy, tôi khẳng định rằng bạn phải ghi lại nó dưới dạng giao diện, ngay cả khi ngôn ngữ không.
Darron

15
Giống. Xem Javadoc for Collections.syn syncizedMap () - đối tượng được trả về sử dụng đồng bộ hóa (cái này) bên trong và họ hy vọng người tiêu dùng sẽ tận dụng điều đó để sử dụng cùng một khóa cho các hoạt động nguyên tử quy mô lớn như lặp.
Darron

3
Trong thực tế Collections.syn syncizedMap () KHÔNG sử dụng đồng bộ hóa (cái này) bên trong, nó sử dụng một đối tượng khóa riêng cuối cùng.
Bas Leijdekkers

1
@Bas Leijdekkers: tài liệu chỉ định rõ ràng rằng việc đồng bộ hóa xảy ra trên thể hiện bản đồ được trả về. Điều thú vị là, các chế độ xem được trả về keySet()values()không khóa (của chúng) this, nhưng đối tượng bản đồ, điều quan trọng là có được hành vi nhất quán cho tất cả các hoạt động của bản đồ. Lý do, đối tượng khóa được đưa vào một biến, là, lớp con SynchronizedSortedMapcần nó để thực hiện các bản đồ con khóa trên thể hiện bản đồ gốc.
Holger

86

Vâng, trước tiên cần phải chỉ ra rằng:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

tương đương về mặt ngữ nghĩa với:

public synchronized void blah() {
  // do stuff
}

đó là lý do không sử dụng synchronized(this). Bạn có thể lập luận rằng bạn có thể làm những thứ xung quanh synchronized(this)khối. Lý do thông thường là để thử và tránh phải thực hiện kiểm tra đồng bộ hóa, điều này dẫn đến tất cả các loại vấn đề tương tranh, đặc biệt là vấn đề khóa kiểm tra kép , điều này cho thấy việc kiểm tra tương đối khó khăn đến mức nào chủ đề an toàn.

Khóa riêng là một cơ chế phòng thủ, không bao giờ là một ý tưởng tồi.

Ngoài ra, như bạn đã đề cập, khóa riêng có thể kiểm soát mức độ chi tiết. Một tập hợp các hoạt động trên một đối tượng có thể hoàn toàn không liên quan đến một đối tượng khác nhưng synchronized(this)sẽ loại trừ lẫn nhau quyền truy cập vào tất cả chúng.

synchronized(this) thực sự không cung cấp cho bạn bất cứ điều gì.


4
"được đồng bộ hóa (điều này) thực sự không cung cấp cho bạn bất cứ điều gì." Ok, tôi thay thế nó bằng một đồng bộ hóa (myPrivateFinalLock). Điều đó mang lại cho tôi những gì? Bạn nói về nó là một cơ chế phòng thủ. Tôi được bảo vệ chống lại cái gì?
eljenso

14
Bạn được bảo vệ chống lại việc vô tình (hoặc độc hại) khóa 'cái này' bởi các đối tượng bên ngoài.
cletus

14
Tôi hoàn toàn không đồng ý với câu trả lời này: khóa phải luôn được giữ trong khoảng thời gian ngắn nhất có thể và đó chính xác là lý do tại sao bạn muốn "làm công cụ" xung quanh một khối được đồng bộ hóa thay vì đồng bộ hóa toàn bộ phương pháp .
Olivier

5
Làm những thứ bên ngoài khối đồng bộ luôn có thiện chí. Vấn đề là mọi người thường mắc phải lỗi này rất nhiều lần và thậm chí không nhận ra nó, giống như trong vấn đề khóa được kiểm tra hai lần. Đường dẫn đến địa ngục được lót đường bởi các ý định tốt.
cletus

16
Tôi không đồng ý chung với "X là một cơ chế phòng thủ, đó không bao giờ là một ý tưởng tồi." Có rất nhiều mã cồng kềnh không cần thiết ngoài kia vì thái độ này.
vây

54

Trong khi bạn đang sử dụng đồng bộ hóa (cái này), bạn đang sử dụng thể hiện của lớp như là một khóa. Điều này có nghĩa là trong khi khóa được lấy bởi luồng 1 , thì luồng 2 sẽ chờ.

Giả sử mã sau đây:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

Phương pháp 1 sửa đổi biến a và phương pháp 2 sửa đổi biến b , nên tránh sửa đổi đồng thời của cùng một biến bởi hai luồng và đó là. NHƯNG trong khi thread1 sửa đổi athread2 sửa đổi b nó có thể được thực hiện mà không có bất kỳ điều kiện cuộc đua.

Thật không may, đoạn mã trên sẽ không cho phép điều này vì chúng tôi đang sử dụng cùng một tham chiếu cho khóa; Điều này có nghĩa là các luồng ngay cả khi chúng không ở trong điều kiện cuộc đua nên chờ đợi và rõ ràng mã hy sinh đồng thời của chương trình.

Giải pháp là sử dụng 2 khóa khác nhau cho hai biến khác nhau:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

Ví dụ trên sử dụng nhiều khóa hạt mịn hơn (thay vào đó là 2 khóa ( lockAlockB cho các biến ab ) và kết quả là cho phép đồng thời tốt hơn, mặt khác, nó trở nên phức tạp hơn so với ví dụ đầu tiên ...


Điều này rất nguy hiểm. Bây giờ bạn đã giới thiệu một yêu cầu đặt hàng khóa phía khách hàng (người dùng của lớp này). Nếu hai luồng đang gọi phương thức1 () và phương thức 2 () theo một thứ tự khác, chúng có khả năng bị bế tắc, nhưng người dùng của lớp này không biết rằng đây là trường hợp.
daveb

7
Độ chi tiết không được cung cấp bởi "đồng bộ hóa (này)" nằm ngoài phạm vi câu hỏi của tôi. Và không phải trường khóa của bạn là cuối cùng?
eljenso

10
để có sự bế tắc, chúng ta nên thực hiện một cuộc gọi từ khối được đồng bộ hóa bởi A đến khối được đồng bộ hóa bởi B. daveb, bạn đã nhầm ...
Andreas Bakurov

3
Không có bế tắc trong ví dụ này như tôi có thể thấy. Tôi chấp nhận nó chỉ là giả nhưng tôi sẽ sử dụng một trong những triển khai của java.util.concurrent.locks.Lock như java.util.concurrent.locks.ReentrantLock
Shawn Vader

15

Trong khi tôi đồng ý về việc không tuân thủ một cách mù quáng các quy tắc giáo điều, liệu kịch bản "ăn cắp khóa" có vẻ quá lập dị với bạn? Một luồng thực sự có thể có được khóa trên đối tượng của bạn "bên ngoài" ( synchronized(theObject) {...}), chặn các luồng khác đang chờ trên các phương thức cá thể được đồng bộ hóa.

Nếu bạn không tin vào mã độc hại, hãy xem xét rằng mã này có thể đến từ bên thứ ba (ví dụ: nếu bạn phát triển một số loại máy chủ ứng dụng).

Phiên bản "tình cờ" có vẻ ít khả năng hơn, nhưng như họ nói, "làm cho một cái gì đó ngớ ngẩn và ai đó sẽ phát minh ra một thằng ngốc tốt hơn".

Vì vậy, tôi đồng ý với trường phái tư tưởng phụ thuộc vào nó.


Chỉnh sửa 3 bình luận đầu tiên của eljenso:

Tôi chưa bao giờ gặp vấn đề ăn cắp khóa nhưng đây là một kịch bản tưởng tượng:

Giả sử hệ thống của bạn là một thùng chứa servlet và đối tượng chúng tôi đang xem xét là việc ServletContexttriển khai. getAttributePhương thức của nó phải an toàn cho luồng, vì các thuộc tính bối cảnh là dữ liệu được chia sẻ; vì vậy bạn tuyên bố nó làsynchronized . Chúng ta cũng hãy tưởng tượng rằng bạn cung cấp một dịch vụ lưu trữ công cộng dựa trên việc triển khai container của bạn.

Tôi là khách hàng của bạn và triển khai servlet "tốt" của tôi trên trang web của bạn. Nó xảy ra rằng mã của tôi chứa một cuộc gọi đếngetAttribute .

Một hacker, cải trang thành một khách hàng khác, triển khai servlet độc hại của anh ta trên trang web của bạn. Nó chứa mã sau đây tronginit phương thức:

đã đồng bộ hóa (this.getServletConfig (). getServletContext ()) {
   trong khi (đúng) {}
}

Giả sử chúng ta chia sẻ cùng một bối cảnh servlet (được thông số kỹ thuật cho phép miễn là hai servlet trên cùng một máy chủ ảo), cuộc gọi của tôi getAttributesẽ bị khóa vĩnh viễn. Tin tặc đã đạt được một DoS trên servlet của tôi.

Cuộc tấn công này là không thể nếu getAttributeđược đồng bộ hóa trên một khóa riêng, bởi vì mã của bên thứ 3 không thể có được khóa này.

Tôi thừa nhận rằng ví dụ này được đặt ra và một cái nhìn bao quát về cách thức hoạt động của một thùng chứa servlet, nhưng IMHO nó chứng minh điều đó.

Vì vậy, tôi sẽ đưa ra lựa chọn thiết kế của mình dựa trên sự cân nhắc về bảo mật: tôi sẽ có toàn quyền kiểm soát mã có quyền truy cập vào các phiên bản không? Điều gì sẽ là hậu quả của một chủ đề giữ một khóa trên một trường hợp vô thời hạn?


it-lệ thuộc vào những gì lớp học: nếu nó là một đối tượng 'quan trọng', sau đó khóa tham chiếu cá nhân? Khác khóa khóa sẽ đủ?
eljenso

6
Vâng, kịch bản ăn cắp khóa dường như rất xa vời với tôi. Mọi người đều đề cập đến nó, nhưng ai đã thực sự làm nó hoặc trải nghiệm nó? Nếu bạn "vô tình" khóa một đối tượng bạn không nên, thì có một tên cho loại tình huống này: đó là một lỗi. Sửa nó.
eljenso

4
Ngoài ra, việc khóa các tham chiếu nội bộ không thoát khỏi "cuộc tấn công đồng bộ bên ngoài": nếu bạn biết rằng một phần được đồng bộ hóa của mã chờ một sự kiện bên ngoài xảy ra (ví dụ: ghi tệp, giá trị trong DB, sự kiện hẹn giờ), bạn có thể có thể sắp xếp cho nó để chặn là tốt.
eljenso

Hãy để tôi thú nhận rằng tôi là một trong những kẻ ngốc đó, alitit tôi đã làm điều đó khi tôi còn trẻ. Tôi nghĩ rằng mã sạch hơn bằng cách không tạo một đối tượng khóa rõ ràng và thay vào đó, sử dụng một đối tượng riêng tư cuối cùng cần tham gia vào màn hình. Tôi không biết rằng chính đối tượng đã tự đồng bộ hóa. Bạn có thể tưởng tượng ra vụ cướp tiếp theo ...
Alan Cabrera

12

Nó phụ thuộc vào tình hình.
Nếu chỉ có một thực thể chia sẻ hoặc nhiều hơn một.

Xem ví dụ làm việc đầy đủ ở đây

Giới thiệu nhỏ.

Chủ đề và các thực thể có thể chia sẻ
Có thể cho nhiều chủ đề truy cập vào cùng một thực thể, ví dụ như nhiều kết nối. Chia sẻ một thông điệp duy nhất. Vì các luồng chạy đồng thời, có thể có cơ hội ghi đè dữ liệu của một người khác có thể là một tình huống lộn xộn.
Vì vậy, chúng ta cần một số cách để đảm bảo rằng thực thể có thể chia sẻ chỉ được truy cập bởi một luồng tại một thời điểm. (Ý TƯỞNG).

Khối được
đồng bộ hóa Khối được đồng bộ hóa () là một cách để đảm bảo truy cập đồng thời của thực thể có thể chia sẻ.
Đầu tiên, một sự tương tự nhỏ
Giả sử Có hai người P1, P2 (chủ đề) một Washbasin (thực thể có thể chia sẻ) trong phòng vệ sinh và có một cánh cửa (khóa).
Bây giờ chúng tôi muốn một người sử dụng chậu rửa mặt tại một thời điểm.
Cách tiếp cận là khóa cửa bằng P1 khi cửa bị khóa P2 đợi cho đến khi p1 hoàn thành công việc của mình,
P1 mở khóa cửa
sau đó chỉ p1 mới có thể sử dụng chậu rửa mặt.

cú pháp.

synchronized(this)
{
  SHARED_ENTITY.....
}

"cái này" cung cấp khóa nội tại được liên kết với lớp (Nhà phát triển Java đã thiết kế lớp Object theo cách mà mỗi đối tượng có thể hoạt động như màn hình). Cách tiếp cận trên hoạt động tốt khi chỉ có một thực thể được chia sẻ và nhiều luồng (1: N). N chủ đề có thể chia sẻ-M Bây giờ hãy nghĩ đến một tình huống khi có hai chậu rửa bên trong phòng vệ sinh và chỉ có một cửa. Nếu chúng ta đang sử dụng cách tiếp cận trước đó, chỉ p1 có thể sử dụng một chậu rửa tại một thời điểm trong khi p2 sẽ đợi bên ngoài. Đó là sự lãng phí tài nguyên vì không ai sử dụng B2 (chậu rửa mặt). Một cách tiếp cận khôn ngoan hơn là tạo ra một căn phòng nhỏ hơn bên trong phòng vệ sinh và cung cấp cho họ một cửa cho mỗi chậu rửa. Theo cách này, P1 có thể truy cập B1 và ​​P2 có thể truy cập B2 và ngược lại.
nhập mô tả hình ảnh ở đây

washbasin1;  
washbasin2;

Object lock1=new Object();
Object lock2=new Object();

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

nhập mô tả hình ảnh ở đây
nhập mô tả hình ảnh ở đây

Xem thêm về Chủ đề ----> tại đây


11

Dường như có một sự đồng thuận khác nhau trong các trại C # và Java về điều này. Phần lớn mã Java tôi đã thấy sử dụng:

// apply mutex to this instance
synchronized(this) {
    // do work here
}

trong khi phần lớn mã C # chọn cách an toàn hơn:

// instance level lock object
private readonly object _syncObj = new object();

...

// apply mutex to private instance level field (a System.Object usually)
lock(_syncObj)
{
    // do work here
}

Thành ngữ C # chắc chắn an toàn hơn. Như đã đề cập trước đây, không có quyền truy cập độc hại / vô tình vào khóa có thể được thực hiện từ bên ngoài ví dụ. Mã Java cũng có rủi ro này, nhưng dường như cộng đồng Java đã bị hấp dẫn theo thời gian đến phiên bản kém an toàn hơn một chút, nhưng hơi ngắn hơn.

Điều đó không có nghĩa là một công cụ chống lại Java, chỉ là sự phản ánh trải nghiệm của tôi khi làm việc trên cả hai ngôn ngữ.


3
Có lẽ vì C # là một ngôn ngữ trẻ hơn, họ đã học được từ các mẫu xấu được tìm ra trong trại Java và các công cụ mã như thế này tốt hơn? Có phải cũng ít singletons? :)
Bill K

3
Anh ấy anh. Hoàn toàn có thể đúng, nhưng tôi sẽ không nổi lên mồi! Một suy nghĩ tôi có thể nói chắc chắn là có nhiều chữ in hoa trong mã C #;)
serg10

1
Chỉ là không đúng sự thật (nói một cách độc đáo)
tcurdt

7

Các java.util.concurrentgói đã bao la giảm sự phức tạp của mã đề an toàn của tôi. Tôi chỉ có bằng chứng giai thoại để tiếp tục, nhưng hầu hết các công việc tôi đã thấy synchronized(x)dường như đang thực hiện lại Khóa, Semaphore hoặc Latch, nhưng sử dụng các màn hình cấp thấp hơn.

Với suy nghĩ này, đồng bộ hóa bằng cách sử dụng bất kỳ cơ chế nào trong số này là tương tự như đồng bộ hóa trên một đối tượng bên trong, thay vì rò rỉ khóa. Điều này có lợi ở chỗ bạn chắc chắn tuyệt đối rằng bạn kiểm soát mục nhập vào màn hình bằng hai hoặc nhiều luồng.


6
  1. Làm cho dữ liệu của bạn không thay đổi nếu có thể ( final các biến)
  2. Nếu bạn không thể tránh đột biến dữ liệu được chia sẻ trên nhiều luồng, hãy sử dụng các cấu trúc lập trình cấp cao [ví dụ: LockAPI dạng hạt ]

Khóa cung cấp quyền truy cập độc quyền vào tài nguyên được chia sẻ: chỉ một luồng tại một thời điểm có thể có được khóa và tất cả quyền truy cập vào tài nguyên được chia sẻ yêu cầu khóa phải được lấy trước.

Mã mẫu để sử dụng ReentrantLockmà thực hiện Lockgiao diện

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

Ưu điểm của khóa so với đồng bộ hóa (điều này)

  1. Việc sử dụng các phương thức hoặc câu lệnh được đồng bộ hóa buộc tất cả việc thu thập và giải phóng khóa xảy ra theo cách có cấu trúc khối.

  2. Việc triển khai khóa cung cấp chức năng bổ sung cho việc sử dụng các phương thức và câu lệnh được đồng bộ hóa bằng cách cung cấp

    1. Một nỗ lực không chặn để có được một khóa ( tryLock())
    2. Một nỗ lực để có được khóa có thể bị gián đoạn ( lockInterruptibly())
    3. Một nỗ lực để có được khóa có thể hết thời gian ( tryLock(long, TimeUnit)).
  3. Một lớp Khóa cũng có thể cung cấp hành vi và ngữ nghĩa khác hoàn toàn với khóa màn hình ngầm, chẳng hạn như

    1. đảm bảo đặt hàng
    2. sử dụng không tái nhập
    3. Phát hiện bế tắc

Có một cái nhìn về câu hỏi SE này liên quan đến các loại khác nhau Locks:

Đồng bộ hóa với Khóa

Bạn có thể đạt được sự an toàn của luồng bằng cách sử dụng API đồng thời nâng cao thay vì các khối được Đồng bộ hóa. Trang tài liệu này cung cấp các cấu trúc lập trình tốt để đạt được sự an toàn của luồng.

Khóa Đối tượng hỗ trợ khóa thành ngữ đơn giản hóa nhiều ứng dụng đồng thời.

Các nhà điều hành xác định API cấp cao để khởi chạy và quản lý các luồng. Việc triển khai thực thi được cung cấp bởi java.util.conc hiện cung cấp quản lý nhóm luồng phù hợp cho các ứng dụng quy mô lớn.

Bộ sưu tập đồng thời giúp quản lý bộ sưu tập dữ liệu lớn dễ dàng hơn và có thể giảm đáng kể nhu cầu đồng bộ hóa.

Biến nguyên tử có các tính năng giảm thiểu đồng bộ hóa và giúp tránh các lỗi thống nhất bộ nhớ.

ThreadLocalRandom (trong JDK 7) cung cấp việc tạo số giả ngẫu nhiên hiệu quả từ nhiều luồng.

Tham khảo java.util.concurrentjava.util.concurrent.atomic gói quá cho các cấu trúc lập trình khác.


5

Nếu bạn đã quyết định rằng:

  • điều bạn cần làm là khóa đối tượng hiện tại; và
  • bạn muốn khóa nó với độ chi tiết nhỏ hơn toàn bộ phương thức;

sau đó tôi không thấy điều cấm kỵ trên syncizezd (cái này).

Một số người cố tình sử dụng đồng bộ hóa (cái này) (thay vì đánh dấu phương thức được đồng bộ hóa) bên trong toàn bộ nội dung của một phương thức vì họ nghĩ rằng nó "rõ ràng hơn với người đọc" mà đối tượng đang thực sự được đồng bộ hóa. Chừng nào mọi người còn đưa ra lựa chọn sáng suốt (ví dụ hiểu rằng bằng cách thực hiện, họ thực sự chèn thêm mã byte vào phương thức và điều này có thể có tác dụng kích thích tối ưu hóa tiềm năng), tôi không thấy vấn đề gì với điều này . Bạn phải luôn ghi lại hành vi đồng thời của chương trình của mình, vì vậy tôi không thấy đối số "'được đồng bộ hóa" xuất bản hành vi "là rất hấp dẫn.

Đối với câu hỏi bạn nên sử dụng khóa đối tượng nào, tôi nghĩ không có gì sai khi đồng bộ hóa trên đối tượng hiện tại nếu điều này được mong đợi bởi logic của những gì bạn đang làm và cách lớp của bạn thường được sử dụng . Ví dụ, với một bộ sưu tập, đối tượng mà bạn mong muốn sẽ khóa một cách hợp lý nói chung là chính bộ sưu tập đó.


1
"Nếu điều này sẽ được logic theo logic ..." là một điểm tôi cũng đang cố gắng vượt qua. Tôi không thấy quan điểm luôn luôn sử dụng khóa riêng, mặc dù sự đồng thuận chung dường như là tốt hơn, vì nó không gây tổn thương và phòng thủ nhiều hơn.
eljenso

4

Tôi nghĩ rằng có một lời giải thích tốt về lý do tại sao mỗi trong số đó là các kỹ thuật quan trọng dưới vành đai của bạn trong một cuốn sách có tên là Java Concurrency In Practice của Brian Goetz. Anh ta đưa ra một điểm rất rõ ràng - bạn phải sử dụng cùng một khóa "MỌI NƠI" để bảo vệ trạng thái của đối tượng của bạn. Phương pháp đồng bộ hóa và đồng bộ hóa trên một đối tượng thường đi đôi với nhau. Ví dụ Vector đồng bộ hóa tất cả các phương thức của nó. Nếu bạn có một đối tượng với một đối tượng vectơ và sẽ thực hiện "đặt nếu vắng mặt" thì chỉ đơn giản là Vector đồng bộ hóa các phương thức riêng của nó sẽ không bảo vệ bạn khỏi tham nhũng của nhà nước. Bạn cần đồng bộ hóa bằng cách sử dụng đồng bộ hóa (vectorHandle). Điều này sẽ dẫn đến khóa SAME được thu thập bởi mọi luồng có tay cầm cho vectơ và sẽ bảo vệ trạng thái chung của vectơ. Điều này được gọi là khóa phía khách hàng. Chúng ta biết rằng vectơ thực tế đã đồng bộ hóa (điều này) / đồng bộ hóa tất cả các phương thức của nó và do đó đồng bộ hóa trên đối tượng vectorHandle sẽ dẫn đến việc đồng bộ hóa đúng trạng thái của các đối tượng vector. Thật ngu ngốc khi tin rằng bạn là chủ đề an toàn chỉ vì bạn đang sử dụng bộ sưu tập an toàn chủ đề. Đây chính xác là lý do đồng thời giới thiệu phương thức put IfAbsent - để biến các hoạt động đó thành nguyên tử.

Tóm tắt

  1. Đồng bộ hóa ở cấp phương thức cho phép khóa phía máy khách.
  2. Nếu bạn có một đối tượng khóa riêng - nó làm cho khóa máy khách không thể khóa. Điều này tốt nếu bạn biết rằng lớp của bạn không có loại chức năng "đặt nếu vắng mặt".
  3. Nếu bạn đang thiết kế một thư viện - thì việc đồng bộ hóa trên này hoặc đồng bộ hóa phương thức thường khôn ngoan hơn. Bởi vì bạn hiếm khi ở một vị trí để quyết định lớp của bạn sẽ được sử dụng như thế nào.
  4. Nếu Vector đã sử dụng một đối tượng khóa riêng - sẽ không thể có được "đặt nếu vắng mặt" ngay. Mã máy khách sẽ không bao giờ có được một tay cầm cho khóa riêng, do đó phá vỡ quy tắc cơ bản của việc sử dụng KHÓA CHÍNH XÁC để bảo vệ trạng thái của nó.
  5. Đồng bộ hóa trên phương thức này hoặc phương thức đồng bộ hóa có một vấn đề như những người khác đã chỉ ra - ai đó có thể có được một khóa và không bao giờ phát hành nó. Tất cả các chủ đề khác sẽ tiếp tục chờ khóa được phát hành.
  6. Vì vậy, biết những gì bạn đang làm và chấp nhận một trong những điều đó là chính xác.
  7. Ai đó lập luận rằng việc có một đối tượng khóa riêng mang lại cho bạn mức độ chi tiết tốt hơn - ví dụ: nếu hai thao tác không liên quan - chúng có thể được bảo vệ bởi các khóa khác nhau dẫn đến thông lượng tốt hơn. Nhưng điều này tôi nghĩ là mùi thiết kế và không phải mùi mã - nếu hai thao tác hoàn toàn không liên quan tại sao chúng là một phần của lớp CÙNG? Tại sao một câu lạc bộ lớp không có chức năng liên quan? Có thể là một lớp tiện ích? Hmmmm - một số sử dụng cung cấp thao tác chuỗi và định dạng ngày theo lịch thông qua cùng một ví dụ ?? ... ít nhất không có ý nghĩa gì với tôi !!

3

Không, bạn không nên luôn luôn . Tuy nhiên, tôi có xu hướng tránh nó khi có nhiều mối quan tâm về một đối tượng cụ thể chỉ cần là chủ đề an toàn đối với chính họ. Ví dụ: bạn có thể có một đối tượng dữ liệu có thể thay đổi có các trường "nhãn" và "cha mẹ"; những cái này cần phải an toàn, nhưng thay đổi cái này không cần phải chặn cái kia được viết / đọc. (Trong thực tế, tôi sẽ tránh điều này bằng cách khai báo các trường không ổn định và / hoặc sử dụng các hàm bao AtomicFoo của java.util.conc hiện).

Đồng bộ hóa nói chung là một chút vụng về, vì nó khóa một khóa lớn thay vì suy nghĩ chính xác làm thế nào các chủ đề có thể được phép làm việc xung quanh nhau. Việc sử dụng synchronized(this)thậm chí còn vụng về và phản xã hội hơn, vì nó nói rằng "không ai có thể thay đổi bất cứ điều gì trên lớp này trong khi tôi giữ khóa". Bao lâu bạn thực sự cần phải làm điều đó?

Tôi muốn có nhiều khóa hạt hơn; ngay cả khi bạn muốn ngăn mọi thứ thay đổi (có lẽ bạn đang tuần tự hóa đối tượng), bạn chỉ có thể có được tất cả các khóa để đạt được điều tương tự, cộng với cách đó rõ ràng hơn. Khi bạn sử dụng synchronized(this), không rõ chính xác lý do tại sao bạn đồng bộ hóa hoặc tác dụng phụ có thể là gì. Nếu bạn sử dụng synchronized(labelMonitor), hoặc thậm chí tốt hơn labelLock.getWriteLock().lock(), nó sẽ rõ ràng những gì bạn đang làm và những ảnh hưởng của phần quan trọng của bạn bị hạn chế.


3

Câu trả lời ngắn : Bạn phải hiểu sự khác biệt và đưa ra lựa chọn tùy thuộc vào mã.

Câu trả lời dài : Nói chung tôi thà cố gắng tránh đồng bộ hóa (điều này) để giảm sự tranh chấp nhưng khóa riêng làm tăng thêm sự phức tạp mà bạn phải biết. Vì vậy, sử dụng đồng bộ hóa đúng cho công việc phù hợp. Nếu bạn không có nhiều kinh nghiệm với lập trình đa luồng, tôi thà dính vào khóa cá thể và đọc về chủ đề này. (Điều đó nói rằng: chỉ sử dụng đồng bộ hóa (điều này) không tự động làm cho lớp của bạn hoàn toàn an toàn cho chuỗi.) Đây không phải là một chủ đề dễ dàng nhưng một khi bạn đã quen với nó, câu trả lời có nên sử dụng đồng bộ hóa (điều này) hay không tự nhiên .


Tôi có hiểu bạn chính xác khi bạn nói điều đó phụ thuộc vào kinh nghiệm của bạn không?
eljenso

Ở nơi đầu tiên, nó phụ thuộc vào mã mà bạn muốn viết. Chỉ cần nói rằng bạn có thể cần thêm một chút kinh nghiệm khi bạn chuyển hướng để không sử dụng đồng bộ hóa (điều này).
tcurdt

2

Một khóa được sử dụng cho khả năng hiển thị hoặc để bảo vệ một số dữ liệu khỏi sửa đổi đồng thời có thể dẫn đến cuộc đua.

Khi bạn chỉ cần thực hiện các hoạt động kiểu nguyên thủy để trở thành nguyên tử, có các tùy chọn có sẵn như AtomicIntegervà thích.

Nhưng giả sử bạn có hai số nguyên có liên quan với nhau như xytọa độ, có liên quan với nhau và nên được thay đổi theo cách nguyên tử. Sau đó, bạn sẽ bảo vệ chúng bằng cách sử dụng cùng một khóa.

Một khóa chỉ nên bảo vệ trạng thái có liên quan với nhau. Không ít hơn và không hơn. Nếu bạn sử dụng synchronized(this)trong mỗi phương thức thì ngay cả khi trạng thái của lớp không liên quan, tất cả các luồng sẽ phải đối mặt ngay cả khi cập nhật trạng thái không liên quan.

class Point{
   private int x;
   private int y;

   public Point(int x, int y){
       this.x = x;
       this.y = y;
   }

   //mutating methods should be guarded by same lock
   public synchronized void changeCoordinates(int x, int y){
       this.x = x;
       this.y = y;
   }
}

Trong ví dụ trên tôi chỉ có một phương thức làm đột biến cả hai xykhông có hai phương thức khác nhau xycó liên quan và nếu tôi đã đưa ra hai phương thức khác nhau để đột biến xyriêng rẽ thì nó sẽ không an toàn cho luồng.

Ví dụ này chỉ để chứng minh và không nhất thiết là cách nó nên được thực hiện. Cách tốt nhất để làm điều đó là làm cho nó NGAY LẬP TỨC .

Bây giờ đối lập với Pointví dụ, có một ví dụ TwoCountersđã được @Andreas cung cấp trong đó trạng thái đang được bảo vệ bởi hai khóa khác nhau vì trạng thái không liên quan với nhau.

Quá trình sử dụng các khóa khác nhau để bảo vệ các trạng thái không liên quan được gọi là Khóa sọc hoặc Khóa tách


1

Lý do không đồng bộ hóa trên điều này là vì đôi khi bạn cần nhiều hơn một khóa (khóa thứ hai thường bị xóa sau một số suy nghĩ bổ sung, nhưng bạn vẫn cần nó ở trạng thái trung gian). Nếu bạn khóa cái này , bạn luôn phải nhớ cái nào trong hai cái khóa này ; nếu bạn khóa trên một đối tượng riêng tư, tên biến cho bạn biết điều đó.

Từ quan điểm của người đọc, nếu bạn thấy khóa về điều này , bạn luôn phải trả lời hai câu hỏi:

  1. Những loại truy cập được bảo vệ bởi điều này ?
  2. Là một khóa thực sự đủ, không ai giới thiệu một lỗi?

Một ví dụ:

class BadObject {
    private Something mStuff;
    synchronized setStuff(Something stuff) {
        mStuff = stuff;
    }
    synchronized getStuff(Something stuff) {
        return mStuff;
    }
    private MyListener myListener = new MyListener() {
        public void onMyEvent(...) {
            setStuff(...);
        }
    }
    synchronized void longOperation(MyListener l) {
        ...
        l.onMyEvent(...);
        ...
    }
}

Nếu hai luồng bắt đầu longOperation()trên hai trường hợp khác nhau BadObject, chúng có được khóa của chúng; khi đến lúc phải gọi l.onMyEvent(...), chúng ta gặp bế tắc vì không có chủ đề nào có thể có được khóa của đối tượng khác.

Trong ví dụ này, chúng tôi có thể loại bỏ bế tắc bằng cách sử dụng hai khóa, một cho các hoạt động ngắn và một cho các khóa dài.


2
Cách duy nhất để có được sự bế tắc trong ví dụ này là khi BadObjectA gọi longOperationB, vượt qua A myListenervà ngược lại. Không phải là không thể, nhưng khá phức tạp, hỗ trợ các điểm trước đó của tôi.
eljenso

1

Như đã nói ở đây, khối được đồng bộ hóa có thể sử dụng biến do người dùng xác định làm đối tượng khóa, khi chức năng được đồng bộ hóa chỉ sử dụng "này". Và tất nhiên bạn có thể thao tác với các khu vực chức năng của bạn sẽ được đồng bộ hóa và vv.

Nhưng mọi người đều nói rằng không có sự khác biệt giữa chức năng được đồng bộ hóa và khối bao gồm toàn bộ chức năng sử dụng "this" làm đối tượng khóa. Điều đó không đúng, sự khác biệt nằm ở mã byte sẽ được tạo ra trong cả hai tình huống. Trong trường hợp sử dụng khối đồng bộ nên được phân bổ biến cục bộ chứa tham chiếu đến "này". Và kết quả là chúng ta sẽ có kích thước hàm lớn hơn một chút (không liên quan nếu bạn chỉ có một số ít hàm).

Giải thích chi tiết hơn về sự khác biệt bạn có thể tìm thấy ở đây: http://www.artima.com/insidejvm/ed2/threadsynchP.html

Ngoài ra việc sử dụng khối được đồng bộ hóa là không tốt do quan điểm sau:

Từ khóa được đồng bộ hóa rất hạn chế trong một khu vực: khi thoát khỏi một khối được đồng bộ hóa, tất cả các luồng đang chờ khóa đó phải được bỏ chặn, nhưng chỉ một trong số các luồng đó được khóa; tất cả những người khác thấy rằng khóa được lấy và trở về trạng thái bị chặn. Đó không chỉ là nhiều chu trình xử lý lãng phí: thường thì việc chuyển đổi ngữ cảnh để bỏ chặn một luồng cũng liên quan đến việc phân trang bộ nhớ ra khỏi đĩa và điều đó rất, rất tốn kém.

Để biết thêm chi tiết trong lĩnh vực này, tôi khuyên bạn nên đọc bài viết này: http://java.dzone.com/articles/synyncized-considered


1

Đây thực sự chỉ là bổ sung cho các câu trả lời khác, nhưng nếu sự phản đối chính của bạn đối với việc sử dụng các đối tượng riêng để khóa là nó khóa lớp của bạn với các trường không liên quan đến logic nghiệp vụ thì Project Lombok @Synchronizedphải tạo ra bản tóm tắt vào thời gian biên dịch:

@Synchronized
public int foo() {
    return 0;
}

biên dịch thành

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

0

Một ví dụ tốt để sử dụng đồng bộ (này).

// add listener
public final synchronized void addListener(IListener l) {listeners.add(l);}
// remove listener
public final synchronized void removeListener(IListener l) {listeners.remove(l);}
// routine that raise events
public void run() {
   // some code here...
   Set ls;
   synchronized(this) {
      ls = listeners.clone();
   }
   for (IListener l : ls) { l.processEvent(event); }
   // some code here...
}

Như bạn có thể thấy ở đây, chúng tôi sử dụng đồng bộ hóa trên này để dễ dàng hợp tác lâu dài (có thể là vòng lặp vô hạn của phương thức chạy) với một số phương thức được đồng bộ hóa ở đó.

Tất nhiên nó có thể được viết lại rất dễ dàng bằng cách sử dụng đồng bộ hóa trên trường riêng. Nhưng đôi khi, khi chúng ta đã có một số thiết kế với các phương thức được đồng bộ hóa (tức là lớp kế thừa, chúng ta xuất phát từ, đồng bộ hóa (đây) có thể là giải pháp duy nhất).


Bất kỳ đối tượng có thể được sử dụng như khóa ở đây. Nó không cần phải như vậy this. Nó có thể là một lĩnh vực tư nhân.
vây

Đúng, nhưng mục đích của ví dụ này là chỉ ra cách thực hiện đồng bộ hóa đúng cách, nếu chúng tôi quyết định sử dụng đồng bộ hóa phương thức.
Bart Prokop

0

Nó phụ thuộc vào nhiệm vụ bạn muốn làm, nhưng tôi sẽ không sử dụng nó. Ngoài ra, hãy kiểm tra xem tính năng lưu luồng mà bạn muốn thực hiện có thể được thực hiện bằng cách đồng bộ hóa (điều này) ở vị trí đầu tiên không? Ngoài ra còn có một số khóa đẹp trong API có thể giúp bạn :)


0

Tôi chỉ muốn đề cập đến một giải pháp khả thi cho các tham chiếu riêng tư duy nhất trong các phần nguyên tử của mã mà không phụ thuộc. Bạn có thể sử dụng Hashmap tĩnh với các khóa và một phương thức tĩnh đơn giản có tên là nguyên tử () tự động tạo các tham chiếu cần thiết bằng cách sử dụng thông tin ngăn xếp (tên lớp đầy đủ và số dòng). Sau đó, bạn có thể sử dụng phương thức này trong việc đồng bộ hóa các câu lệnh mà không cần viết đối tượng khóa mới.

// Synchronization objects (locks)
private static HashMap<String, Object> locks = new HashMap<String, Object>();
// Simple method
private static Object atomic() {
    StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point 
    StackTraceElement exepoint = stack[2];
    // creates unique key from class name and line number using execution point
    String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); 
    Object lock = locks.get(key); // use old or create new lock
    if (lock == null) {
        lock = new Object();
        locks.put(key, lock);
    }
    return lock; // return reference to lock
}
// Synchronized code
void dosomething1() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 1
        ...
    }
    // other command
}
// Synchronized code
void dosomething2() {
    // start commands
    synchronized (atomic()) {
        // atomic commands 2
        ...
    }
    // other command
}

0

Tránh sử dụng synchronized(this)như một cơ chế khóa: Điều này khóa toàn bộ thể hiện của lớp và có thể gây ra bế tắc. Trong các trường hợp như vậy, hãy cấu trúc lại mã để chỉ khóa một phương thức hoặc biến cụ thể, theo cách đó cả lớp không bị khóa. Synchronisedcó thể được sử dụng bên trong mức phương thức.
Thay vì sử dụng synchronized(this), đoạn mã dưới đây cho thấy cách bạn có thể khóa một phương thức.

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }

0

Hai xu của tôi vào năm 2019 mặc dù câu hỏi này có thể đã được giải quyết.

Khóa 'cái này' không tệ nếu bạn biết bạn đang làm gì nhưng đằng sau cảnh khóa trên 'cái này' là (điều không may là từ khóa được đồng bộ hóa trong định nghĩa phương thức cho phép).

Nếu bạn thực sự muốn người dùng trong lớp của bạn có thể 'đánh cắp' khóa của bạn (tức là ngăn các luồng khác xử lý nó), bạn thực sự muốn tất cả các phương thức được đồng bộ hóa chờ trong khi phương thức đồng bộ hóa khác đang chạy. Nó nên có chủ ý và suy nghĩ tốt (và do đó được ghi lại để giúp người dùng của bạn hiểu nó).

Để giải thích thêm, ngược lại, bạn phải biết bạn đang 'đạt được' (hoặc 'thua') nếu bạn khóa trên một khóa không thể truy cập (không ai có thể 'đánh cắp' khóa của bạn, bạn hoàn toàn kiểm soát được. ..).

Vấn đề đối với tôi là từ khóa được đồng bộ hóa trong chữ ký định nghĩa phương thức làm cho các lập trình viên quá dễ dàng không nghĩ về việc khóa cái gì, đây là một điều rất quan trọng để suy nghĩ nếu bạn không muốn gặp vấn đề trong đa chương trình đã đọc.

Người ta không thể tranh luận rằng 'thông thường' bạn không muốn người dùng trong lớp của bạn có thể thực hiện những công cụ này hoặc 'thông thường' bạn muốn ... Điều đó phụ thuộc vào chức năng bạn đang mã hóa. Bạn không thể thực hiện quy tắc ngón tay cái vì bạn không thể dự đoán tất cả các trường hợp sử dụng.

Ví dụ, hãy xem xét các nhà in sử dụng khóa nội bộ nhưng sau đó mọi người đấu tranh để sử dụng nó từ nhiều luồng nếu họ không muốn đầu ra của họ xen kẽ.

Khóa của bạn có thể truy cập được bên ngoài lớp hay không là quyết định của bạn với tư cách là một lập trình viên trên cơ sở chức năng của lớp đó. Nó là một phần của api. Chẳng hạn, bạn không thể chuyển từ đồng bộ hóa (cái này) sang đồng bộ hóa (provateObjet) mà không mạo hiểm phá vỡ các thay đổi trong mã bằng cách sử dụng nó.

Lưu ý 1: Tôi biết bạn có thể đạt được bất kỳ điều gì được đồng bộ hóa (điều này) bằng cách sử dụng một đối tượng khóa rõ ràng và phơi bày nó nhưng tôi nghĩ không cần thiết nếu hành vi của bạn được ghi lại rõ ràng và bạn thực sự biết khóa 'điều này' nghĩa là gì.

Lưu ý 2: Tôi không đồng tình với lập luận rằng nếu một số mã vô tình đánh cắp khóa của bạn thì đó là lỗi và bạn phải giải quyết nó. Điều này theo một cách nào đó là lập luận tương tự như nói rằng tôi có thể công khai tất cả các phương thức của mình ngay cả khi chúng không có nghĩa là công khai. Nếu ai đó 'vô tình' gọi tôi dự định là phương thức riêng tư thì đó là một lỗi. Tại sao lại kích hoạt tai nạn này ngay từ đầu !!! Nếu khả năng đánh cắp khóa của bạn là một vấn đề cho lớp học của bạn, đừng cho phép nó. Đơn giản vậy thôi.


-3

Tôi nghĩ rằng điểm một (ai đó khác sử dụng khóa của bạn) và hai (tất cả các phương pháp sử dụng cùng một khóa không cần thiết) có thể xảy ra trong bất kỳ ứng dụng khá lớn nào. Đặc biệt là khi không có giao tiếp tốt giữa các nhà phát triển.

Nó không được đúc bằng đá, nó chủ yếu là một vấn đề thực hành tốt và ngăn ngừa lỗi.

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.