JavaProgramlamaKıdemli Java Geliştirici

Stream çerçevesi, SIZED niteliğini taşımayan Spliterator örneklerine dayanan boru hatlarını paralelleştirirken hangi spesifik uyum stratejisini kullanır ve bu, görev boyutu patlaması riskini nasıl azaltır?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

Sorunun tarihi

Java 8'den önce, koleksiyon işleme paralelleştirilmesi, manuel Thread yönetimi veya açık ExecutorService gönderimi gerektiriyordu, bu da geliştiricilerin iş bölümü ve senkronizasyonu manuel olarak ele almasını zorunlu kılıyordu. Java 8'de Stream API'sinin tanıtılması, SIZED gibi özelliklere dayanan Spliterator arayüzü aracılığıyla paralellik soyutlaması sağladı. Bu özellik, çerçevenin dengeli ikili bölme işlemleri gerçekleştirmesine olanak tanır, böylece ForkJoinPool için optimal görev ağaçları oluşturulur.

Problemler

Bir Spliterator'un SIZED niteliğine sahip olmaması durumunda—üretici işlevlerde, Iterator destekli akışlarda veya sonsuz dizilerde yaygındır—çerçeve dengeli görev ağaçları oluşturmak için ikili bölme (ikiye bölme) gerçekleştiremez. Kör bölme, ya koordinasyon üzerindeki yükü artırarak yürütme süresini düşüren ya da büyük parçalar oluşturarak işçi thread'lerini boş bırakacak küçük görevler (boyut patlaması) üretir. Bu öngörülemezlik, fork-join paralelliğinin temel varsayımını bozar: işlerin yaklaşık eşit alt görevlere bölünebilir olması.

Çözüm

Çerçeve, varsayılan IteratorSpliterator uygulaması aracılığıyla geometrik bunlama kullanır. Yarısı kadar bölmek yerine, yükleme maliyetlerini amorti ederken görev oluşturmayı logaritmik derinlik ile sınırlayan (1, 2, 4, 8, MAX_BATCH'a kadar) üssel olarak artan yük boyutları kullanır. ForkJoinPool, bilinen boyutlar için iş çalımı (work-stealing) kullanarak hafif görevleri tercih eder ve AbstractTask, toplam boyut bilgisine ihtiyaç duymadan tamamlanma sinyallerini hesaplar. Sıralı boyutsuz akışlar için, boru hattı elemanları bölme sırasında karşılaşma sırasını korumak amacıyla bir ArrayList içine tamponlanır ve bellek, paralellik güvenliği için feda edilir.


Hayat hikayesinden bir durum

Bağlam

Bir telemetri sistemi, bir Socket bağlantısı aracılığıyla gelen gerçek zamanlı sensör verilerini işler. Veriler, JSON nesnelerinin süreklilik arz eden bir akışı olarak gelir ve işletme gereksinimleri, saklamadan önce bu nesneleri paralel olarak ayrıştırmayı ve filtrelemeyi zorunlu kılarak gecikmeyi en aza indirmektedir. Zorluk, verilerin geliş hızının ve toplam hacminin öngörülemezliğidir.

Problem tanımı

Başlangıç uygulaması, InputStream'i bir BufferedReader içine sararak lines().parallel() kullandı. Ancak, performans profilleme, paralel akışın aşırı görev oluşturma yükü nedeniyle ardışık işlemeye göre önemli ölçüde daha yavaş olduğunu ortaya çıkardı. Temel neden, BufferedReader.lines()'den gelen temel Spliterator'un SIZED niteliğine sahip olmaması ve ilk olarak Long.MAX_VALUE'ı tahmin olarak bildirmesi olduğu için, çerçeve bireysel satırlar için mikro-görevler oluşturdu.

Değerlendirilen farklı çözümler

Bir yaklaşım, paralel işlemeye geçmeden önce tüm akışı bir ArrayList<String> içine tamponlamaktı. Bu, SIZED niteliğini sağlar ve CPU çekirdekleri arasında mükemmel ikili bölünmeyi mümkün kılardı. Ancak, bu, kabul edilemez bir gecikme getirdi—verilerin işlenmesi, tüm partinin gelmesini beklemeden mümkün olamazdı—ve dakikada milyonlarca olayla başa çıkarken ciddi bellek baskısı yarattı, bu da etkin bir akış paradigmalarını ortadan kaldırdı.

Başka bir düşünülen çözüm, alt akışa bağlı olarak, tam olarak 1000 satırlık sabit boyutlu parçaları her zaman bölen özel bir Spliterator uygulamaktı. Bu, öngörülebilir görev boyutları sağlasa da, işleme süresi satırlara göre önemli ölçüde değiştiğinde başarısız oldu; bir işçi 1000 karmaşık nesne alırken, diğer bir işçi 1000 basit nesne alabiliyordu, bu da ciddi yük dengesizliğine ve en yavaş görev için bekleyen boş CPU çekirdeklerine yol açıyordu.

Seçilen çözüm, standart kütüphanenin geometrik bunlama stratejisini taklit eden özel bir Spliterator uygulamaktı. Başlangıçta 1 olan bir batch değişkenini izledi ve her başarılı bölme ile maksimum 1024'e kadar iki katına çıkarak, çerçevenin önceden bilgi olmadan gerçek akış uzunluğuna uyum sağlamasına olanak tanıdı. Bu yaklaşım, akış ilerledikçe daha büyük grupların verimliliği ile küçük görevlerin başlangıç aşamasındaki yükünü dengeledi.

Sonuç

Geometrik bunlama yaklaşımı, ardışık işleme ile karşılaştırıldığında 8 çekirdekli bir sistemde 3.5 kat hız kazancı sağladı. Bellek kullanımı, akış süresi ne olursa olsun sabit kaldı ve gecikme, tam malzeme beklemeden işlemeye hemen başlandığı için düşük kaldı. Uyumlu boyutlandırma, ilk uygulamanın kabusuna dönüşen boyut patlamasını önledi.


Adayların sıkça kaçırdığı noktalar

Neden senkronize bir koleksiyonu paralel bir akış içinde sarmak, ardışık karşılığına kıyasla genellikle performansı düşürür, hatta CPU yoğun işlemler için bile?

Birçok aday, Collections.synchronizedList() veya senkronize Map uygulamalarının paralel akışlar için güvenli olduğunu varsayıyor. Ancak, bu koleksiyonların Spliterator'u SIZED bildirse de, her erişim için içsel senkronizasyon, devasa bellek tutarlılığı trafiği oluşturur. Aynı anda birden fazla ForkJoinPool thread'i, her öğe için aynı monitör üzerinde yarışırsa, senkronizasyon ve bağlam geçişi maliyeti, her türlü paralel kazancı aşar. Doğru yaklaşım, ya ConcurrentHashMap veya nadir yazma işlemleri varsa CopyOnWriteArrayList kullanmayı ya da kaynak koleksiyonunun müdahele etmeden erişim sağlandığından ve iş parçacığına güvenli Spliterator özellikleri gibi CONCURRENT ile erişildiğinden emin olmayı gerektirir.

ORDERED niteliği, boyutsuz akışlarla nasıl etkileşime geçer ve terminal işlemleri potansiyel olarak nasıl sıralı hale getirir ve sorted() bunu neden kötüleştirir?

Adaylar genellikle ORDERED ile SIZED niteliğinin yokluğunun, çerçevenin, bilgiler işlenmeden önce tüm elemanları tamponlamasını zorunlu kıldığını kaçırır; bu, durumlu işlemler gibi sorted() veya distinct() için geçerlidir. Toplam boyut bilinmediğinden, çerçeve toArray() için nihai dizi veya birleştirme sıralama tamponları ayırmakta zorlanır. Bunun yerine, elemanları bir bağlı listeye veya dinamik olarak boyutlandırılan bir ArrayList'e toplar ve bu, boru hattının tamamlama aşamasını etkili bir şekilde seri hale getirir. Bu, paralel hız artışının yalnızca harita/filtre aşamalarına sınırlı olduğu, terminal aşamanın ise tam veri kümesini bekleyen tek iş parçacığı darboğazı haline geldiği anlamına gelir.

Özel bir Spliterator'un trySplit() yöntemi, ebeveynin farklı bir özellik setini raporlayan bir Spliterator dönerse ne tür bir sözleşme ihlali gerçekleşir?

Geliştiricilerin trySplit()'i aşırı yüklenip, özellik tutarlılığını korumayı başaramadığı durumlarda ince bir hata oluşur. Spliterator sözleşmesi, dönen spliterator'un sıralama, ayırt edilme ve sıralama ile ilgili olanları aynı özelliklere sahip olması gerektiğini gerektirir. Eğer bir ebeveyn ORDERED bildirirken, çocuk (bölünmüş sonuç) bildirmezse, Stream çerçevesinin optimizasyon geçişleri sıralama adımlarını ortadan kaldırabilir veya işlemleri yeniden sıralayabilir, bu da yanlış sonuçlara yol açabilir. Özellikler, parçalar üzerinde kararlı olmalıdır, çünkü boru hattı bu bayraklara dayanarak birleşim (örneğin, filter ve map'in kombinasyonu) optimizasyonu gerçekleştirir ve tutarsız bayraklar, paralel doğruluk için gerekli olan gerçekleşme ilişkilerini bozar.