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


182

java.util.concurrentAPI cung cấp một lớp được gọi là Lock, về cơ bản sẽ tuần tự hóa điều khiển để truy cập tài nguyên quan trọng. Nó đưa ra phương thức như park()unpark().

Chúng ta có thể làm những điều tương tự nếu chúng ta có thể sử dụng synchronizedtừ khóa và sử dụng wait()notify() notifyAll()phương pháp.

Tôi đang tự hỏi cái nào trong số này tốt hơn trong thực tế và tại sao?


1
bài viết hữu ích ở đây: javarevisited.blogspot.in/2013/03/ từ
roottraveller

Câu trả lời:


178

Nếu bạn chỉ đơn giản là khóa một đối tượng, tôi muốn sử dụng synchronized

Thí dụ:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

Bạn phải làm rõ ràng try{} finally{}ở khắp mọi nơi.

Trong khi với đồng bộ hóa, nó rất rõ ràng và không thể sai:

synchronized(myObject) {
    doSomethingNifty();
}

Điều đó nói rằng, Locks có thể hữu ích hơn cho những thứ phức tạp hơn mà bạn không thể có được và phát hành theo cách sạch sẽ như vậy. Tôi thực sự muốn tránh sử dụng trần Lockở nơi đầu tiên, và chỉ cần đi với một điều khiển đồng thời phức tạp hơn như a CyclicBarrierhoặc a LinkedBlockingQueue, nếu chúng đáp ứng nhu cầu của bạn.

Tôi chưa bao giờ có lý do để sử dụng wait()hoặc notify()có thể có một số lý do tốt.


1
Sự khác biệt giữa chờ / thông báo so với park / unpark của LockSupport là gì? docs.oracle.com/javase/7/docs/api/java/util/conciverse/locks/ mẹo
Pacerier

6
Lúc đầu, ví dụ có ý nghĩa với các khóa nhưng sau đó tôi nhận ra rằng nếu bạn sử dụng thử cuối cùng thì chặn được vấn đề đó với các khóa không được phát hành
William Reed

À ... Một trong những khoảnh khắc để đánh giá cao mô hình RAII trong C ++. std::lock_guard
WhiZTiM

67

Tôi đang tự hỏi cái nào trong số này tốt hơn trong thực tế và tại sao?

Tôi đã thấy rằng LockCondition(và các concurrentlớp mới khác ) chỉ là nhiều công cụ hơn cho hộp công cụ. Tôi có thể làm hầu hết mọi thứ tôi cần với chiếc búa vuốt cũ ( synchronizedtừ khóa), nhưng thật khó sử dụng trong một số tình huống. Một vài trong số những tình huống khó xử đó trở nên đơn giản hơn nhiều khi tôi thêm nhiều công cụ vào hộp công cụ của mình: một cái vồ cao su, một cây búa bóng, một thanh prybar và một số cú đấm móng tay. Tuy nhiên , búa vuốt cũ của tôi vẫn thấy phần sử dụng của nó.

Tôi không nghĩ rằng cái này thực sự "tốt" hơn cái kia, nhưng mỗi cái lại phù hợp hơn cho các vấn đề khác nhau. Tóm lại, mô hình đơn giản và tính chất định hướng phạm vi synchronizedgiúp bảo vệ tôi khỏi các lỗi trong mã của tôi, nhưng những lợi thế tương tự đôi khi lại gây trở ngại trong các tình huống phức tạp hơn. Đây là những kịch bản phức tạp hơn mà gói đồng thời được tạo ra để giúp giải quyết. Nhưng sử dụng các cấu trúc mức cao hơn này đòi hỏi quản lý rõ ràng và cẩn thận hơn trong mã.

===

Tôi nghĩ rằng JavaDoc thực hiện tốt việc mô tả sự khác biệt giữa Locksynchronized(sự nhấn mạnh là của tôi):

Việc triển khai khóa cung cấp các hoạt động khóa mở rộng hơn mức có thể thu được bằng cách sử dụng các phương thức và câu lệnh được đồng bộ hóa. Chúng cho phép cấu trúc linh hoạt hơn , có thể có các thuộc tính khá khác nhau và có thể hỗ trợ nhiều đối tượng Điều kiện liên quan .

...

Việc sử dụng phương pháp đồng bộ hoặc báo cáo cung cấp quyền truy cập vào các khóa màn ngầm kết hợp với mọi đối tượng, nhưng lực lượng tất cả mua lại khóa và phát hành để xảy ra một cách khối cấu trúc : khi nhiều ổ khóa được mua họ phải được phát hành theo thứ tự ngược lại , và tất cả các khóa phải được phát hành trong cùng một phạm vi từ vựng mà chúng đã được mua .

Mặc dù cơ chế phạm vi cho các phương thức và câu lệnh được đồng bộ hóa giúp lập trình với khóa màn hình dễ dàng hơn nhiều và giúp tránh nhiều lỗi lập trình phổ biến liên quan đến khóa, có những lúc bạn cần làm việc với khóa theo cách linh hoạt hơn. Ví dụ: * * một số thuật toán * để truyền tải các cấu trúc dữ liệu được truy cập đồng thời yêu cầu sử dụng "bàn giao tay" hoặc "khóa chuỗi" : bạn có được khóa của nút A, sau đó nút B, sau đó giải phóng A và thu được C, sau đó giải phóng B và thu nhận D và cứ thế. Việc triển khai giao diện Khóa cho phép sử dụng các kỹ thuật như vậy bằng cách cho phép khóa và được phát hành trong các phạm vi khác nhaucho phép nhiều khóa được mua và phát hành theo bất kỳ thứ tự nào .

Với sự linh hoạt tăng lên này có thêm trách nhiệm . Việc không có khóa có cấu trúc khối sẽ loại bỏ việc giải phóng khóa tự động xảy ra với các phương thức và câu lệnh được đồng bộ hóa. Trong hầu hết các trường hợp, nên sử dụng thành ngữ sau:

...

Khi khóa và mở khóa xảy ra ở các phạm vi khác nhau , phải cẩn thận để đảm bảo rằng tất cả mã được thực thi trong khi khóa được giữ được bảo vệ bằng cách thử cuối cùng hoặc thử bắt để đảm bảo rằng khóa được giải phóng khi cần thiết.

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 một nỗ lực không chặn để có được khóa (tryLock ()), một nỗ lực để có được khóa có thể bị gián đoạn (lockInterruptingly () và cố gắng có được khóa có thể hết thời gian (tryLock (dài, TimeUnit)).

...


23

Bạn có thể đạt được tất cả những gì tiện ích trong java.util.concurrent làm với các nguyên thủy cấp thấp như synchronized, volatilehoặc chờ đợi / thông báo

Tuy nhiên, đồng thời là khó khăn và hầu hết mọi người đều nhận được ít nhất một số phần của nó sai, làm cho mã của họ không chính xác hoặc không hiệu quả (hoặc cả hai).

API đồng thời cung cấp một cách tiếp cận cấp cao hơn, dễ sử dụng hơn (và như vậy an toàn hơn). Tóm lại, bạn không cần phải sử dụng synchronized, volatile, wait, notifytrực tiếp nữa.

Bản thân lớp Khóa nằm ở phía dưới của hộp công cụ này, bạn thậm chí có thể không cần sử dụng trực tiếp (bạn có thể sử dụng QueuesSemaphore và các công cụ, v.v., hầu hết thời gian).


2
Là sự chờ đợi / thông báo cũ đơn giản được coi là một nguyên thủy cấp thấp hơn java.util.concản.locks.LockSupport's park / unpark, hay nó là cách khác?
Pacerier

@Pacerier: Tôi coi cả hai là cấp thấp (nghĩa là thứ mà lập trình viên ứng dụng muốn tránh sử dụng trực tiếp), nhưng chắc chắn các phần cấp thấp hơn của java.util.concurrency (chẳng hạn như gói khóa) được xây dựng trên đầu của các nguyên hàm JVM gốc chờ / thông báo (thậm chí ở mức thấp hơn).
Thilo

2
Không, ý tôi là trong số 3: Thread.s ngủ / ngắt, Object.wait / notify, LockSupport.park / unpark, cái nào nguyên thủy nhất ?
Pacerier

2
@Thilo Tôi không chắc cách bạn hỗ trợ tuyên bố của mình java.util.concurrentdễ dàng hơn [nói chung] so với các tính năng ngôn ngữ ( synchronized, v.v ...). Khi bạn sử dụng, java.util.concurrentbạn phải tạo thói quen hoàn thành lock.lock(); try { ... } finally { lock.unlock() }trước khi viết mã trong khi với synchronizedbạn về cơ bản là ổn ngay từ đầu. Trên cơ sở này một mình tôi sẽ nói synchronizedlà dễ dàng hơn (cho bạn muốn hành vi của nó) hơn java.util.concurrent.locks.Lock. par 4
Iwan Aucamp

1
Đừng nghĩ rằng bạn có thể sao chép chính xác hành vi của các lớp AtomicXXX chỉ bằng các nguyên hàm đồng thời, vì chúng dựa vào lời gọi CAS gốc không có sẵn trước java.util.conc hiện.
Duncan Armstrong

15

Có 4 yếu tố chính khiến bạn muốn sử dụng synchronizedhoặc java.util.concurrent.Lock.

Lưu ý: Khóa đồng bộ là ý tôi khi tôi nói khóa nội tại.

  1. Khi Java 5 ra mắt với ReentrantLocks, họ đã chứng minh rằng có một sự khác biệt thông lượng khá rõ ràng sau đó là khóa nội tại. Nếu bạn đang tìm kiếm cơ chế khóa nhanh hơn và đang chạy 1.5, hãy xem xét jucReentrantLock. Khóa nội tại của Java 6 hiện có thể so sánh được.

  2. jucLock có các cơ chế khác nhau để khóa. Khóa bị gián đoạn - cố gắng khóa cho đến khi chuỗi khóa bị gián đoạn; khóa thời gian - cố gắng khóa trong một khoảng thời gian nhất định và bỏ cuộc nếu bạn không thành công; tryLock - cố gắng khóa, nếu một số luồng khác đang giữ khóa bỏ cuộc. Tất cả điều này được bao gồm ngoài khóa đơn giản. Khóa nội tại chỉ cung cấp khóa đơn giản

  3. Phong cách. Nếu cả 1 và 2 không thuộc các loại mà bạn quan tâm với hầu hết mọi người, bao gồm cả bản thân tôi, sẽ thấy các tinh dịch khóa nội tại dễ đọc hơn và ít dài dòng hơn sau đó khóa jucLock.
  4. Nhiều điều kiện. Một đối tượng bạn khóa chỉ có thể được thông báo và chờ đợi cho một trường hợp duy nhất. Phương thức newCondition của Lock cho phép một Khóa duy nhất có các lý do khác nhau để chờ đợi hoặc báo hiệu. Tôi chưa thực sự cần chức năng này trong thực tế, nhưng là một tính năng tốt cho những người cần nó.

Tôi thích các chi tiết về bình luận của bạn. Tôi sẽ thêm một dấu đầu dòng nữa - ReadWriteLock cung cấp hành vi hữu ích nếu bạn đang xử lý một số luồng, chỉ một số trong đó cần ghi vào đối tượng. Nhiều luồng có thể đồng thời đọc đối tượng và chỉ bị chặn nếu một luồng khác đã ghi vào nó.
Sam Goldberg

5

Tôi muốn thêm một số điều nữa vào đầu câu trả lời của Bert F.

Lockshỗ trợ các phương pháp khác nhau để kiểm soát khóa hạt mịn hơn, biểu cảm hơn so với màn hình ngầm ( synchronizedkhóa)

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. Tuy nhiên, một số khóa có thể cho phép truy cập đồng thời vào tài nguyên được chia sẻ, chẳng hạn như khóa đọc của ReadWriteLock.

Ưu điểm của khóa trên đồng bộ hóa từ trang tài liệu

  1. Việc sử dụng các phương thức hoặc câu lệnh được đồng bộ hóa cung cấp quyền truy cập vào khóa màn hình ngầm được liên kết với mọi đối tượng, nhưng 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 một nỗ lực không chặn để có được lock (tryLock()), một nỗ lực để có được khóa có thể bị gián đoạn ( lockInterruptibly()và một nỗ lực để có được khóa có thể timeout (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ư đặt hàng được đảm bảo, sử dụng không tái phát hoặc phát hiện khóa chết

ReentrantLock : Theo cách hiểu đơn giản theo sự hiểu biết của tôi, ReentrantLockcho phép một đối tượng nhập lại từ một phần quan trọng sang phần quan trọng khác. Vì bạn đã có khóa để nhập một phần quan trọng, bạn có thể phần quan trọng khác trên cùng một đối tượng bằng cách sử dụng khóa hiện tại.

ReentrantLockCác tính năng chính theo bài viết này

  1. Khả năng khóa gián đoạn.
  2. Khả năng hết thời gian chờ trong khi chờ khóa.
  3. Sức mạnh để tạo khóa công bằng.
  4. API để có được danh sách các chuỗi chờ cho khóa.
  5. Linh hoạt để thử khóa mà không chặn.

Bạn có thể dùng ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock để có thêm quyền kiểm soát đối với khóa chi tiết đối với các hoạt động đọc và ghi.

Ngoài ba ReentrantLocks này, java 8 còn cung cấp thêm một Khóa

Đóng dấu:

Các tàu Java 8 có một loại khóa mới gọi là StampedLock cũng hỗ trợ các khóa đọc và ghi giống như trong ví dụ trên. Ngược lại với ReadWriteLock, các phương thức khóa của StampedLock trả về một tem được biểu thị bằng một giá trị dài.

Bạn có thể sử dụng những con tem này để phát hành khóa hoặc để kiểm tra xem khóa có còn hiệu lực hay không. Ngoài ra các khóa được đóng dấu hỗ trợ một chế độ khóa khác gọi là khóa tối ưu.

Hãy xem bài viết này về việc sử dụng các loại ReentrantLockStampedLockkhóa khác nhau .


4

Sự khác biệt chính là sự công bằng, nói cách khác là các yêu cầu được xử lý FIFO hoặc có thể có mặc cả? Đồng bộ hóa mức phương pháp đảm bảo phân bổ công bằng hoặc FIFO của khóa. Sử dụng

synchronized(foo) {
}

hoặc là

lock.acquire(); .....lock.release();

không đảm bảo sự công bằng.

Nếu bạn có nhiều tranh cãi về khóa, bạn có thể dễ dàng bắt gặp mặc cả trong đó các yêu cầu mới hơn có được khóa và các yêu cầu cũ hơn bị kẹt. Tôi đã thấy các trường hợp 200 luồng đến theo thứ tự ngắn cho khóa và lần thứ hai đến đã được xử lý lần cuối. Điều này là ổn đối với một số ứng dụng nhưng đối với những ứng dụng khác thì thật nguy hiểm.

Xem cuốn sách "Thực hành đồng thời Java" của Brian Goetz, phần 13.3 để biết thảo luận đầy đủ về chủ đề này.


5
"Đồng bộ hóa mức phương pháp đảm bảo phân bổ khóa công bằng hoặc theo năm." => Thật sao? Bạn có nói rằng một phương thức được đồng bộ hóa hành xử công bằng khác với việc gói nội dung của các phương thức thành một khối {} được đồng bộ hóa không? Tôi sẽ không nghĩ như vậy, hoặc tôi đã hiểu câu đó sai ...?
weiresr

Vâng, mặc dù đáng ngạc nhiên và phản biện trực quan đó là chính xác. Cuốn sách của Goetz là lời giải thích tốt nhất.
Brian Tarbox

Nếu bạn xem mã được cung cấp bởi @BrianTarbox, khối được đồng bộ hóa đang sử dụng một số đối tượng khác ngoài "này" để khóa. Về lý thuyết, không có sự khác biệt giữa một phương thức được đồng bộ hóa và đặt toàn bộ phần thân của phương thức đã nói bên trong một khối được đồng bộ hóa, miễn là khối đó sử dụng "cái này" làm khóa.
xburgos

Câu trả lời nên được chỉnh sửa để bao gồm trích dẫn và làm rõ "bảo đảm" ở đây là "đảm bảo thống kê", không mang tính quyết định.
Nathan Hughes

Xin lỗi, tôi vừa phát hiện ra rằng tôi đã bỏ phiếu sai câu trả lời này vài ngày trước (nhấp chuột vụng về). Thật không may, SO hiện tại sẽ không cho phép tôi hoàn nguyên nó. Hy vọng tôi có thể sửa nó sau.
Mikko Östlund

3

Cuốn sách "Thực hành đồng thời Java" của Brian Goetz, phần 13.3: "... Giống như ReentrantLock mặc định, khóa nội tại không đảm bảo tính công bằng xác định, nhưng đảm bảo tính công bằng thống kê của hầu hết các triển khai khóa là đủ tốt cho hầu hết mọi tình huống ..."


1

Khóa làm cho cuộc sống lập trình viên dễ dàng hơn. Dưới đây là một vài tình huống có thể đạt được dễ dàng hơn với khóa.

  1. Khóa trong một phương thức và giải phóng khóa trong phương thức khác.
  2. Bạn có hai luồng làm việc trên hai đoạn mã khác nhau, tuy nhiên luồng đầu tiên có sự phụ thuộc vào luồng thứ hai để hoàn thành một đoạn mã nhất định trước khi tiến hành thêm (trong khi một số luồng khác cũng hoạt động đồng thời). Một khóa chia sẻ có thể giải quyết vấn đề này khá dễ dàng.
  3. Thực hiện giám sát. Ví dụ, một hàng đợi đơn giản trong đó các phương thức put và get được thực thi từ nhiều luồng khác nhau. Tuy nhiên, bạn có muốn chồng chéo các phương thức không giống nhau lên nhau không, cả hai phương thức put và get đều có thể trùng nhau. Trong trường hợp như vậy một khóa riêng làm cho cuộc sống rất dễ dàng.

Trong khi, khóa và điều kiện được xây dựng trên đồng bộ hóa. Vì vậy, chắc chắn bạn có thể đạt được mục tiêu tương tự với điều đó. Tuy nhiên, điều đó có thể làm cho cuộc sống của bạn khó khăn và có thể khiến bạn không thể giải quyết vấn đề thực tế.


1

Sự khác biệt chính giữa khóa và đồng bộ hóa:

  • với ổ khóa, bạn có thể giải phóng và thu được ổ khóa theo bất kỳ thứ tự nào.
  • với đồng bộ hóa, bạn chỉ có thể giải phóng các khóa theo thứ tự được mua.

0

Khóa và đồng bộ hóa cả hai phục vụ cùng một mục đích nhưng nó phụ thuộc vào cách sử dụng. Xem xét phần dưới đây

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

Trong trường hợp trên, nếu một luồng vào khối đồng bộ hóa, khối khác cũng bị khóa. Nếu có nhiều khối đồng bộ hóa như vậy trên cùng một đối tượng, tất cả các khối sẽ bị khóa. Trong các tình huống như vậy, java.util.concản.Lock có thể được sử dụng để ngăn chặn việc khóa các khối không mong muốn

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.