20 Şubat 2020 Perşembe

volatile Anahtar Kelimesi

Volatile
Volatile kelimesi C++ ve Java'da farklı manalara gelir. Java ve C#'ta volatile kelimesi multithreading için kullanılır. Başka bir yerde işe yaramaz. Açıklaması şöyle.
volatile keyword guarantees visibility of shared state among multiple threads whenever there is a change.
Thread, volatile değişkeni cache'lemez her zaman en son değeri göreceği garanti edilir. Açıklaması şöyle.
Each thread has its own stack, and so its own copy of variables it can access. When the thread is created, it copies the value of all accessible variables in its own memory. The volatile keyword is used to say to the jvm "Warning, this variable may be modified in an other Thread". Without this keyword the JVM is free to make some optimizations, like never refreshing those local copies in some threads. The volatile force the thread to update the original variable for each variable. The volatile keyword could be used on every kind of variable, either primitive or objects!
Bunun nasıl gerçekleşeceği ise açık değil. İlaveten Java'da çalışma prensibini açıklayan bir yazı burada. Gerçekleştirim nasıl olacağı belli olmasa bile her volatile değişken erişiminde RAM'e gidilmediği söylenebilir. Açıklaması şöyle
As a computer engineer who has spent half a decade working with caches at Intel and Sun, I’ve learnt a thing or two about cache-coherency. (...) For another, if volatile variables were truly written/read from main-memory > every single time, they would be horrendously slow – main-memory references are > 200x slower than L1 cache references. In reality, volatile-reads (in Java) can > often be just as cheap as a L1 cache reference, putting to rest the notion that volatile forces reads/writes all the way to main memory. If you’ve been avoiding the use of volatiles because of performance concerns, you might have been a victim of the above misconceptions.
Volatile Hangi Tipler İle Kullanılabilir
Primitive ve Object Değişken İçin Kullanılabilir
Açıklaması şöyle.
This keyword can be used for primitives as well as objects. The usage of the volatile keyword on a variable ensures that the given variable is read directly from main memory and written back to the main memory when updated.
Açıklaması şöyle
The volatile keyword can be used with variables of any kind, including primitive types and object references. When used with objects, all threads will see the most up-to-date values for all fields in the object.
Volatile ve Primitive Tip
Açıklaması şöyle
The volatile keyword is often used with flags that indicate that a thread needs to stop running. For example, a thread might have a boolean flag called “done”, and when another thread sets this flag to “true”, the first thread will know to stop running. Without the volatile keyword, the first thread might keep running indefinitely, because it would never see the updated value of the “done” flag.
Örnek - Yanlış Kod
// class with race condition
// Add volatile to `ready` to improve the situation
class DataRace {
  boolean ready = false; // add `volatile` here
  int answer = 0;
  
  void threadOne() {
    while (!ready) {
        assert answer == 42;
    }
  }
  
  void threadTwo() {
    answer = 42;
    ready = true;
  }
}
Volatile ve Object Tip
Örnek
Double Checked Locking Singleton volatile kullanan bir örnek

Örnek
Object değişken için kullanılmasına bir örnek CopyOnWriteArrayList sınıfı. Bu sınıfta write işleminde lock kullanılır ancak read işleminde lock kullanılmaz. Dolayısıyla memory barrier devreye girmez, ancak array  object değişkeni volatile olduğu ve adresi atomic olarak güncellendiği için read ve write işlemlerini yapan herkes aynı adresi görür.

Yazma İşlemi
Yazma işlemini tek bir thread'in yapması doğrudur. Açıklaması şöyle.
volatile keyword works well if there is only one writing thread.
Eğer iki thread aynı anda yazma yapıyorsa bir tanesinin yazdığı ezilir. Açıklaması şöyle.
If there are 2 or more writing threads, race condition will happen: all writing threads get the latest version of variable, modify value at its own CPU, then write to main memory. The consequence is that data in memory is just the output of one thread, other threads' modification were overridden.
Yazma İşleminde Yapılmaması Gereken Şeyler
volatile kelimesi atomic işlem anlamına gelmez.Açıklaması şöyle
My rephrase of Brian Goetz’s “Java Concurrency in practice”:

Locking(aka synchronization) can guarantee both: visibility and atomicity, volatile can only guarantee visibility. You can freely use volatile when:

Only single thread writes to variable or writes to values don’t depend on it’s current value
locking isn’t required when variable is accessed
variable doesn’t participate in any invariant with other state variables
Örnek
Açıklaması şöyle
For example, if you modify a number with volatile and operate number++ in a multithreaded environment, the data will definitely be inconsistent. Indivisible, to ensure the integrity and consistency of the data, either succeed or fail at the same time

Elimizde şöyle bir kod olsun. increment() metodu çoklu thread tarafından çağrılırsa, yanlış çalışır
@NotThreadSafe
public class UnsafeCounter {
  private volatile Integer counter;

  public void increment() {
    counter++;
  }
}
Eğer atomic olsaydı altta Barrier kullanılır ve olaylar şöyle olur. Memory Barrier yazısına bakabilirsiniz Her yazma işleminden sonra bir store bariyeri kullanılır. Böylece yazılan değeri diğer işlemciler de görebilirler. Kodda bir yerde synchronized anahtar kelimesi varsa, Barrier kullanılma ihtimali yüksektir.

Sadece Volatile Değişken Değil Tüm İşlemci Cache'i Güncellenir
Açıklaması şöyle.
By declaring a field as volatile, we tell the JVM that when a thread reads the volatile field we want to see the latest written value. The JVM than uses special instructions to tell the CPU that it should synchronize its caches. For the x86 processor family, like the mentioned Intel Xeon or the AMD Ryzen, those instructions are called memory fences as described here.

The processor not only synchronizes the value of the volatile field but the complete cache. So if we read from a volatile field we see all writes on other cores to this variable and also the values which were written on those cores before the write to the volatile variable.
Bu Happens-Before ilişkisi kurulması anlamına geliyor.

Örnek
Şöyle yaparız
public class Foo {
  int x;
  volatile in y;
}

final Foo foo = new Foo ();
Thread t1 = new Thread(()-> {
  foo.x = 1;
  //y volatile olduğu için tüm önceli değişiklikler aynı sıradadır
  foo.y = 1;
});

Thread t2 = new Thread(()-> {
  //Önce volatile y değişkeni okunur
  if (foo.y == 1) {
    //Burada artık x'teki değişikliklerin görüleceği garanti edilir
    System.out.println(foo.x);
  }
});

t1.start();
t2.start();
Okuma İşlemi
Eğer altta Barrier kullanılıyorsa olaylar şöyle olur. Memory Barrier yazısına bakabilirsiniz Her okuma işleminden önce bir load (acquire) bariyeri kullanılır. Böylece diğer işlemcilerin yazdıkları değerler görülebilir. Kodda bir yerde synchronized anahtar kelimesi varsa, Barrier kullanılma ihtimali yüksektir.

Önce Okuma Sonra Yazma Varsa
Bu durumda volatile kullanılamaz. Açıklaması şöyle. Yani aslında tek bir yazan thread kuralı ihlal edilmemeli deniyor.
- if you have a field and you want to make sure that if one thread writes to it, other threads can immediately read the written value - volatile is enough (without volatile, other threads may never even see the written value)
- if you have a field that you need to first read and only then write (based on the value you just read so no operations on this value may happen in between), volatile is not enough; instead, you can use:
- AtomicReference, AtomicInteger, etc.
- or synchronization.
Volatile ve Final
Volatile ve final mantık olarak bir araya gelemeyeceği için derleyici hata verir. final değerin değişmeyeceğini belirtir. Volatile ise değişebileceğini belirtir. Dolayısıyla çelişirler.
private final volatile AtomicInteger size; //1, Not compile
Örnek
volatile int kullanarak "double buffering" yapılabilir. İki tane volatile int tanımlanır
volatile int buffer = 0;
volatile int process = 1;
İki tane de kuyruk tanımlanır
Queue<Foo> [] doubleQueues = ...;
Yazan taraf şöyle yazar
doubleQueues [buffer].add (...)
İşleyen taraf şöyle yapar ve önce dolu kuyruğun tamamını işler.
doubleQueues [process].poll (...)
Daha sonra işleyen taraf işi bitince kuyrukları değiştirir
buffer ^= 1; //buffer 1 olur ve yazan taraf boş kuyruğa geçer
process ^= 1;//process 0 olur ve işleyen taraf dolu kuyruğa geçer
Örnek - volatile Map
Şöyle yaparız. ConcurrentHashMap kullanmak ta bir seçenek olabilirdi ancak burada  esas amaç yepyeni bir immutable map nesnesi kullanmak.
public final class  Cache
{
  private volatile Map<?,?> cache;

  private void mapUpdate() {
    Map<?,?> newCache = new HashMap<>();

    // populate the map

    // update the reference with an immutable collection
    cache = Collections.unmodifiableMap(newCache);
  }
}
Örnek - volatile Set
Şöyle yaparız. Burada  esas amaç yepyeni bir set nesnesi kullanmak. ConcurrentHashSet değişkeninin kendisi yeniden yaratılınca volatile olduğu için diğer thread'ler de yeni değişkeni görebilir.
public class Test {

  private volatile Set<Integer> cache = Sets.newConcurrentHashSet();

  // multiple threads call this
  public boolean contain(int num) {
    return cache.contains(num);
  }

  // background thread periodically calls this
  private void refresh() {
    Set<Integer> newSet = getNums();
    cache = newSet; 
  }

  private Set<Integer> getNums() {
    Set<Integer> newSet = Sets.newConcurrentHashSet();
    // read numbers from file and add them to newSet
    return newSet;
  }
}


1 yorum: