Khi nào nên sử dụng AtomicReference trong Java?


311

Khi nào chúng ta sử dụng AtomicReference?

Có cần thiết để tạo các đối tượng trong tất cả các chương trình đa luồng không?

Cung cấp một ví dụ đơn giản trong đó nên sử dụng AtomicReference.

Câu trả lời:


215

Tham chiếu nguyên tử nên được sử dụng trong cài đặt nơi bạn cần thực hiện các thao tác nguyên tử đơn giản (tức là an toàn luồng , không tầm thường) trên tham chiếu, trong đó đồng bộ hóa dựa trên màn hình là không phù hợp. Giả sử bạn muốn kiểm tra xem liệu một trường cụ thể chỉ khi trạng thái của đối tượng vẫn như bạn đã kiểm tra lần cuối:

AtomicReference<Object> cache = new AtomicReference<Object>();

Object cachedValue = new Object();
cache.set(cachedValue);

//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);

Do ngữ nghĩa tham chiếu nguyên tử, bạn có thể làm điều này ngay cả khi cacheđối tượng được chia sẻ giữa các luồng mà không cần sử dụng synchronized. Nói chung, tốt hơn hết bạn nên sử dụng đồng bộ hóa hoặc java.util.concurrentkhung chứ không phải trần Atomic*trừ khi bạn biết bạn đang làm gì.

Hai tài liệu tham khảo cây chết tuyệt vời sẽ giới thiệu cho bạn chủ đề này:

Lưu ý rằng (tôi không biết nếu điều này luôn luôn đúng) thì việc gán tham chiếu (nghĩa là =) chính nó là nguyên tử (cập nhật các loại 64 bit nguyên thủy như longhoặc doublecó thể không phải là nguyên tử; nhưng cập nhật tham chiếu luôn là nguyên tử, ngay cả khi đó là 64 bit ) mà không sử dụng một cách rõ ràng Atomic*.
Xem Đặc tả ngôn ngữ Java 3ed, Phần 17.7 .


43
Sửa lỗi cho tôi nếu tôi sai, nhưng có vẻ như chìa khóa để cần điều này là do bạn cần thực hiện một "so sánh". Nếu tất cả những gì tôi cần làm là tôi sẽ không cần AtomicObject vì bản cập nhật tham chiếu có phải là nguyên tử không?
sMoZely

Có an toàn khi thực hiện cache.compareAndset (cacheedValue, someFunctionOfOld (cacheedValueToUpdate))? Tức là nội tuyến tính toán?
kaqqao

4
@veggen Các đối số hàm trong Java được đánh giá trước chính hàm, do đó, nội tuyến không tạo ra sự khác biệt trong trường hợp này. Vâng, nó an toàn.
Dmitry

29
@sMoZely Điều đó đúng, nhưng nếu bạn không sử dụng, AtomicReferencebạn nên đánh dấu biến volatilevì trong khi thời gian chạy đảm bảo gán tham chiếu là nguyên tử, trình biên dịch có thể thực hiện tối ưu hóa theo giả định rằng biến không bị sửa đổi bởi các luồng khác.
kbolino

1
@BradCupit lưu ý rằng tôi đã nói "nếu bạn không sử dụng AtomicReference"; Nếu bạn đang sử dụng nó, thì lời khuyên của tôi sẽ là đi theo hướng ngược lại và đánh dấu nó finalđể trình biên dịch có thể tối ưu hóa cho phù hợp.
kbolino

91

Một tham chiếu nguyên tử là lý tưởng để sử dụng khi bạn cần chia sẻ và thay đổi trạng thái của một đối tượng bất biến giữa nhiều luồng. Đó là một tuyên bố siêu dày đặc vì vậy tôi sẽ phá vỡ nó một chút.

Đầu tiên, một đối tượng bất biến là một đối tượng có hiệu quả không bị thay đổi sau khi xây dựng. Thường thì các phương thức của đối tượng bất biến trả về các thể hiện mới của cùng một lớp. Một số ví dụ bao gồm các lớp bao bọc của Long và Double, cũng như String, chỉ để đặt tên cho một số. (Theo Lập trình đồng thời trên các đối tượng bất biến JVM là một phần quan trọng của đồng thời hiện đại).

Tiếp theo, tại sao AtomicReference tốt hơn một đối tượng dễ bay hơi để chia sẻ giá trị được chia sẻ đó. Một ví dụ mã đơn giản sẽ cho thấy sự khác biệt.

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

Mỗi lần bạn muốn sửa đổi chuỗi được tham chiếu bởi trường biến động đó dựa trên giá trị hiện tại của nó, trước tiên bạn cần phải có khóa trên đối tượng đó. Điều này ngăn một số luồng khác đến trong thời gian đó và thay đổi giá trị ở giữa nối chuỗi mới. Sau đó, khi chủ đề của bạn tiếp tục, bạn ghi đè công việc của chủ đề khác. Nhưng thành thật mà nói, mã sẽ hoạt động, nó trông sạch sẽ, và nó sẽ làm cho hầu hết mọi người hài lòng.

Vấn đề nhỏ. Nó chậm. Đặc biệt là nếu có rất nhiều sự tranh chấp của đối tượng khóa đó. Đó là bởi vì hầu hết các khóa yêu cầu một cuộc gọi hệ thống hệ điều hành, và luồng của bạn sẽ chặn và được chuyển ngữ cảnh ra khỏi CPU để nhường chỗ cho các quy trình khác.

Tùy chọn khác là sử dụng AtomicRefrence.

public static AtomicReference<String> shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
  String prevValue=shared.get();
  // do all the work you need to
  String newValue=shared.get()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

Bây giờ tại sao điều này tốt hơn? Thành thật mà nói, mã này ít sạch hơn trước một chút. Nhưng có một thứ thực sự quan trọng xảy ra dưới cái mũ trong AtomicRefrence, và đó là so sánh và trao đổi. Đó là một lệnh CPU duy nhất, không phải là một cuộc gọi hệ điều hành, làm cho việc chuyển đổi xảy ra. Đó là một hướng dẫn duy nhất trên CPU. Và bởi vì không có khóa, không có chuyển đổi ngữ cảnh trong trường hợp khóa được thực hiện mà tiết kiệm nhiều thời gian hơn!

Điều thú vị là, đối với AtomicReferences, điều này không sử dụng lệnh gọi .equals (), mà thay vào đó là một so sánh == cho giá trị mong đợi. Vì vậy, hãy chắc chắn rằng đối tượng dự kiến ​​là đối tượng thực tế được trả về từ get trong vòng lặp.


14
Hai ví dụ của bạn cư xử khác nhau. Bạn sẽ phải lặp đi lặp workedlại để có được ngữ nghĩa tương tự.
RèmDog

5
Tôi nghĩ bạn nên khởi tạo giá trị bên trong hàm tạo AtomicReference, nếu không, một luồng khác vẫn có thể thấy giá trị null trước khi bạn gọi shared.set. (Trừ khi shared.set được chạy trong trình khởi tạo tĩnh.)
Henno Vermeulen

8
Trong ví dụ thứ hai của bạn, bạn nên sử dụng Java 8 như: shared.updateAndGet ((x) -> (x + "cho phép thêm một cái gì đó")); ... sẽ liên tục gọi .compareAndset cho đến khi nó hoạt động. Điều đó tương đương với khối được đồng bộ hóa sẽ luôn thành công. Bạn cần đảm bảo rằng lambda bạn truyền vào không có tác dụng phụ vì nó có thể được gọi nhiều lần.
Tom Dibble

2
Không cần thiết phải tạo Chuỗi chia sẻ dễ bay hơi. Đồng bộ (khóa) là đủ tốt để thiết lập xảy ra trước khi mối quan hệ.
Jai Pandit

2
"... Thay đổi trạng thái của một vật thể bất biến" là không chính xác ở đây, một phần bởi vì theo nghĩa đen, bạn không thể thay đổi trạng thái của một vật thể bất biến. Ví dụ cho thấy việc thay đổi tham chiếu từ một thể hiện đối tượng bất biến sang một đối tượng khác. Tôi nhận ra đó là mô phạm nhưng tôi nghĩ rằng nó đáng để làm nổi bật khi logic logic có thể gây nhầm lẫn.
Đánh dấu Phillips

30

Đây là trường hợp sử dụng cho AtomicReference:

Hãy xem xét lớp này hoạt động như một phạm vi số và sử dụng các biến AtmomicInteger riêng lẻ để duy trì giới hạn số thấp hơn và cao hơn.

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

Cả setLower và setUpper đều là các chuỗi kiểm tra hành động, nhưng chúng không sử dụng khóa đủ để biến chúng thành nguyên tử. Nếu phạm vi số giữ (0, 10) và một luồng gọi setLower (5) trong khi một luồng khác gọi setUpper (4), với một số thời điểm không may mắn cả hai sẽ vượt qua kiểm tra trong setters và cả hai sửa đổi sẽ được áp dụng. Kết quả là phạm vi hiện giữ (5, 4) trạng thái không hợp lệ. Vì vậy, trong khi các AtomicIntegers cơ bản là an toàn luồng, thì lớp tổng hợp lại không. Điều này có thể được khắc phục bằng cách sử dụng AtomicReference thay vì sử dụng từng AtomicIntegers cho giới hạn trên và dưới.

public class CasNumberRange {
    // Immutable
    private static class IntPair {
        final int lower;  // Invariant: lower <= upper
        final int upper;

        private IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}

2
Bài viết này tương tự như câu trả lời của bạn, nhưng đi sâu vào những điều phức tạp hơn. Thật thú vị! ibm.com/developerworks/java/l
Library / j

20

Bạn có thể sử dụng AtomicReference khi áp dụng các khóa lạc quan. Bạn có một đối tượng chia sẻ và bạn muốn thay đổi nó từ nhiều hơn 1 luồng.

  1. Bạn có thể tạo một bản sao của đối tượng chia sẻ
  2. Sửa đổi đối tượng chia sẻ
  3. Bạn cần kiểm tra xem đối tượng chia sẻ có còn như trước không - nếu có, sau đó cập nhật với tham chiếu của bản sao đã sửa đổi.

Vì các luồng khác có thể đã sửa đổi nó và / có thể sửa đổi giữa 2 bước này. Bạn cần phải làm điều đó trong một hoạt động nguyên tử. đây là nơi mà AtomicReference có thể giúp đỡ


7

Đây là một trường hợp sử dụng rất đơn giản và không liên quan gì đến an toàn luồng.

Để chia sẻ một đối tượng giữa các yêu cầu lambda, đây AtomicReferencelà một tùy chọn :

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

Tôi không nói rằng đây là thiết kế tốt hay bất cứ điều gì (nó chỉ là một ví dụ tầm thường), nhưng nếu bạn gặp trường hợp bạn cần chia sẻ một đối tượng giữa các yêu cầu lambda, thì đó AtomicReferencelà một lựa chọn.

Trong thực tế, bạn có thể sử dụng bất kỳ đối tượng nào chứa tham chiếu, ngay cả Bộ sưu tập chỉ có một mục. Tuy nhiên, AtomicReference là một sự phù hợp hoàn hảo.


6

Tôi sẽ không nói nhiều. Đã có những người bạn đáng kính của tôi đã cho đầu vào có giá trị của họ. Mã chạy đầy đủ chính thức ở cuối blog này sẽ loại bỏ bất kỳ sự nhầm lẫn nào. Đó là về một chỗ ngồi đặt phim chương trình nhỏ trong kịch bản đa luồng.

Một số sự kiện cơ bản quan trọng như sau. 1> Các luồng khác nhau chỉ có thể tranh luận về các biến thành viên tĩnh và ví dụ trong không gian heap. 2> Đọc hoặc ghi dễ bay hơi là hoàn toàn nguyên tử và tuần tự / xảy ra trước và chỉ được thực hiện từ bộ nhớ. Bằng cách nói điều này tôi có nghĩa là bất kỳ đọc sẽ theo ghi trước đó trong bộ nhớ. Và bất kỳ ghi sẽ theo sau đọc từ bộ nhớ. Vì vậy, bất kỳ luồng nào làm việc với một biến động sẽ luôn luôn thấy giá trị cập nhật nhất. AtomicReference sử dụng tính chất dễ bay hơi này.

Sau đây là một số mã nguồn của AtomicReference. AtomicReference đề cập đến một tham chiếu đối tượng. Tham chiếu này là một biến thành viên dễ bay hơi trong trường hợp AtomicReference như dưới đây.

private volatile V value;

get () chỉ đơn giản trả về giá trị mới nhất của biến (như các chất bay hơi thực hiện theo cách "xảy ra trước").

public final V get()

Sau đây là phương pháp quan trọng nhất của AtomicReference.

public final boolean  compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

Phương thức so sánh (mong đợi, cập nhật) gọi phương thức so sánhAndSwapObject () của lớp Java không an toàn. Cuộc gọi phương thức không an toàn này gọi cuộc gọi riêng, gọi một lệnh duy nhất đến bộ xử lý. "mong đợi" và "cập nhật" mỗi tham chiếu một đối tượng.

Nếu và chỉ khi biến "thành viên" của đối tượng AtomicReference tham chiếu đến cùng một đối tượng được gọi bằng "mong đợi", "cập nhật" được gán cho biến thể hiện này ngay bây giờ và "true" được trả về. Hoặc nếu không, sai được trả lại. Toàn bộ điều được thực hiện nguyên tử. Không có chủ đề khác có thể chặn ở giữa. Vì đây là một hoạt động của bộ xử lý (ma thuật của kiến ​​trúc máy tính hiện đại), nó thường nhanh hơn so với sử dụng một khối được đồng bộ hóa. Nhưng hãy nhớ rằng khi nhiều biến số cần được cập nhật nguyên tử, AtomicReference sẽ không giúp ích.

Tôi muốn thêm một mã chạy đầy đủ, có thể chạy trong nhật thực. Nó sẽ xóa nhiều nhầm lẫn. Tại đây 22 người dùng (chủ đề MyTh) đang cố gắng đặt 20 chỗ. Sau đây là đoạn mã theo sau là mã đầy đủ.

Đoạn mã nơi 22 người dùng đang cố gắng đặt 20 chỗ.

for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }

Sau đây là mã chạy đầy đủ.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    

5

Khi nào chúng ta sử dụng AtomicReference?

AtomicReference là cách linh hoạt để cập nhật giá trị biến nguyên tử mà không cần sử dụng đồng bộ hóa.

AtomicReference hỗ trợ lập trình luồng an toàn khóa trên các biến đơn.

Có nhiều cách để đạt được sự an toàn của Thread với API đồng thời ở mức cao . Biến nguyên tử là một trong nhiều lựa chọn.

Lock các đối tượng hỗ trợ khóa thành ngữ đơn giản hóa nhiều ứng dụng đồng thời.

Executorsxác định API cấp cao để khởi chạy và quản lý luồng. Việc triển khai thực thi được cung cấp bởi java.util.conc hiện cung cấp quản lý nhóm luồng phù hợp cho các ứng dụng quy mô lớn.

Bộ sưu tập đồng thời giúp quản lý bộ sưu tập dữ liệu lớn dễ dàng hơn và có thể giảm đáng kể nhu cầu đồng bộ hóa.

Biến nguyên tử có các tính năng giảm thiểu đồng bộ hóa và giúp tránh các lỗi thống nhất bộ nhớ.

Cung cấp một ví dụ đơn giản trong đó nên sử dụng AtomicReference.

Mã mẫu với AtomicReference:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

Có cần thiết để tạo các đối tượng trong tất cả các chương trình đa luồng không?

Bạn không phải sử dụng AtomicReferencetrong tất cả các chương trình đa luồng.

Nếu bạn muốn bảo vệ một biến duy nhất, sử dụng AtomicReference. Nếu bạn muốn bảo vệ một khối mã, hãy sử dụng các cấu trúc khác như Lock/ synchronizedv.v.


-1

Một ví dụ đơn giản khác là thực hiện sửa đổi luồng an toàn trong đối tượng phiên.

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

Nguồn: http://www.ibm.com/developerworks/l Library / j-jtp09238 / index.html

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.