2009-12-04 34 views
32

JSR-133 FAQ nói:hiệu ứng nhớ đồng bộ hóa trong Java

Nhưng có nhiều hơn để đồng bộ hóa hơn loại trừ lẫn nhau. Đồng bộ hóa đảm bảo rằng bộ nhớ ghi theo luồng trước hoặc trong khối đồng bộ được hiển thị theo cách có thể dự đoán đối với các chủ đề khác mà đồng bộ trên cùng một màn hình. Sau chúng tôi thoát khỏi khối đồng bộ, chúng tôi phát hành màn hình, có hiệu ứng khi xả bộ nhớ cache tới bộ nhớ chính , do đó chủ đề này có thể hiển thị với các chủ đề khác. Trước khi chúng tôi có thể nhập một khối đồng bộ , chúng tôi có được màn hình , có hiệu lực làm mất hiệu lực bộ nhớ cache của bộ xử lý cục bộ sao cho các biến sẽ được tải lại từ bộ nhớ chính. Sau đó, chúng tôi sẽ có thể để xem tất cả các bài viết được hiển thị bằng bản phát hành trước đó.

Tôi cũng nhớ rằng trên máy ảo Sun hiện đại, đồng bộ hóa không được giám sát là rẻ. Tôi hơi bối rối trước tuyên bố này. Hãy xem xét mã như:

class Foo { 
    int x = 1; 
    int y = 1; 
    .. 
    synchronized (aLock) { 
     x = x + 1; 
    } 
} 

Cập nhật cho x cần đồng bộ hóa, nhưng việc mua lại khóa có xóa giá trị của y cũng khỏi bộ nhớ cache không? Tôi không thể tưởng tượng được như vậy, bởi vì nếu nó là sự thật, thì các kỹ thuật như việc khóa dải có thể không giúp được gì. Ngoài ra, JVM có thể phân tích mã một cách đáng tin cậy để đảm bảo rằng y không được sửa đổi trong một khối đồng bộ khác bằng cách sử dụng cùng một khóa và do đó không kết xuất giá trị của y trong bộ đệm khi nhập khối được đồng bộ?

+3

Gần đây tôi đã xem qua bài viết [CPU Cache Flushing Fallacy] (http://mechanical-sympathy.blogspot.com/2013/02/cpu-cache-flushing-fallacy.html), đó là hữu ích trong việc hiểu điều này tốt hơn. –

Trả lời

36

Câu trả lời ngắn gọn là JSR-133 đi quá xa trong lời giải thích của nó. Đây không phải là vấn đề nghiêm trọng vì JSR-133 là một tài liệu không quy chuẩn không phải là một phần của ngôn ngữ hoặc các tiêu chuẩn JVM.

Mô hình bộ nhớ Java được định nghĩa chính thức về những thứ như tầm nhìn, nguyên tử, xảy ra-trước khi mối quan hệ và như vậy, điều này giải thích chính xác những gì đề phải xem những gì, những hành động phải xảy ra trước hành động khác và khác các mối quan hệ sử dụng một mô hình được xác định chính xác (toán học). Hành vi không được định nghĩa chính thức có thể là ngẫu nhiên hoặc được xác định rõ trong thực tế trên một số phần cứng và triển khai JVM - nhưng tất nhiên bạn không bao giờ nên dựa vào điều này, vì nó có thể thay đổi trong tương lai và bạn không bao giờ có thể chắc chắn nó đã được xác định rõ ngay từ đầu trừ khi bạn viết JVM và nhận thức rõ về ngữ nghĩa phần cứng. Vì vậy, văn bản mà bạn trích dẫn không chính thức mô tả những gì Java đảm bảo, nhưng đúng hơn là mô tả một số kiến ​​trúc giả định có thứ tự bộ nhớ rất yếu và khả năng hiển thị bảo đảm có thể đáp ứng các yêu cầu về mô hình bộ nhớ Java. Bất kỳ thảo luận thực tế về bộ nhớ đệm đỏ, bộ nhớ chính và như vậy rõ ràng là không thể áp dụng chung cho Java vì các khái niệm này không tồn tại trong ngôn ngữ trừu tượng và thông số mô hình bộ nhớ. Trong thực tế, các bảo đảm được cung cấp bởi mô hình bộ nhớ yếu hơn nhiều so với việc xả hoàn toàn - có mọi hoạt động liên quan đến nguyên tử, đồng thời hoặc khóa tuôn ra toàn bộ bộ nhớ cache sẽ cực kỳ tốn kém - và điều này hầu như chưa bao giờ được thực hiện trong thực tế. Thay vào đó, các hoạt động CPU nguyên tử đặc biệt được sử dụng, đôi khi kết hợp với các hướng dẫn memory barrier, giúp đảm bảo khả năng hiển thị và sắp xếp bộ nhớ. Vì vậy, sự không nhất quán giữa đồng bộ hóa không được đồng bộ hóa và "hoàn toàn xóa bộ nhớ đệm" được giải quyết bằng cách lưu ý rằng điều đầu tiên là đúng và thứ hai là không - không yêu cầu tuôn ra đầy đủ bởi mô hình bộ nhớ Java (và không có tuôn ra xảy ra trong thực tế).

Nếu mô hình bộ nhớ chính thức hơi quá nặng để tiêu hóa (bạn sẽ không đơn độc), bạn cũng có thể đi sâu vào chủ đề này bằng cách xem Doug Lea's cookbook, thực tế được liên kết trong JSR-133 Các câu hỏi thường gặp, nhưng xuất phát từ vấn đề từ góc độ phần cứng cụ thể, vì nó dành cho các nhà biên dịch trình biên dịch. Ở đó, họ nói về chính xác những rào cản nào cần thiết cho các hoạt động cụ thể, bao gồm đồng bộ hóa - và các rào cản được thảo luận ở đó có thể dễ dàng được ánh xạ tới phần cứng thực tế. Phần lớn ánh xạ thực tế được thảo luận ngay trong sách dạy nấu ăn.

-1

Vì y nằm ngoài phạm vi của phương thức được đồng bộ hóa, không có gì đảm bảo rằng những thay đổi đối với nó được hiển thị trong các chủ đề khác. Nếu bạn muốn đảm bảo rằng các thay đổi đối với y được thấy giống nhau trên tất cả các chuỗi, thì tất cả các chủ đề phải sử dụng đồng bộ hóa khi đọc/ghi y.

Nếu một số chủ đề thay đổi y theo cách được đồng bộ hóa và một số chủ đề khác thì bạn sẽ không nhận được hành vi không mong muốn. Tất cả trạng thái có thể thay đổi được chia sẻ giữa các luồng phải được đồng bộ hóa để có bất kỳ đảm bảo nào về việc thấy các thay đổi giữa các luồng. Tất cả quyền truy cập vào tất cả các luồng để chia sẻ trạng thái có thể thay đổi (biến) phải được đồng bộ hóa.

Và có, JVM đảm bảo rằng trong khi khóa được giữ không có chủ đề nào khác có thể nhập một vùng mã được bảo vệ bởi cùng một khóa.

+0

ne0sonic, tôi nghĩ bạn đã hiểu nhầm câu hỏi. Hãy sửa tôi nếu tôi sai. –

+1

Có _no_ đảm bảo về những gì xảy ra với các biến được sửa đổi bởi nhiều luồng bên ngoài ngữ cảnh bị khóa. Vì vậy, nếu y được sửa đổi bởi nhiều chủ đề biết giá trị sẽ là gì, có thể giá trị được sửa đổi lần cuối bởi chuỗi hiện tại, có thể một số giá trị từ một chuỗi khác. Nếu đó là chuỗi địa phương (chỉ được sửa đổi bởi một chuỗi đơn lẻ) thì bạn sẽ thấy giá trị cập nhật nhất. –

+0

Trong ví dụ ban đầu y không nằm trong khối được đồng bộ hóa để khi mã được thực hiện, JVM sẽ không làm bất cứ điều gì đặc biệt để đảm bảo y được cập nhật nếu được sửa đổi bởi một chuỗi khác. Nó có thể hoặc không thể thấy những thay đổi được thực hiện bởi các chủ đề khác. –

6

Cập nhật cho x cần đồng bộ hóa, nhưng không mua lại của các khóa rõ ràng giá trị của y cũng từ bộ nhớ cache? Tôi không thể tưởng tượng rằng đó là trường hợp , bởi vì nếu nó là đúng, thì các kỹ thuật như cắt khóa có thể không giúp được gì.

Tôi không chắc chắn, nhưng tôi nghĩ câu trả lời có thể là "có". Hãy xem xét điều này:

class Foo { 
    int x = 1; 
    int y = 1; 
    .. 
    void bar() { 
     synchronized (aLock) { 
      x = x + 1; 
     } 
     y = y + 1; 
    } 
} 

Bây giờ mã này không an toàn, tùy thuộc vào những gì xảy ra với phần còn lại của chương trình.Tuy nhiên, tôi nghĩ rằng mô hình bộ nhớ có nghĩa là giá trị của y được xem bởi bar không được lớn hơn giá trị "thực" tại thời điểm mua khóa. Điều đó có nghĩa là bộ nhớ cache phải được vô hiệu hóa cho y cũng như x.

Cũng có thể JVM đáng tin cậy phân tích mã để đảm bảo rằng y không được sửa đổi trong một khối đồng bộ sử dụng khóa giống nhau không?

Nếu khóa là this, phân tích này có thể khả thi khi tối ưu hóa toàn cầu khi tất cả các lớp đã được tải trước. (Tôi không nói rằng nó sẽ dễ dàng, hoặc đáng giá ...)

Trong trường hợp tổng quát hơn, vấn đề chứng minh rằng một khóa nhất định chỉ được sử dụng liên quan đến một trường hợp "sở hữu" cụ thể khó nắm bắt.

4

chúng tôi là nhà phát triển java, chúng tôi chỉ biết máy ảo chứ không phải máy thực!

hãy để tôi giả thuyết những gì đang xảy ra - nhưng tôi phải nói rằng tôi không biết mình đang nói về điều gì.

nói thread đang chạy trên CPU A với bộ nhớ cache A, thread B đang chạy trên CPU B với bộ nhớ cache B,

  1. chủ đề A đọc y; CPU A tìm nạp y từ bộ nhớ chính và lưu giá trị trong bộ nhớ cache A.

  2. chủ đề B gán giá trị mới cho 'y'. VM không phải cập nhật bộ nhớ chính tại thời điểm này; theo như đề tài B có liên quan, nó có thể được đọc/ghi trên một hình ảnh địa phương của 'y'; có lẽ 'y' không là gì ngoài đăng ký cpu.

  3. chủ đề B thoát khỏi khối đồng bộ và nhả màn hình. (khi nào và ở đâu nó vào khối không quan trọng). thread B đã cập nhật khá một số biến cho đến thời điểm này, bao gồm cả 'y'. Tất cả những cập nhật này phải được ghi vào bộ nhớ chính ngay bây giờ.

  4. CPU B ghi giá trị y mới để đặt 'y' trong bộ nhớ chính. (Tôi tưởng tượng rằng) gần như ngay lập tức, thông tin 'y chính được cập nhật' là có dây để bộ nhớ cache A, và bộ nhớ cache A vô hiệu hóa bản sao riêng của y. Điều đó phải đã xảy ra thực sự NHANH trên phần cứng.

  5. chủ đề A mua lại màn hình và nhập khối đồng bộ - tại thời điểm này, không cần phải làm gì liên quan đến bộ nhớ cache A. 'y' đã chuyển từ bộ nhớ cache A. khi luồng A đọc y lần nữa, nó mới từ bộ nhớ chính có giá trị mới được gán bởi B.

xem xét một biến z khác, được lưu trong bộ đệm A ở bước (1), nhưng không được cập nhật bởi chuỗi B ở bước (2). nó có thể tồn tại trong bộ nhớ đệm A tất cả các cách để bước (5). quyền truy cập 'z' không bị chậm lại do đồng bộ hóa.

nếu các câu trên có ý nghĩa, thì thực sự chi phí không cao lắm.


Ngoài bước (5): chủ đề A có thể có bộ nhớ cache riêng nhanh hơn bộ đệm A - có thể sử dụng đăng ký biến 'y' chẳng hạn. sẽ không bị vô hiệu bởi bước (4), do đó ở bước (5), luồng A phải xóa bộ nhớ cache của chính nó khi đồng bộ nhập. đó không phải là một hình phạt lớn mặc dù.

+0

Chỉ cần một lưu ý từ sự hiểu biết của tôi ... trong (3) bạn nói "Tất cả những cập nhật phải được ghi vào bộ nhớ chính bây giờ." Tôi nghĩ rằng điều này không đúng khi đọc các vấn đề với khóa kép được kiểm tra. Sự bắt đầu của một khối đồng bộ hóa đảm bảo rằng bạn thấy dữ liệu mới nhất nhưng không có tuôn ra rõ ràng ở cuối khối. Giữa đó và thực tế là các câu lệnh có thể được sắp xếp lại, bạn không thể mong đợi một cái nhìn nhất quán về dữ liệu trừ khi bạn cũng đang ở trong một khối đồng bộ. Các từ khóa dễ bay hơi thay đổi một số các ngữ nghĩa nhưng chủ yếu là đảm bảo đặt hàng và đỏ bừng. Các bản cập nhật – PSpeed

+0

phải được chuyển sang bộ nhớ chính ở cuối khối đồng bộ hóa, do đó khối đồng bộ tiếp theo được bắt đầu bởi một chuỗi khác có thể thấy các cập nhật đó. sắp xếp lại xảy ra, nhưng nó không thể vi phạm quan hệ "xảy ra-trước". – irreputable

+0

Không chính xác về mặt kỹ thuật, không thể thu hồi ...các dữ liệu không cần phải được flushed ở cuối. Một 'tuôn ra' hoàn toàn có thể xảy ra hợp pháp ngay trước khi chủ đề tiếp theo lấy khóa, nó không cần phải xảy ra ngay sau (hoặc trước) luồng cập nhật giải phóng nó để đáp ứng mô hình bộ nhớ. – Cowan

1

đảm bảo đồng bộ hóa, chỉ một chuỗi có thể nhập một khối mã. Nhưng nó không đảm bảo, các biến sửa đổi được thực hiện trong phần được đồng bộ hóa sẽ hiển thị với các luồng khác. Chỉ các luồng vào khối được đồng bộ mới được đảm bảo để xem các thay đổi. Hiệu ứng bộ nhớ của đồng bộ hóa trong Java có thể được so sánh với vấn đề Khóa đôi được kiểm tra đối với C++ và Java Khóa đôi được kiểm tra rộng rãi và được sử dụng như một phương pháp hiệu quả để thực hiện khởi tạo lười biếng trong môi trường nhiều luồng. Thật không may, nó sẽ không hoạt động đáng tin cậy theo cách độc lập trên nền tảng khi được triển khai trong Java mà không cần đồng bộ hóa bổ sung. Khi được thực hiện bằng các ngôn ngữ khác, chẳng hạn như C++, nó phụ thuộc vào mô hình bộ nhớ của bộ xử lý, thứ tự sắp xếp lại được trình biên dịch thực hiện và sự tương tác giữa trình biên dịch và thư viện đồng bộ. Vì không có điều nào trong số này được xác định bằng một ngôn ngữ như C++, bạn có thể nói rất ít về các tình huống mà nó sẽ hoạt động. Các rào cản bộ nhớ rõ ràng có thể được sử dụng để làm cho nó hoạt động trong C++, nhưng các rào cản này không có sẵn trong Java.

7

BeeOnRope là đúng, văn bản bạn trích dẫn sẽ tìm hiểu thêm về chi tiết triển khai điển hình hơn là những gì Mô hình bộ nhớ Java thực sự đảm bảo. Trong thực tế, bạn thường có thể thấy rằng y thực sự bị xóa khỏi bộ đệm CPU khi bạn đồng bộ hóa trên x (cũng vậy, nếu x trong ví dụ của bạn là một biến dễ bay hơi trong trường hợp đồng bộ hóa rõ ràng là không cần thiết để kích hoạt hiệu ứng). Điều này là bởi vì trên hầu hết CPU (lưu ý rằng đây là một hiệu ứng phần cứng, không phải là một cái gì đó mô tả JMM), bộ nhớ cache hoạt động trên các đơn vị được gọi là dòng bộ nhớ cache, thường dài hơn một từ máy (ví dụ 64 byte rộng). Vì chỉ có các dòng hoàn chỉnh mới có thể được nạp hoặc không hợp lệ trong bộ nhớ đệm, có nhiều khả năng x và y sẽ rơi vào cùng một dòng và việc xả một trong số chúng cũng sẽ tuôn ra một dòng khác.

Có thể viết điểm chuẩn cho biết hiệu ứng này. Tạo một lớp chỉ với hai trường int dễ bay hơi và cho phép hai luồng thực hiện một số thao tác (ví dụ: tăng dần trong một vòng lặp dài), một trên một trong các trường và một trên một trường khác. Thời gian hoạt động. Sau đó, chèn 16 trường int vào giữa hai trường gốc và lặp lại phép thử (16 * 4 = 64). Lưu ý rằng một mảng chỉ là một tham chiếu để một mảng gồm 16 phần tử sẽ không thực hiện thủ thuật. Bạn có thể thấy sự cải thiện đáng kể về hiệu suất bởi vì các thao tác trên một trường sẽ không ảnh hưởng đến một trường khác nữa. Cho dù điều này làm việc cho bạn sẽ phụ thuộc vào việc thực hiện JVM và kiến ​​trúc bộ vi xử lý. Tôi đã thấy điều này trong thực tế trên Sun JVM và một máy tính xách tay x64 điển hình, sự khác biệt về hiệu suất là bởi một yếu tố của nhiều lần.

+1

Khuôn khổ Disruptor (http://code.google.com/p/disruptor/) sử dụng thủ thuật căn chỉnh này trong thực tế. – gubby

+0

Nếu có thể, bạn có thể xây dựng điểm chuẩn một chút không? Tốt hơn với một mã. – sgp15

+0

Tôi đã thấy điểm chuẩn trong bản trình bày trực tiếp, tôi không thể tìm thấy bất kỳ trang trình bày nào cho nó một cách không may. Nhưng ý tưởng là: do cách dữ liệu được sắp xếp trong bộ nhớ vật lý và cách bộ nhớ cache CPU được tổ chức, hiệu ứng hiển thị bộ nhớ và chi phí hiệu suất của chúng có thể ảnh hưởng không chỉ đến trường được đánh dấu là trường dễ bay hơi mà còn ở bên cạnh. Tất nhiên đây là một tác dụng phụ phụ thuộc vào việc thực hiện nên không dựa vào nó. Tôi mong đợi disruptor phụ thuộc chỉ vào "khả năng hiển thị piggybacking" đó là hoàn toàn hợp pháp theo JLS và không phụ thuộc phần cứng, nhưng tôi không chắc chắn 100%. –

3

bạn có thể muốn kiểm tra tài liệu jdk6.0 http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility

Memory quán Thuộc tính Chương 17 của ngôn ngữ Java Specification xác định xảy ra-trước khi mối quan hệ về hoạt động bộ nhớ chẳng hạn như đọc và viết của các biến chia sẻ. Kết quả của việc viết bởi một luồng được đảm bảo chỉ hiển thị cho một chuỗi khác đọc nếu thao tác ghi xảy ra trước khi thao tác đọc. Các cấu trúc đồng bộ và dễ bay hơi, cũng như các phương thức Thread.start() và Thread.join(), có thể hình thành các mối quan hệ xảy ra trước khi xảy ra. Cụ thể:

  • Mỗi hành động trong một chủ đề xảy ra trước mỗi hành động trong chuỗi đó đến sau theo thứ tự của chương trình.
  • Mở khóa (khối đồng bộ hoặc lối thoát phương thức) của màn hình xảy ra trước mỗi khóa tiếp theo (khối đồng bộ hoặc mục nhập phương thức) của cùng một màn hình đó. Và bởi vì mối quan hệ xảy ra trước đó là transitive, tất cả các hành động của một luồng trước khi mở khóa xảy ra trước khi tất cả các hành động tiếp theo với bất kỳ luồng nào khóa màn hình đó.
  • Việc ghi vào trường dễ bay hơi xảy ra trước mỗi lần đọc tiếp theo của cùng trường đó. Viết và đọc của các lĩnh vực dễ bay hơi có tác dụng nhất quán bộ nhớ tương tự như vào và thoát khỏi màn hình, nhưng không kéo theo khóa loại trừ lẫn nhau.
  • Cuộc gọi bắt đầu trên một chuỗi diễn ra trước khi bất kỳ hành động nào trong chuỗi bắt đầu.
  • Tất cả những hành động trong một thread xảy ra-trước khi bất kỳ thread khác trả về thành công từ một gia về chủ đề đó

Vì vậy, như đã nêu tại điểm đánh dấu trên: Tất cả những thay đổi đó xảy ra trước khi một unlock xảy ra trên một màn hình là hiển thị cho tất cả các chủ đề đó (và trong đó có khối đồng bộ hóa riêng) có khóa trên cùng một màn hình.Điều này phù hợp với ngữ nghĩa xảy ra trước đó của Java. Do đó, tất cả các thay đổi được thực hiện cho y cũng sẽ bị xóa sang bộ nhớ chính khi một số luồng khác mua lại màn hình trên 'aLock'.

Các vấn đề liên quan