Коли використовувати AtomicReference на Java?


311

Коли ми використовуємо AtomicReference?

Чи потрібно для створення об'єктів у всіх багатопотокових програмах?

Наведіть простий приклад, коли слід використовувати AtomicReference.

Відповіді:


215

Атомна посилання повинна використовуватися в умовах, коли вам потрібно виконати прості атомні (тобто безпечні для потоків , нетривіальні) операції над посиланням, для яких синхронізація на основі монітора не є доцільною. Припустимо, ви хочете перевірити, чи є певне поле лише в тому випадку, якщо стан об'єкта залишається таким, яким ви востаннє перевіряли:

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);

Через атомну еталонну семантику ви можете це зробити, навіть якщо cacheоб'єкт ділиться між потоками, не використовуючи synchronized. Взагалі вам краще використовувати синхронізатори або java.util.concurrentрамки, а не голі, Atomic*якщо ви не знаєте, що робите.

Дві відмінні посилання на мертве дерево, які ознайомлять вас із цією темою:

Зауважте, що (я не знаю, чи завжди це було правдою) призначення посилання (тобто =) саме по собі атомне (оновлення примітивних 64-бітових типів на кшталт longабо doubleне може бути атомним; але оновлення посилання завжди є атомним, навіть якщо це 64 біт ) без явного використання Atomic*.
Дивіться специфікацію мови Java 3ed, розділ 17.7 .


43
Виправте мене, якщо я помиляюся, але здається, що ключ до необхідності цього полягає в тому, що вам потрібно зробити "CompareAndSet". Якщо все, що мені потрібно було зробити, це було встановлено, мені AtomicObject взагалі не знадобиться через те, що оновлення посилань є атомними?
sMoZely

Чи безпечно робити cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Тобто вбудовані обчислення?
kaqqao

4
Аргументи функції @veggen в Java оцінюються перед самою функцією, тому вбудоване значення не має значення в цьому випадку. Так, це безпечно.
Дмитро

29
@sMoZely Це правильно, але якщо ви не використовуєте, AtomicReferenceви повинні позначити змінну, volatileоскільки, хоча час виконання гарантує, що присвоєння посилань є атомним, компілятор може проводити оптимізацію за умови, що змінна не змінювалася іншими потоками.
kbolino

1
@BradCupit зауважте, що я сказав "якщо ви не використовуєте AtomicReference"; якщо ви будете використовувати його, то моя порада буде йти в напрямку , протилежної і позначити його , finalтак що компілятор може оптимізувати відповідно.
kbolino

91

Атомна посилання ідеально підходить для використання, коли вам потрібно поділити і змінити стан незмінного об'єкта між декількома потоками. Це надзвичайно щільне твердження, тому я його трохи розберу.

По-перше, незмінний об’єкт - це об'єкт, який фактично не змінюється після побудови. Найчастіше методи незмінного об'єкта повертають нові екземпляри цього ж класу. Деякі приклади включають класи обгортки Long і Double, а також String, щоб назвати лише декілька. (Згідно програмування Concurrency на JVM незмінні об'єкти є критичною частиною сучасної одночасності).

Далі, чому AtomicReference краще, ніж мінливий об’єкт для спільного використання спільного значення. Простий приклад коду покаже різницю.

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

Кожен раз, коли ви хочете змінити рядок, на який посилається це мінливе поле, виходячи з його поточного значення, спочатку потрібно отримати замок на цьому об'єкті. Це запобігає тим, що якийсь інший потік тим часом входить і змінює значення в середині нового конкатенації рядків. Потім, коли ваша нитка поновлюється, ви припиняєте роботу іншої нитки. Але якщо чесно, цей код буде працювати, він виглядає чисто, і це зробило б більшість людей щасливими.

Незначна проблема. Це повільно. Особливо, якщо існує багато суперечок цього об'єкта блокування. Це тому, що для більшості блокувань потрібен системний виклик ОС, і ваш потік блокується та буде вимкнений з процесора, щоб змінити місце для інших процесів.

Інший варіант - використовувати 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);
}

Тепер чому це краще? Чесно кажучи, цей код трохи менш чистий, ніж раніше. Але є щось дійсно важливе, що відбувається під кришкою в AtomicRefrence, і це порівняння та обмін. Переключення відбувається через одну інструкцію процесора, а не виклик ОС. Це єдина інструкція щодо процесора. А оскільки немає блокувань, немає контекстного перемикача у випадку, коли блокування виконується, що економить ще більше часу!

Улов - це для AtomicReferences не використовується виклик .equals (), а замість порівняння == для очікуваного значення. Тому переконайтеся, що очікуваний фактичний об’єкт, повернутий з get у циклі.


14
Ваші два приклади поводяться по-різному. Щоб workedотримати ту саму семантику, вам доведеться ввімкнути цикл .
CurtainDog

5
Я думаю, вам слід ініціалізувати значення всередині конструктора AtomicReference, інакше інший потік може все-таки побачити значення null перед тим, як викликати shared.set. (Якщо не використовується спільний набір в статичному ініціалізаторі.)
Генно Вермеулен

8
У вашому другому прикладі вам слід на Java 8 використовувати щось на кшталт: shared.updateAndGet ((x) -> (x + "дозволяє додати щось")); ... який буде неодноразово викликати .compareAndSet, поки він не працює. Це еквівалентно синхронізованому блоку, який завжди мав би успіх. Вам потрібно переконатися, що лямбда, в яку ви переходите, не має побічних ефектів, хоча може бути викликана кілька разів.
Том Діббл

2
Не потрібно робити мінливих String sharedValue. Синхронізація (блокування) є достатньою для встановлення того, що відбудеться до відносин.
Джай Пандіт

2
"... змінити стан незмінного об'єкта" тут неточне, частково тому, що будучи буквальним, ви не можете змінити стан незмінного об'єкта. Приклад демонструє зміну посилання з одного незмінного об'єкта на інший. Я розумію, що це педантично, але, думаю, варто підкреслити, враховуючи, наскільки заплутаною може бути логіка теми.
Марк Філіпс

30

Ось випадок використання AtomicReference:

Розглянемо цей клас, який діє як діапазон чисел, і використовує окремі змінні AtmomicInteger для підтримки нижньої та верхньої меж числа.

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());
    }
}

І setLower, і setUpper є послідовністю "check-then-act", але вони не використовують достатнє блокування для того, щоб зробити їх атомними. Якщо діапазон чисел утримується (0, 10), і один потік викликає setLower (5), а інший потік викликає setUpper (4), з деяким невдалим терміном обидва пройдуть перевірки в сеттерах, і обидві модифікації будуть застосовані. Результатом є те, що діапазон тепер містить (5, 4) недійсний стан. Тому, хоча базові AtomicIntegers є безпечними для потоків, складний клас не є. Це можна виправити за допомогою AtomicReference замість використання окремих AtomicIntegers для верхньої та нижньої меж.

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
Ця стаття схожа на вашу відповідь, але заглиблюється в складніші речі. Це цікаво! ibm.com/developerworks/java/library/j-jtp04186
LppEdd

20

Ви можете використовувати AtomicReference при застосуванні оптимістичних блокувань. У вас є спільний об’єкт, і ви хочете змінити його з більш ніж 1 потоку.

  1. Ви можете створити копію спільного об’єкта
  2. Змініть об'єкт, що поділяється
  3. Вам потрібно перевірити, чи спільний об’єкт все-таки такий же, як і раніше - якщо так, то оновіть посилання на модифіковану копію.

Оскільки інші потоки, можливо, змінили його та / можуть змінювати між цими двома кроками. Це потрібно робити в атомній операції. саме тут AtomicReference може допомогти


7

Ось дуже простий випадок використання і не має нічого спільного з безпекою різьби.

Щоб поділити об'єкт між викликами лямбда, AtomicReferenceце варіант :

public void doSomethingUsingLambdas() {

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

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

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

Я не кажу, що це гарний дизайн чи що-небудь (це лише тривіальний приклад), але якщо у вас є випадок, коли вам потрібно поділитися об'єктом між викликами лямбда, AtomicReferenceце варіант.

Насправді ви можете використовувати будь-який об’єкт, який містить посилання, навіть колекцію, що містить лише один предмет. Однак AtomicReference - це ідеально підходить.


6

Я не буду багато говорити. Вже мої шановні друзі-колеги дали свій цінний внесок. Повноцінний запущений код в останньому блозі повинен усунути будь-яку плутанину. Йдеться про невелику програму бронювання сидінь у фільмі в багатопотоковому сценарії.

Деякі важливі елементарні факти такі. 1> Різні потоки можуть протиставляти лише змінні, наприклад, статичні елементи в просторі купи. 2> Летючі читання або записування є повністю атомними і серіалізованими / відбувається раніше і відбувається лише з пам'яті. Кажучи це, я маю на увазі, що будь-яке прочитане буде слідувати попередньому запису в пам'яті. І будь-яке записування буде слідувати попередньому прочитаному з пам'яті. Тож будь-яка нитка, яка працює з летючою речовиною, завжди побачить найновіше значення. AtomicReference використовує цю властивість мінливих.

Нижче наведено деякі вихідні коди AtomicReference. AtomicReference відноситься до посилання на об'єкт. Ця посилання є змінною змінною членом в екземплярі AtomicReference, як показано нижче.

private volatile V value;

get () просто повертає останнє значення змінної (як летючі речовини роблять "відбувається раніше").

public final V get()

Далі йде найважливіший метод AtomicReference.

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

Метод CompareAndSet (очікувати, оновити) викликає метод сравнениеAndSwapObject () небезпечного класу Java. Цей метод виклику небезпечних посилається на власний виклик, який викликає одну інструкцію для процесора. "очікувати" та "оновлювати" кожну посилання на об'єкт.

Якщо і лише тоді, коли змінна члена екземпляра AtomicReference "значення" посилається на один і той же об'єкт, посилається на "очікувати", "оновлення" призначається цій змінній інстанції зараз, а "істина" повертається. Або ж повертається помилка. Вся справа робиться атомно. Жодна інша нитка не може перехоплюватися між ними. Оскільки це одна операція процесора (магія сучасної архітектури комп'ютера), це часто швидше, ніж використання синхронізованого блоку. Але пам’ятайте, що коли кілька змінних потрібно оновити атомно, AtomicReference не допоможе.

Я хотів би додати повноцінний запущений код, який можна запустити в затемнення. Це дозволило б очистити багато плутанини. Тут 22 користувачі (теми MyTh) намагаються забронювати 20 місць. Далі йде фрагмент коду, а потім повний код.

Фрагмент коду, де 22 користувачі намагаються забронювати 20 місць.

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();
        }

Далі йде повний запущений код.

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

Коли ми використовуємо AtomicReference?

AtomicReference - гнучкий спосіб атомного оновлення змінної величини без використання синхронізації.

AtomicReference підтримка безблокового програмування безпечного потоку на окремих змінних.

Існує кілька способів досягнення безпеки потоку за допомогою одночасного API високого рівня . Атомні змінні є одним із безлічі варіантів.

Lock об'єкти підтримують ідіоми блокування, які спрощують багато паралельних програм.

Executorsвизначити API високого рівня для запуску та управління потоками. Реалізації виконавця, що надаються java.util.concurrent, забезпечують управління пулом потоків, що підходить для масштабних програм.

Одночасні колекції полегшують управління великими колекціями даних і можуть значно зменшити потребу в синхронізації.

Атомні змінні мають функції, що мінімізують синхронізацію та допомагають уникнути помилок узгодженості пам'яті.

Наведіть простий приклад, коли слід використовувати AtomicReference.

Приклад коду з 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);

Чи потрібно для створення об'єктів у всіх багатопотокових програмах?

Вам не доведеться використовувати AtomicReferenceу всіх багатопотокових програмах.

Якщо ви хочете зберегти одну змінну, використовуйте AtomicReference. Якщо ви хочете захистити блок коду, використовуйте інші конструкції, як Lock/ synchronizedetc.


-1

Ще один простий приклад - це зробити модифікацію безпечної нитки в об'єкті сеансу.

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;
    } 
}

Джерело: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.