
Java 8 LongAdder vs AtomicLong
Java 8 ile birlikte pek çok görünen veya arkada görünmeyen yenilikler getirildi. Bunlardan bir tanesi de LongAdder sınıfıdır. Bir benzeri olan AtomicLong sınıfı ise Java 5’ten beri var.
Her iki sınıf da hemen hemen aynı işi yapıyor. Yaptıkları iş ise, Long türündeki bir tamsayıya +1 eklemek veya -1 eklemek. Peki neden bu sınıflara ihtiyaç duyuluyor?
Bunu şu şekilde açıklayabiliriz. Elimizde aşağıdaki gibi Counter adında bir sınıfımız olsun. Bu sınıfın increment() and decrement() adında da iki metodu bulunsun. Bu metodların yapacağı iş ise bir tamsayıya bir eklemek ve çıkarmak olsun. Şöyle ki;
public class Counter { (1)
private Long count = 0L; (2)
public void increment(){ (3)
count++;
}
public void decrement(){ (4)
count--;
}
public Long getCount(){ (5)
return count;
}
}
| 1 | Sayaç sınıfı |
| 2 | count değişkeninin başlangıcı 0 |
| 3 | count değeri 1 artırılıyor. |
| 4 | count değeri 1 azaltılıyor. |
| 5 | count ‘un o anki değeri döndürülüyor. |
Burada herşey iyi hoş fakat, bu yapı thread-safe değil!! Aslında tek bir eylemmiş gibi görünen count++ ve count-- aslında birçok işten oluşuyor.
Şöyle ki;
count değerini oku (1) count değerini bir artır veya azalt (2) count değerini güncelle (3)
Bu üç iş 1-2 ve 2-3 ara noktalarında context-switch yapılabilmesine imkan verdiği için beklendik sonuç ile çıkan sonuç uyumsuz olabilecektir.
Örneğin 2 iş parçacığı (thread) aynı anda increment() metodunu çağırıyor olsun ve adımlar aşağıdaki gibi işlesin;
Thread 1 -> count değerini okur (1)
Context-Switch gerçekleşir (2)
Thread 2 -> count değerini okur (3)
Thread 2 -> count değerini artırır (4)
Thread 2 -> count değerini günceller (5)
Context-Switch gerçekleşir (6)
Thread 1 -> Okuduğu count değerini bir artırır (7)
Thread 1 -> Artırdığı değeri günceller (8)
| 1 | Thread 1 için count değeri 0 okunuyor |
| 2 | Diğer Thread’e geçiş |
| 3 | Thread 2 için count değeri 0 okunuyor |
| 4 | Thread 2 için count değeri 1 artırılıyor |
| 5 | Thread 2 için count değeri 1 güncelleniyor |
| 6 | Diğer Thread’e geçiş |
| 7 | Thread 1 için count değeri 1 artırılıyor |
| 8 | Thread 1 için count değeri 1 güncelleniyor |
Bu senaryo sonunda count değişkeninin son değeri 1 olur. Fakat sonuç 2 olmalıydı. İşte bu sebeple bu yapı thread-safe değildir diyoruz.
Bu aslında atomiklik problemidir. Yani tek bir bütün gibi görünen işlem aslında birden fazla ayrık iştir. Bu sebeple context-switch ‘e imkan vererek beklenmedik sonuçlara sebep olmaktadır.
Bu üç işi tek bir iş yapabilmek için, bu işleri parçalanamaz (context-switch ile bölünemez) hale getirebiliriz (atom haline getirebiliriz). Birden fazla işi atomik hale getirmek için synchronized anahtar kelimesini veya ReentrantLock gibi nesnelerden faydalanabiliriz.
Örneğin;
public class Counter {
private Long count = 0L;
public synchronized void increment(){ // synchronized
count++;
}
public synchronized void decrement(){ // synchronized
count--;
}
public synchronized Long getCount(){
return count;
}
}
Metodlar synchronized yapıldığında, T anında yalnızca 1 iş parçacığı increment ve decrement metodları içinde iş yapacaktır. Diğer iş parçacıkları ise, iş yapan iş parçacığın metodu terk etmesini bekleyeceklerdir.
Bu durum sonucunda thread-safety problemi çözülmüş olur, fakat yeni bir problem ortaya çıkar o da Performans problemi.
Yukarıdaki increment ve decrement metodları iş parçacıklarını hizaya koyar, aynı anda iş yapamaz hale gelirler. Bu da sayaç artırma/azaltma işlemi için darboğaz oluşturacağı anlamını taşıyor.
Sayaç artırma senaryosunda thread-safety ve performans problemlerini söndürmek için Java içerisinde AtomicInteger, AtomicLong ve LongAdder nesneleri bulunuyor. Atomic* olanlar Java 5’ten beri varken, LongAdder Java 8 ile henüz yeni getirildi. Fakat IntAdder diye bir sınıf getirilmediğini belirtmek isterim, sadece Long hali bulunuyor.
Atomic* sınıfları türünden nesneler, hem thread-safe ‘tir. Hem de yapılan işlemi yüksek başarımlı olarak gerçekleştirirler. LongAdder nesneleri ise, AtomicLong nesnesine göre kat kat daha fazla yüksek başarımla sayaç işlemlerini gerçekleştirirler. Fakat LongAdder nesnesi AtomicLong ‘a göre daha fazla bellek alanı tüketmektedir.
Yukarıda bahsettiklerimizi kanıtlamak için, sizlerle JMH (Java MicroBenchmark Harness) ile yaptığım kıyaslamayı paylaşmak isterim.
Counter vs AtomicLong vs LongAdder
JMH ile yapılan kıyaslamada, aşağıdaki niteliklere sahip Amazon EC2 makinası kullanılmıştır.
Amazon EC2 – Compute Optimized (c3.8xlarge)
OS – Ubuntu 14.04 (x64)
CPU – 32 Core
Memory – 60 Gib
Disk – 2 x 320 SSD
Java – JDK 1.8 x64
JMH ile yapılan bu ölçüm sonucunda aşağıdaki çıktılar elde edilmiştir.
| Thread sayısı | Counter (opts/ms) | AtomicLong (opts/ms) | LongAdder (opts/ms) |
|---|---|---|---|
|
1 |
41672.68909613381 |
140253.50851349483 |
88159.29678717554 |
|
2 |
21427.389750899158 |
80461.53894458264 |
176168.92737348034 |
|
3 |
28647.729284927846 |
76716.30113465142 |
261230.59792909838 |
|
4 |
27209.234760929074 |
67990.24783775007 |
351318.7946836924 |
|
5 |
27465.74504786415 |
69041.89169888754 |
439091.1919015038 |
|
6 |
25838.56806379749 |
66226.24153866756 |
527889.262511702 |
|
7 |
27895.005716758176 |
70962.58786769061 |
604720.7845669618 |
|
8 |
29824.63604508381 |
72030.62263077003 |
700302.8153259079 |
|
9 |
23160.47999126532 |
71234.49684954488 |
778763.7886768953 |
|
10 |
29224.18154845496 |
69897.36028303344 |
861418.8185465168 |
|
11 |
16948.03390480977 |
69974.02050098566 |
941304.4224780009 |
|
12 |
26730.29944976108 |
69021.07450449596 |
1016534.1595596515 |
|
13 |
16025.220262360124 |
73130.64286118638 |
1085067.0573038626 |
|
14 |
19862.662553970724 |
71855.46330526042 |
1150916.1856743009 |
|
15 |
21864.71243784617 |
66756.24362408795 |
1206257.065200144 |
|
16 |
14311.927044503234 |
70172.81000368376 |
1286617.7092898681 |
|
17 |
13812.790859316134 |
72771.08592896884 |
1364026.7288176445 |
|
18 |
12915.302399663751 |
68714.59365814141 |
1407000.6514096512 |
|
19 |
14079.797370133612 |
67909.2908569055 |
1448556.4113242174 |
|
20 |
13781.660590830888 |
70911.64805900287 |
1525443.4720664842 |
|
21 |
13661.317043400575 |
67420.32688299088 |
1590701.745499809 |
|
22 |
13123.256421277907 |
74014.52396619573 |
1512402.6709205445 |
|
23 |
14046.940134185787 |
74219.95136660068 |
1668385.0168750533 |
|
24 |
13883.126332713113 |
66822.51696197722 |
1704745.2462318498 |
|
25 |
13695.700287741183 |
67392.98538139547 |
1771630.2211731498 |
|
26 |
14263.609176633352 |
72078.81502528508 |
1803194.4472211923 |
|
27 |
14235.15711567844 |
69513.41638494289 |
1827386.1809486842 |
|
28 |
12897.078507902097 |
65653.42458349481 |
1881694.8392604052 |
|
29 |
14684.357169859464 |
70201.21669138117 |
1930174.7003092975 |
|
30 |
14566.82924130262 |
71106.7300332613 |
1726288.8632039533 |
|
31 |
14442.104127009969 |
70359.76043375299 |
2016763.8781386625 |
|
32 |
14664.16730602341 |
72063.45431988215 |
2000693.716820047 |
| opts/ms : 1 milisaniyede yapılan operasyon sayısı. (Throughput) |
Yukarıdaki tablonun grafiksel görünümü ise aşağıdaki gibidir. Sonuç gerçekten dramatik.
|
Mavi: |
Benchmark uygulamasına https://github.com/rahmanusta/jmh-samples bağlantısından erişebilirsiniz.
Tekrar görüşmek dileğiyle..
Tag:atomiclong, backend, jmh, longadder





2 Comments
LongAdder bu kadar performans kazancını nasıl sağlamış merak eden olursa buraya bakabilir. https://minddotout.wordpress.com/2013/05/11/java-8-concurrency-longadder/
Yazı çok güzeldi, teşekkürler.
Teşekkürler. Güzel paylaşımınız için ben de teşekkür ederim.