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.
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:
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.concurrent
khung 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ư long
hoặc double
có 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 .
AtomicReference
bạn nên đánh dấu biến volatile
vì 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.
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.
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.
worked
lại để có được ngữ nghĩa tương tự.
Đâ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;
}
}
}
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.
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 đỡ
Đâ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 AtomicReference
là 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ì đó AtomicReference
là 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.
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
}
}
}
}
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.
Executors
xá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 AtomicReference
trong 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
/ synchronized
v.v.
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