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.