Câu trả lời:
Có hai cách sử dụng chính của AtomicInteger
:
Là một bộ đếm nguyên tử ( incrementAndGet()
, v.v.) có thể được sử dụng đồng thời bởi nhiều luồng
Là một nguyên thủy hỗ trợ hướng dẫn so sánh và trao đổi ( compareAndSet()
) để thực hiện các thuật toán không chặn.
Dưới đây là một ví dụ về trình tạo số ngẫu nhiên không chặn từ Thực hành đồng thời Java của Brian Gotetz :
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed;
AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
}
public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
...
}
Như bạn có thể thấy, về cơ bản nó hoạt động gần giống như cách incrementAndGet()
, nhưng thực hiện phép tính tùy ý ( calculateNext()
) thay vì tăng (và xử lý kết quả trước khi trả về).
read
và các write that value + 1
hoạt động, thì điều này được phát hiện thay vì ghi đè lên bản cập nhật cũ (tránh vấn đề "mất cập nhật"). Đây thực sự là một trường hợp đặc biệt compareAndSet
- nếu giá trị cũ là 2
, lớp thực sự gọi compareAndSet(2, 3)
- vì vậy nếu một luồng khác đã sửa đổi giá trị trong thời gian đó, phương thức tăng sẽ khởi động lại hiệu quả ngay từ đầu.
Ví dụ đơn giản tuyệt đối nhất mà tôi có thể nghĩ đến là thực hiện tăng hoạt động nguyên tử.
Với số liệu chuẩn:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
Với AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Cách thứ hai là một cách rất đơn giản để thực hiện các hiệu ứng đột biến đơn giản (đặc biệt là đếm hoặc lập chỉ mục duy nhất) mà không cần phải dùng đến việc đồng bộ hóa tất cả các truy cập.
Logic không đồng bộ hóa phức tạp hơn có thể được sử dụng bằng cách sử dụng compareAndSet()
như một loại khóa tối ưu - lấy giá trị hiện tại, tính kết quả dựa trên điều này, đặt kết quả này giá trị iff vẫn là đầu vào được sử dụng để thực hiện phép tính, nhưng bắt đầu lại - nhưng các ví dụ đếm rất hữu ích và tôi thường sử dụng AtomicIntegers
để đếm và các trình tạo duy nhất trên toàn VM nếu có bất kỳ gợi ý nào về nhiều luồng được tham gia, bởi vì chúng rất dễ làm việc với tôi gần như coi đó là tối ưu hóa sớm để sử dụng đơn giản ints
.
Mặc dù bạn hầu như luôn có thể đạt được các đảm bảo đồng bộ hóa giống nhau ints
và các synchronized
khai báo phù hợp , nhưng điều tuyệt vời AtomicInteger
là sự an toàn của luồng được tích hợp vào chính đối tượng thực tế, thay vì bạn cần phải lo lắng về các xen kẽ có thể và các màn hình được giữ trong mọi phương thức Điều đó xảy ra để truy cập int
giá trị. Việc vô tình vi phạm chủ đề an toàn khi gọi điện sẽ khó getAndIncrement()
hơn nhiều so với khi quay lại i++
và ghi nhớ (hoặc không) để có được bộ màn hình chính xác trước đó.
Nếu bạn xem các phương thức mà AtomicInteger có, bạn sẽ nhận thấy rằng chúng có xu hướng tương ứng với các hoạt động phổ biến trên ints. Ví dụ:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
là phiên bản an toàn của chủ đề này:
static int i;
// Later, in a thread
int current = ++i;
Lập bản đồ các phương pháp như thế này:
++i
là i.incrementAndGet()
i++
được i.getAndIncrement()
--i
là i.decrementAndGet()
i--
được i.getAndDecrement()
i = x
là i.set(x)
x = i
đượcx = i.get()
Có nhiều phương pháp tiện lợi khác, như compareAndSet
hoặcaddAndGet
Việc sử dụng chính AtomicInteger
là khi bạn ở trong một bối cảnh đa luồng và bạn cần thực hiện các hoạt động an toàn của luồng trên một số nguyên mà không cần sử dụng synchronized
. Việc gán và truy xuất trên kiểu nguyên thủy int
đã là nguyên tử nhưng AtomicInteger
đi kèm với nhiều hoạt động không phải là nguyên tử int
.
Đơn giản nhất là getAndXXX
hoặc xXXAndGet
. Ví dụ, getAndIncrement()
một nguyên tử tương đương với i++
nguyên tử không phải là nguyên tử vì nó thực sự là một cách rút gọn cho ba thao tác: truy xuất, bổ sung và gán. compareAndSet
là rất hữu ích để thực hiện semaphores, khóa, chốt, vv
Sử dụng AtomicInteger
nhanh hơn và dễ đọc hơn so với thực hiện tương tự bằng cách sử dụng đồng bộ hóa.
Một bài kiểm tra đơn giản:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
Trên PC của tôi với Java 1.6, thử nghiệm nguyên tử chạy trong 3 giây trong khi thử nghiệm được đồng bộ hóa chạy trong khoảng 5,5 giây. Vấn đề ở đây là thao tác để đồng bộ hóa ( notAtomic++
) thực sự ngắn. Vì vậy, chi phí của việc đồng bộ hóa thực sự quan trọng so với hoạt động.
Bên cạnh tính nguyên tử, AtomicInteger có thể được sử dụng như một phiên bản có thể thay đổi của Integer
ví dụ trong Map
s làm giá trị.
AtomicInteger
làm khóa bản đồ, bởi vì nó sử dụng equals()
triển khai mặc định , gần như chắc chắn không phải là điều bạn mong đợi về ngữ nghĩa nếu được sử dụng trong bản đồ.
Ví dụ, tôi có một thư viện tạo các thể hiện của một số lớp. Mỗi trường hợp này phải có một ID số nguyên duy nhất, vì các trường hợp này đại diện cho các lệnh được gửi đến máy chủ và mỗi lệnh phải có một ID duy nhất. Vì nhiều luồng được phép gửi lệnh đồng thời, tôi sử dụng một AtomicInteger để tạo các ID đó. Một cách tiếp cận khác là sử dụng một số loại khóa và số nguyên thông thường, nhưng cả hai đều chậm hơn và kém thanh lịch.
Giống như gabuzo đã nói, đôi khi tôi sử dụng AtomicIntegers khi tôi muốn truyền int bằng cách tham chiếu. Đó là một lớp tích hợp có mã dành riêng cho kiến trúc, vì vậy nó dễ dàng hơn và có khả năng được tối ưu hóa hơn bất kỳ MutableInteger nào mà tôi có thể nhanh chóng viết mã. Điều đó nói rằng, nó cảm thấy như một sự lạm dụng của lớp học.
Trong Java 8 lớp nguyên tử đã được mở rộng với hai hàm thú vị:
Cả hai đều đang sử dụng updateFunction để thực hiện cập nhật giá trị nguyên tử. Sự khác biệt là cái đầu tiên trả về giá trị cũ và cái thứ hai trả về giá trị mới. Bản cập nhật có thể được triển khai để thực hiện các thao tác "so sánh và thiết lập" phức tạp hơn so với tiêu chuẩn. Ví dụ, nó có thể kiểm tra bộ đếm nguyên tử không xuống dưới 0, thông thường nó sẽ yêu cầu đồng bộ hóa và ở đây mã không bị khóa:
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
Mã được lấy từ ví dụ nguyên tử Java .
Tôi thường sử dụng AtomicInteger khi tôi cần cung cấp Id cho các đối tượng có thể được tích lũy hoặc tạo từ nhiều luồng và tôi thường sử dụng nó như một thuộc tính tĩnh trên lớp mà tôi truy cập trong hàm tạo của các đối tượng.
Bạn có thể triển khai các khóa không chặn bằng cách sử dụng so sánhAndSwap (CAS) trên các số nguyên hoặc độ dài nguyên tử. Tài liệu Bộ nhớ Giao dịch Phần mềm "Tl2" mô tả điều này:
Chúng tôi liên kết một khóa ghi phiên bản đặc biệt với mọi vị trí bộ nhớ được giao dịch. Ở dạng đơn giản nhất, khóa ghi được phiên bản là một spinlock từ duy nhất sử dụng thao tác CAS để thu được khóa và một cửa hàng để phát hành nó. Vì người ta chỉ cần một bit duy nhất để chỉ ra rằng khóa đã được thực hiện, chúng tôi sử dụng phần còn lại của từ khóa để giữ số phiên bản.
Những gì nó được mô tả là lần đầu tiên đọc số nguyên tử. Chia phần này thành một khóa bị bỏ qua và số phiên bản. Cố gắng CAS viết nó khi bit-bit bị xóa với số phiên bản hiện tại vào bộ khóa bit và số phiên bản tiếp theo. Lặp lại cho đến khi bạn thành công và bạn là chủ đề sở hữu khóa. Mở khóa bằng cách đặt số phiên bản hiện tại khi đã xóa khóa. Bài viết mô tả sử dụng số phiên bản trong ổ khóa để phối hợp các luồng có tập đọc nhất quán khi chúng viết.
Bài viết này mô tả rằng các bộ xử lý có hỗ trợ phần cứng để so sánh và trao đổi hoạt động làm cho rất hiệu quả. Nó cũng tuyên bố:
các bộ đếm dựa trên CAS không chặn sử dụng các biến nguyên tử có hiệu suất tốt hơn các bộ đếm dựa trên khóa trong sự tranh chấp từ thấp đến trung bình
Điều quan trọng là họ cho phép truy cập đồng thời và sửa đổi một cách an toàn. Chúng thường được sử dụng làm bộ đếm trong môi trường đa luồng - trước khi giới thiệu, đây phải là một lớp người dùng viết bao bọc các phương thức khác nhau trong các khối được đồng bộ hóa.
Tôi đã sử dụng AtomicInteger để giải quyết vấn đề của Philosopher.
Trong giải pháp của tôi, các trường hợp AtomicInteger đã được sử dụng để đại diện cho các nhánh, có hai cần thiết cho mỗi triết gia. Mỗi triết gia được xác định là một số nguyên, từ 1 đến 5. Khi một ngã ba được sử dụng bởi một triết gia, AtomicInteger giữ giá trị của triết gia, từ 1 đến 5, nếu không thì ngã ba không được sử dụng nên giá trị của AtomicInteger là -1 .
Sau đó, AtomicInteger cho phép kiểm tra xem một ngã ba có miễn phí không, giá trị == - 1 và đặt nó cho chủ sở hữu của ngã ba nếu miễn phí, trong một hoạt động nguyên tử. Xem mã dưới đây.
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
Bởi vì phương thức so sánh không chặn, nên nó sẽ tăng thông lượng, công việc được thực hiện nhiều hơn. Như bạn có thể biết, vấn đề Ăn uống triết gia được sử dụng khi cần kiểm soát truy cập vào tài nguyên, ví dụ như dĩa, giống như một quy trình cần tài nguyên để tiếp tục thực hiện công việc.
Ví dụ đơn giản cho hàm so sánhAndset ():
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val = new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(0, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
Giá trị được in là: giá trị trước: 0 Giá trị đã được cập nhật và đó là 6 Ví dụ đơn giản khác:
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val
= new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(10, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
Giá trị được in là: Giá trị trước: 0 Giá trị không được cập nhật