Sử dụng thực tế cho AtomicInteger


229

Tôi có thể hiểu rằng AtomicInteger và các biến Nguyên tử khác cho phép truy cập đồng thời. Trong trường hợp nào thì lớp này thường được sử dụng?

Câu trả lời:


190

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ề).


1
Tôi nghĩ rằng tôi hiểu việc sử dụng đầu tiên. Điều này là để đảm bảo bộ đếm đã được tăng lên trước khi một thuộc tính được truy cập lại. Chính xác? Bạn có thể đưa ra một ví dụ ngắn cho việc sử dụng thứ hai?
James P.

8
Sự hiểu biết của bạn về lần sử dụng đầu tiên là đúng - nó chỉ đơn giản đảm bảo rằng nếu một luồng khác sửa đổi bộ đếm giữa readvà các write that value + 1hoạ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.
Andrzej Doyle

3
"phần còn lại> 0? phần còn lại: phần còn lại + n;" trong biểu thức này có lý do để thêm phần dư vào n khi nó bằng 0 không?
sandeepkunkunuru

100

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 intsvà các synchronizedkhai báo phù hợp , nhưng điều tuyệt vời AtomicIntegerlà 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 intgiá 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 đó.


2
Cảm ơn lời giải thích rõ ràng này. Điều gì sẽ là lợi thế của việc sử dụng một AtomicInteger trên một lớp nơi các phương thức được đồng bộ hóa tất cả? Cái sau sẽ được coi là "nặng hơn"?
James P.

3
Theo quan điểm của tôi, chủ yếu là sự đóng gói mà bạn nhận được với AtomicIntegers - đồng bộ hóa xảy ra trên chính xác những gì bạn cần và bạn có được các phương pháp mô tả trong API công khai để giải thích kết quả dự định là gì. (Ngoài ra, ở một mức độ nào đó, bạn đúng, thường người ta sẽ đồng bộ hóa tất cả các phương thức trong một lớp có vẻ quá thô, mặc dù với HotSpot thực hiện tối ưu hóa khóa và các quy tắc chống tối ưu hóa sớm, tôi coi khả năng đọc là một lợi ích lớn hơn hiệu suất.)
Andrzej Doyle

Đây là lời giải thích rất rõ ràng và chính xác, Cảm ơn !!
Akash5288

Cuối cùng, một lời giải thích đã làm sáng tỏ nó cho tôi.
Benny Bottema

58

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:
++ii.incrementAndGet()
i++được i.getAndIncrement()
--ii.decrementAndGet()
i--được i.getAndDecrement()
i = xi.set(x)
x = iđượcx = i.get()

Có nhiều phương pháp tiện lợi khác, như compareAndSethoặcaddAndGet


37

Việc sử dụng chính AtomicIntegerlà 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à getAndXXXhoặ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. compareAndSetlà rất hữu ích để thực hiện semaphores, khóa, chốt, vv

Sử dụng AtomicIntegernhanh 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 Integerví dụ trong Maps làm giá trị.


1
Tôi không nghĩ rằng tôi muốn sử dụng AtomicIntegerlà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 đồ.
Andrzej Doyle

1
@Andrzej chắc chắn, không phải là khóa được yêu cầu là không thể thay đổi mà là một giá trị.
gabuzo

@gabuzo Bất cứ ý tưởng tại sao số nguyên tử thực hiện tốt hơn đồng bộ hóa?
Supun Wijerathne

Bài kiểm tra này đã khá cũ (hơn 6 năm), có thể tôi thấy thú vị khi kiểm tra lại với một JRE gần đây. Tôi đã không đi sâu đủ vào AtomicInteger để trả lời nhưng vì đây là một nhiệm vụ rất cụ thể nên nó sẽ sử dụng các kỹ thuật đồng bộ hóa chỉ hoạt động trong trường hợp cụ thể này. Cũng lưu ý rằng bài kiểm tra đã được đọc đơn và thực hiện một bài kiểm tra tương tự trong môi trường tải nặng có thể không mang lại chiến thắng rõ ràng như vậy cho AtomicInteger
gabuzo

Tôi tin rằng 3 ms và 5,5 ms
Sathesh

17

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.


Cảm ơn đã chia sẻ ví dụ thực tế này. Điều này nghe có vẻ như là thứ gì đó tôi nên sử dụng vì tôi cần có id duy nhất cho mỗi tệp tôi nhập vào chương trình của mình :)
James P.

7

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.


7

Trong Java 8 lớp nguyên tử đã được mở rộng với hai hàm thú vị:

  • int getAndUpdate (Cập nhật IntUnaryOperator)
  • int updateAndGet (Cập nhật IntUnaryOperator)

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 .


5

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.


4

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


3

Đ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 hiểu rồi. Đây có phải là trong trường hợp một thuộc tính hoặc thể hiện hoạt động như một loại biến toàn cục bên trong một ứng dụng. Hoặc có những trường hợp khác mà bạn có thể nghĩ đến?
James P.

1

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.


0

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

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.