Sự khác biệt giữa nguyên tử / dễ bay hơi / đồng bộ là gì?


297

Làm thế nào để nguyên tử / dễ bay hơi / đồng bộ hóa làm việc trong nội bộ?

Sự khác biệt giữa các khối mã sau đây là gì?

Mã 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

Mã 2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

Mã 3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

volatilehoạt động theo cách sau? Là

volatile int i = 0;
void incIBy5() {
    i += 5;
}

tương đương với

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

Tôi nghĩ rằng hai luồng không thể nhập một khối được đồng bộ hóa cùng một lúc ... tôi có đúng không? Nếu điều này là đúng thì làm thế nào mà không atomic.incrementAndGet()làm việc synchronized? Và nó có an toàn không?

Và sự khác biệt giữa đọc và viết nội bộ với các biến / biến nguyên tử là gì? Tôi đọc trong một số bài viết rằng các chủ đề có một bản sao cục bộ của các biến - đó là gì?


5
Điều đó tạo ra rất nhiều câu hỏi, với mã thậm chí không được biên dịch. Có lẽ bạn nên đọc một cuốn sách hay, như Java đồng thời trong thực tiễn.
JB Nizet 17/03/2016

4
@JBNizet bạn nói đúng !!! Tôi có cuốn sách đó, nó không có khái niệm nguyên tử ngắn gọn và tôi không nhận được một số khái niệm về điều đó. về lời nguyền đó là lỗi của tôi không phải của tác giả.
hardik

4
Bạn không thực sự phải quan tâm đến cách thức triển khai (và nó thay đổi theo HĐH). Điều bạn phải hiểu là hợp đồng: giá trị được tăng lên một cách nguyên tử và tất cả các luồng khác được đảm bảo để xem giá trị mới.
JB Nizet 17/03/2016

Câu trả lời:


392

Bạn đang hỏi cụ thể về cách họ làm việc nội bộ , vì vậy bạn ở đây:

Không đồng bộ hóa

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Về cơ bản, nó đọc giá trị từ bộ nhớ, tăng nó và đưa trở lại bộ nhớ. Điều này hoạt động trong một luồng đơn nhưng ngày nay, trong thời đại của bộ nhớ cache đa lõi, đa CPU, đa cấp, nó sẽ không hoạt động chính xác. Trước hết, nó giới thiệu điều kiện cuộc đua (một số chủ đề có thể đọc giá trị cùng một lúc), nhưng cũng có vấn đề về khả năng hiển thị. Giá trị chỉ có thể được lưu trữ trong bộ nhớ CPU " cục bộ " (một số bộ đệm) và không hiển thị cho các CPU / lõi khác (và do đó - các luồng). Đây là lý do tại sao nhiều người đề cập đến bản sao cục bộ của một biến trong một luồng. Nó rất không an toàn. Xem xét mã dừng phổ biến nhưng bị hỏng này:

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

Thêm volatilevào stoppedbiến và nó hoạt động tốt - nếu bất kỳ luồng nào khác sửa đổi stoppedbiến qua pleaseStop()phương thức, bạn được đảm bảo sẽ thấy thay đổi đó ngay lập tức trong while(!stopped)vòng lặp của luồng làm việc . BTW đây cũng không phải là một cách hay để làm gián đoạn một luồng, xem: Cách dừng một luồng đang chạy mãi mà không sử dụngDừng một luồng java cụ thể .

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

Các AtomicIntegersử dụng lớp CAS ( so sánh-và-swap ) hoạt động CPU ở mức độ thấp (không đồng bộ cần thiết!) Chúng cho phép bạn sửa đổi một biến đặc biệt chỉ khi giá trị hiện tại bằng cái gì khác (và được trả về thành công). Vì vậy, khi bạn thực thi, getAndIncrement()nó thực sự chạy trong một vòng lặp (thực hiện đơn giản hóa thực tế):

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

Về cơ bản: đọc; cố gắng lưu trữ giá trị gia tăng; nếu không thành công (giá trị không còn bằng current), hãy đọc và thử lại. Điều compareAndSet()này được thực hiện trong mã gốc (lắp ráp).

volatile không đồng bộ

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

Mã này không đúng. Nó khắc phục vấn đề về khả năng hiển thị ( volatileđảm bảo các luồng khác có thể thấy thay đổi được thực hiện counter) nhưng vẫn có điều kiện cuộc đua. Điều này đã được giải thích nhiều lần: trước / sau tăng không phải là nguyên tử.

Tác dụng phụ duy nhất của việc volatile" xóa " bộ đệm để tất cả các bên khác thấy phiên bản mới nhất của dữ liệu. Điều này là quá nghiêm ngặt trong hầu hết các tình huống; đó là lý do tại sao volatilekhông được mặc định

volatile không đồng bộ hóa (2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

Vấn đề tương tự như trên, nhưng thậm chí còn tồi tệ hơn vì ikhông phải private. Điều kiện cuộc đua vẫn còn. Tại sao nó là một vấn đề? Nếu, giả sử, hai luồng chạy mã này đồng thời, đầu ra có thể + 5hoặc + 10. Tuy nhiên, bạn được đảm bảo để xem sự thay đổi.

Nhiều độc lập synchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

Thật ngạc nhiên, mã này là không chính xác là tốt. Trên thực tế, nó hoàn toàn sai. Trước hết, bạn đang đồng bộ hóa i, sắp được thay đổi (hơn nữa, ilà một nguyên thủy, vì vậy tôi đoán bạn đang đồng bộ hóa trên một tạm thời Integerđược tạo thông qua hộp tự động ...) Hoàn toàn sai sót. Bạn cũng có thể viết:

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

Không có hai luồng có thể vào cùng một synchronizedkhối với cùng một khóa . Trong trường hợp này (và tương tự trong mã của bạn), đối tượng khóa thay đổi theo mỗi lần thực thi, do đó, synchronizedkhông có hiệu lực.

Ngay cả khi bạn đã sử dụng một biến cuối cùng (hoặc this) để đồng bộ hóa, mã vẫn không chính xác. Hai luồng đầu tiên có thể đọc ithành tempđồng bộ (có cùng giá trị cục bộ temp), sau đó đầu tiên gán giá trị mới cho i(giả sử, từ 1 đến 6) và một luồng khác thực hiện cùng một điều (từ 1 đến 6).

Việc đồng bộ hóa phải trải dài từ đọc đến gán giá trị. Đồng bộ hóa đầu tiên của bạn không có hiệu lực (đọc một intlà nguyên tử) và thứ hai là tốt. Theo tôi, đây là những hình thức chính xác:

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}

10
Điều duy nhất tôi muốn thêm là JVM sao chép các giá trị biến vào các thanh ghi để hoạt động trên chúng. Điều này có nghĩa là các luồng chạy trên một CPU / lõi đơn vẫn có thể thấy các giá trị khác nhau cho một biến không biến động.
David Harkness

@thomasz: được so sánhAndset (hiện tại, hiện tại + 1) được đồng bộ hóa ?? nếu không hơn những gì xảy ra khi hai luồng đang thực thi phương thức này cùng một lúc ??
hardik

@Hardik: compareAndSetchỉ là một trình bao bọc mỏng xung quanh hoạt động CAS. Tôi đi vào một số chi tiết trong câu trả lời của tôi.
Tomasz Nurkiewicz

1
@thomsasz: ok, tôi lướt qua câu hỏi liên kết này và được trả lời bởi jon skeet, anh ta nói "chủ đề không thể đọc một biến dễ bay hơi mà không kiểm tra xem có bất kỳ chủ đề nào khác đã thực hiện ghi không." Nhưng điều gì xảy ra nếu một luồng nằm giữa thao tác viết và luồng thứ hai đang đọc nó !! Liệu tôi có sai ?? không phải là điều kiện cuộc đua về hoạt động nguyên tử ??
hardik

3
@Hardik: vui lòng tạo một câu hỏi khác để nhận được nhiều phản hồi hơn về những gì bạn đang hỏi, ở đây chỉ có bạn và tôi và các bình luận không phù hợp để đặt câu hỏi. Đừng quên đăng một liên kết đến một câu hỏi mới ở đây để tôi có thể theo dõi.
Tomasz Nurkiewicz

61

Khai báo một biến là dễ bay hơi có nghĩa là sửa đổi giá trị của nó ngay lập tức ảnh hưởng đến việc lưu trữ bộ nhớ thực cho biến đó. Trình biên dịch không thể tối ưu hóa bất kỳ tham chiếu nào được thực hiện cho biến. Điều này đảm bảo rằng khi một luồng sửa đổi biến, tất cả các luồng khác sẽ thấy giá trị mới ngay lập tức. (Điều này không được đảm bảo cho các biến không bay hơi.)

Khai báo một biến nguyên tử đảm bảo rằng các hoạt động được thực hiện trên biến đó xảy ra theo kiểu nguyên tử, tức là tất cả các bước phụ của hoạt động được hoàn thành trong luồng mà chúng được thực thi và không bị gián đoạn bởi các luồng khác. Ví dụ, một phép toán tăng và kiểm tra yêu cầu biến phải được tăng lên và sau đó được so sánh với giá trị khác; một hoạt động nguyên tử đảm bảo rằng cả hai bước này sẽ được hoàn thành như thể chúng là một hoạt động không thể chia tách / không bị gián đoạn.

Đồng bộ hóa tất cả các truy cập vào một biến chỉ cho phép một luồng duy nhất tại một thời điểm truy cập vào biến đó và buộc tất cả các luồng khác phải đợi luồng truy cập đó giải phóng quyền truy cập của nó vào biến đó.

Truy cập đồng bộ tương tự như truy cập nguyên tử, nhưng các hoạt động nguyên tử thường được thực hiện ở cấp độ lập trình thấp hơn. Ngoài ra, hoàn toàn có thể chỉ đồng bộ hóa một số quyền truy cập vào một biến và cho phép các truy cập khác không được đồng bộ hóa (ví dụ: đồng bộ hóa tất cả ghi vào một biến nhưng không đọc được từ đó).

Tính nguyên tử, đồng bộ hóa và tính biến động là các thuộc tính độc lập, nhưng thường được sử dụng kết hợp để thực thi hợp tác luồng thích hợp để truy cập các biến.

Phụ lục (tháng 4 năm 2016)

Truy cập đồng bộ vào một biến thường được thực hiện bằng cách sử dụng màn hình hoặc semaphore . Đây là các cơ chế mutex cấp thấp (loại trừ lẫn nhau) cho phép một luồng có được quyền kiểm soát một biến hoặc khối mã riêng, buộc tất cả các luồng khác phải chờ nếu chúng cũng cố gắng có được cùng một mutex. Khi chủ đề sở hữu giải phóng mutex, một chủ đề khác có thể lần lượt nhận được mutex.

Phụ lục (tháng 7 năm 2016)

Đồng bộ hóa xảy ra trên một đối tượng . Điều này có nghĩa là việc gọi một phương thức được đồng bộ hóa của một lớp sẽ khóa thisđối tượng của cuộc gọi. Các phương thức đồng bộ tĩnh sẽ tự khóa Classđối tượng.

Tương tự, nhập một khối được đồng bộ hóa yêu cầu khóa thisđối tượng của phương thức.

Điều này có nghĩa rằng một phương pháp đồng bộ (hoặc khối) có thể được thực hiện trong nhiều chủ đề cùng một lúc nếu chúng được khóa trên khác nhau đối tượng, nhưng chỉ có một chủ đề có thể thực hiện một phương pháp đồng bộ (hoặc khối) tại một thời điểm cho bất kỳ cho đơn đối tượng.


25

bay hơi:

volatilelà một từ khóa. volatilebuộc tất cả các luồng để lấy giá trị mới nhất của biến từ bộ nhớ chính thay vì bộ đệm. Không cần khóa để truy cập các biến dễ bay hơi. Tất cả các chủ đề có thể truy cập giá trị biến dễ bay hơi cùng một lúc.

Sử dụng volatilecác biến làm giảm nguy cơ lỗi nhất quán bộ nhớ, bởi vì bất kỳ ghi vào biến dễ bay hơi nào cũng thiết lập mối quan hệ xảy ra trước khi đọc các biến tiếp theo của cùng biến đó.

Điều này có nghĩa là những thay đổi đối với một volatilebiến luôn được hiển thị cho các luồng khác . Hơn nữa, điều đó cũng có nghĩa là khi một luồng đọc một volatilebiến, nó không chỉ thấy sự thay đổi mới nhất đối với biến động, mà còn là tác dụng phụ của mã dẫn đến thay đổi .

Khi nào nên sử dụng: Một luồng sửa đổi dữ liệu và các luồng khác phải đọc giá trị mới nhất của dữ liệu. Các luồng khác sẽ thực hiện một số hành động nhưng chúng sẽ không cập nhật dữ liệu .

Nguyên tửXXX:

AtomicXXXcác lớp hỗ trợ lập trình luồng an toàn khóa trên các biến đơn. Các AtomicXXXlớp này (như AtomicInteger) giải quyết các lỗi không nhất quán bộ nhớ / tác dụng phụ của việc sửa đổi các biến dễ bay hơi, đã được truy cập trong nhiều luồng.

Khi nào nên sử dụng: Nhiều luồng có thể đọc và sửa đổi dữ liệu.

đã đồng bộ hóa:

synchronizedlà từ khóa được sử dụng để bảo vệ một phương thức hoặc khối mã. Bằng cách làm cho phương thức được đồng bộ hóa có hai tác dụng:

  1. Đầu tiên, hai synchronizedphương thức trên cùng một đối tượng không thể xen kẽ. Khi một luồng đang thực thi một synchronizedphương thức cho một đối tượng, tất cả các luồng khác gọi synchronizedcác phương thức cho cùng một khối đối tượng (tạm dừng thực thi) cho đến khi luồng đầu tiên được thực hiện với đối tượng.

  2. Thứ hai, khi một synchronizedphương thức thoát ra, nó sẽ tự động thiết lập mối quan hệ xảy ra trước khi có bất kỳ lời gọi tiếp theo nào của một synchronizedphương thức cho cùng một đối tượng. Điều này đảm bảo rằng những thay đổi về trạng thái của đối tượng được hiển thị cho tất cả các luồng.

Khi nào nên sử dụng: Nhiều luồng có thể đọc và sửa đổi dữ liệu. Logic kinh doanh của bạn không chỉ cập nhật dữ liệu mà còn thực hiện các hoạt động nguyên tử

AtomicXXXlà tương đương với volatile + synchronizedmặc dù việc thực hiện là khác nhau. AmtomicXXXmở rộng volatilecác biến + compareAndSetphương thức nhưng không sử dụng đồng bộ hóa.

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

Sự khác biệt giữa dễ bay hơi và đồng bộ hóa trong Java

Boolean dễ bay hơi so với AtomicBoolean

Các bài viết hay để đọc: (Nội dung trên được lấy từ các trang tài liệu này)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concản/atomic/package-summary.html


2
Đây là câu trả lời đầu tiên thực sự đề cập đến ngữ nghĩa xảy ra - trước ngữ nghĩa của các từ khóa / tính năng được mô tả, rất quan trọng trong việc hiểu cách chúng thực sự ảnh hưởng đến việc thực thi mã. Câu trả lời cao hơn bỏ lỡ khía cạnh này.
jhyot

5

Tôi biết rằng hai luồng không thể nhập vào khối Đồng bộ hóa cùng một lúc

Hai luồng không thể nhập một khối được đồng bộ hóa trên cùng một đối tượng hai lần. Điều này có nghĩa là hai luồng có thể nhập cùng một khối trên các đối tượng khác nhau. Sự nhầm lẫn này có thể dẫn đến mã như thế này.

private Integer i = 0;

synchronized(i) {
   i++;
}

Điều này sẽ không hoạt động như mong đợi vì nó có thể bị khóa trên một đối tượng khác nhau mỗi lần.

nếu điều này đúng hơn Làm thế nào nguyên tử này.incrementAndGet () hoạt động mà không đồng bộ hóa ?? và chủ đề có an toàn không ??

Đúng. Nó không sử dụng khóa để đạt được an toàn chủ đề.

Nếu bạn muốn biết làm thế nào họ làm việc chi tiết hơn, bạn có thể đọc mã cho họ.

Và sự khác biệt giữa đọc và ghi nội bộ với Biến thiên / Biến nguyên tử dễ bay hơi là gì ??

Lớp nguyên tử sử dụng các trường dễ bay hơi . Không có sự khác biệt trong lĩnh vực này. Sự khác biệt là các hoạt động được thực hiện. Các lớp nguyên tử sử dụng các hoạt động so sánhAndSwap hoặc CAS.

tôi đọc trong một số bài viết rằng chủ đề có bản sao biến cục bộ đó là gì ??

Tôi chỉ có thể giả sử rằng nó đề cập đến thực tế là mỗi CPU có chế độ xem bộ nhớ được lưu trong bộ nhớ cache riêng có thể khác với mọi CPU khác. Để đảm bảo CPU của bạn có chế độ xem dữ liệu nhất quán, bạn cần sử dụng các kỹ thuật an toàn luồng.

Đây chỉ là một vấn đề khi bộ nhớ được chia sẻ ít nhất một luồng cập nhật nó.


@Aniket Thakur bạn có chắc về điều đó không? Số nguyên là bất biến. Vì vậy, i ++ có thể sẽ tự động bỏ hộp giá trị int, tăng nó và sau đó tạo một Integer mới, không giống như trước đây. Hãy thử làm i cuối cùng và bạn sẽ gặp lỗi trình biên dịch khi gọi i ++.
fuemf5

2

Đồng bộ hóa Vs nguyên tử Vs dễ bay hơi:

  • Dễ bay hơi và nguyên tử chỉ được áp dụng trên biến, trong khi Đồng bộ hóa áp dụng trên phương thức.
  • Dễ bay hơi đảm bảo về khả năng hiển thị không phải là nguyên tử / tính nhất quán của vật thể, trong khi các yếu tố khác đều đảm bảo về khả năng hiển thị và tính nguyên tử.
  • Lưu trữ biến động dễ dàng trong RAM và truy cập nhanh hơn nhưng chúng tôi không thể đạt được an toàn chủ đề hoặc đồng bộ hóa từ khóa đồng bộ hóa.
  • Đồng bộ hóa được thực hiện dưới dạng khối đồng bộ hoặc phương thức đồng bộ trong khi cả hai không. Chúng tôi có thể xâu chuỗi nhiều dòng mã an toàn với sự trợ giúp của từ khóa được đồng bộ hóa trong khi với cả hai chúng tôi không thể đạt được như nhau.
  • Đã đồng bộ hóa có thể khóa cùng một đối tượng lớp hoặc đối tượng lớp khác nhau trong khi cả hai không thể.

Xin vui lòng sửa cho tôi nếu bất cứ điều gì tôi bỏ lỡ.


1

Đồng bộ hóa dễ bay hơi + là một giải pháp chứng minh ngu ngốc cho một hoạt động (câu lệnh) hoàn toàn nguyên tử bao gồm nhiều hướng dẫn cho CPU.

Nói ví dụ: volility int i = 2; i ++, không có gì ngoài i = i + 1; mà làm cho tôi là giá trị 3 trong bộ nhớ sau khi thực hiện câu lệnh này. Điều này bao gồm đọc giá trị hiện có từ bộ nhớ cho i (là 2), tải vào thanh ghi tích lũy CPU và thực hiện tính toán bằng cách tăng giá trị hiện có với một (2 + 1 = 3 trong bộ tích lũy) và sau đó ghi lại giá trị tăng đó trở lại ký ức. Các hoạt động này không đủ nguyên tử mặc dù giá trị của i là không ổn định. tôi không ổn định chỉ đảm bảo rằng một SINGLE đọc / ghi từ bộ nhớ là nguyên tử chứ không phải với NHIỀU. Do đó, chúng ta cần phải đồng bộ hóa xung quanh i ++ để giữ cho nó trở thành tuyên bố nguyên tử bằng chứng. Hãy nhớ thực tế rằng một tuyên bố bao gồm nhiều tuyên bố.

Hy vọng lời giải thích là đủ rõ ràng.


1

Công cụ sửa đổi dễ bay hơi Java là một ví dụ về một cơ chế đặc biệt để đảm bảo rằng giao tiếp xảy ra giữa các luồng. Khi một luồng ghi vào một biến dễ bay hơi và một luồng khác nhìn thấy ghi đó, luồng đầu tiên sẽ báo cho luồng thứ hai về tất cả các nội dung của bộ nhớ cho đến khi nó thực hiện ghi vào biến dễ bay hơi đó.

Các hoạt động nguyên tử được thực hiện trong một đơn vị nhiệm vụ duy nhất mà không có sự can thiệp từ các hoạt động khác. Hoạt động nguyên tử là cần thiết trong môi trường đa luồng để tránh sự không nhất quán dữ liệu.

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.