Java Eşzamanlı Programlama Hakkında Özet ve Düşünceler

Yüksek kaliteli eşzamanlı kod yazmak son derece zordur. Java dili, geçmişte çok dikkate değer olan ilk sürümden beri çoklu iş parçacığı için yerleşik desteğe sahiptir, ancak eşzamanlı programlamayı daha derin bir anlayışa ve daha fazla uygulamaya sahip olduğumuzda, eşzamanlı programlamanın gerçekleştirilmesi daha da iyi olacaktır. Daha fazla seçenek ve daha iyi seçimler. Bu makale, eşzamanlı programlama hakkında bir özet ve düşüncedir ve ayrıca Java 5'in sonraki sürümlerinde eşzamanlı kod yazma konusunda küçük bir deneyime sahiptir.

Neden eşzamanlılığa ihtiyacınız var?

Eşzamanlılık, aslında ne yapacağımızı (hedef) ve ne zaman yapacağımızı (zamanlama) ayırmamıza yardımcı olan bir ayrıştırma stratejisidir. Bunu yapmak, uygulamanın verimini (daha fazla CPU planlama süresi) ve yapıyı (programın birlikte çalışan birden çok parçasına sahiptir) önemli ölçüde iyileştirebilir. Java Web geliştirmeyi yapan herkes, Java Web'deki Servlet programının, Servlet kapsayıcısı desteğiyle tek örnekli çok iş parçacıklı bir çalışma modu benimsediğini ve Servlet kapsayıcısının eşzamanlılık sorunlarını sizin için ele aldığını bilir.

Yanlış anlamalar ve Doğru Çözümler

Eşzamanlı programlama hakkında en yaygın yanlış anlamalar şunlardır:

-Concurrency performansı her zaman artırabilir (eşzamanlılık, CPU'nun çok fazla boşta kalma süresi olduğunda programın performansını önemli ölçüde artırabilir, ancak iş parçacığı sayısı büyük olduğunda, iş parçacıkları arasında sık sık zamanlama geçişi aslında sistemin performansını düşürecektir)

-Orijinal tasarımı değiştirmeden eşzamanlı programlar yazın (amaç ve zamanlamanın ayrıştırılması genellikle sistem yapısı üzerinde büyük bir etkiye sahiptir)

-Web veya EJB kapsayıcılarını kullanırken eşzamanlılık sorunlarına dikkat etmeyin (yalnızca kabın ne yaptığını anladığınızda kapsayıcıyı daha iyi kullanabilirsiniz)

Aşağıdaki ifadeler, eşzamanlılığın objektif bir anlayışıdır:

-Eşzamanlı programlar yazmak koda fazladan ek yük getirecektir

-Çok basit problemler için bile doğru eşzamanlılık çok karmaşıktır

- Eşzamanlılıktaki kusurların yeniden üretilmesi kolay değildir ve bulunması kolay değildir

- Para birimi genellikle tasarım stratejisinde temel değişiklikler gerektirir

Eşzamanlı programlamanın ilkeleri ve teknikleri

Tek sorumluluk ilkesi

Eşzamanlılıkla ilgili kodu diğer kodlardan ayırın (eşzamanlı ilişkili kodun kendi geliştirme, değiştirme ve ayarlama yaşam döngüsü vardır).

Veri kapsamını kısıtla

İki iş parçacığı, paylaşılan bir nesnenin aynı alanını değiştirdiğinde, birbirleriyle etkileşime girerek beklenmedik davranışlara yol açabilir Çözümlerden biri kritik bir bölüm oluşturmaktır, ancak kritik bölümlerin sayısı sınırlı olmalıdır.

Veri kopyasını kullan

Veri kopyalama, veri paylaşımını önlemenin iyi bir yoludur Kopyalanan nesneler yalnızca salt okunur olarak kabul edilir. Java 5in java.util.concurrent paketi, List arayüzünün bir alt türü olan CopyOnWriteArrayList adında bir sınıf ekler, böylece onu veri kopyaları oluşturmak için yazarken kopyayı kullanan ArrayList'in iş parçacığı açısından güvenli bir sürümü olarak düşünebilirsiniz. Paylaşılan verilere eşzamanlı erişimin neden olduğu sorunları önlemek için işlemler gerçekleştirin.

Konular olabildiğince bağımsız olmalıdır

Başka konularla veri paylaşmadan ipliğin kendi dünyasında var olmasına izin verin. Java Web geliştirme deneyimi olan herkes, Servlet'in tek örnek ve çok iş parçacıklı bir şekilde çalıştığını ve her bir istekle ilgili verilerin Servlet alt sınıfının hizmet yönteminin (veya doGet veya doPost yönteminin) parametreleri aracılığıyla iletildiğini bilir. nın-nin. Sunucu uygulamasındaki kod yalnızca yerel değişkenleri kullandığı sürece, sunucu uygulaması senkronizasyon sorunlarına neden olmaz. SpringMVC denetleyicisi de aynısını yapar.İstekten elde edilen nesneler, sınıfın üyeleri yerine yöntem parametreleri olarak aktarılır.Açıktır ki, Struts 2 yaklaşımı tam tersidir, bu nedenle Struts 2'deki denetleyici olarak Action sınıfı Her istek bir örneğe karşılık gelir.

Java 5'ten önce eşzamanlı programlama

Java'nın iş parçacığı modeli, önleyici iş parçacığı zamanlamasına dayanmaktadır, yani:

  • Tüm iş parçacıkları aynı süreçte nesneleri kolayca paylaşabilir.
  • Bu nesnelere başvurabilen herhangi bir iş parçacığı bu nesneleri değiştirebilir.
  • Verileri korumak için nesneler kilitlenebilir.

Java'nın iş parçacıkları ve kilitlere dayalı eşzamanlılığı çok düşük seviyededir ve kilitlerin kullanımı çoğu durumda çok kötüdür, çünkü tüm eşzamanlılığı sırada beklemeye eşdeğerdir.

Java 5'ten önce, senkronize edilmiş anahtar sözcük kilit işlevini uygulamak için kullanılabilir, kod blokları ve yöntemlerinde kullanılabilir, bu da iş parçacığının tüm kod bloğunu veya yöntemi çalıştırmadan önce uygun bir kilit elde etmesi gerektiği anlamına gelir. Sınıfın statik olmayan yöntemi (üye yöntemi) için bu, nesne örneğinin kilidinin elde edilmesi gerektiği anlamına gelir.Sınıfın statik yöntemi (sınıf yöntemi) için, sınıfın Class nesnesinin kilidi elde edilmelidir.Senkronizasyon kodu bloğu için program Yönetici, o nesnenin kilidinin alınacağını belirleyebilir.

Senkronize bir kod bloğu veya senkronize bir yöntem olup olmadığına bakılmaksızın, bir seferde yalnızca bir iş parçacığı girebilir. Diğer iş parçacıkları girmeye çalışırsa (aynı eşitlenmiş blok veya farklı bir eşitlenmiş blok), JVM bunları askıya alır (bekleme kilit havuzuna koyun) . Bu yapı, eşzamanlılık teorisinde kritik bölüm olarak adlandırılır. Burada, Java'da senkronize edilmiş senkronizasyon ve kilit işlevlerinin gerçekleştirilmesinin bir özetini yapabiliriz:

  • Temel veri türlerini değil, yalnızca nesneleri kilitleyebilir
  • Kilitli nesne dizisindeki tek bir nesne kilitlenmeyecek
  • Senkronize bir yöntem, tüm yöntemi içeren senkronize edilmiş (bu) {} bir kod bloğu olarak kabul edilebilir
  • Statik senkronizasyon yöntemi Class nesnesini kilitleyecektir
  • İç sınıfların senkronizasyonu dış sınıflardan bağımsızdır
  • Senkronize değiştirici, yöntem imzasının bir parçası değildir, bu nedenle arabirimin yöntem bildiriminde görünemez
  • Senkronize olmayan yöntemler kilidin durumunu önemsemez, senkronize edilmiş yöntem çalışırken yine de çalışabilirler.
  • Senkronize kilitler, evresel kilitlerdir.

JVM'de, verimliliği artırmak için, aynı anda çalışan her iş parçacığı işlediği verinin önbelleğe alınmış bir kopyasına sahip olacaktır.Senkronizasyon için eşzamanlı kullandığımızda, gerçekten eşzamanlı olan, farklı evrelerde kilitli nesneyi temsil eden bellek bloğudur. (Kopyalanan veriler ana hafıza ile senkronize tutulacaktır. Artık neden kelime senkronizasyonunu kullanmanız gerektiğini biliyorsunuz) Basitçe ifade etmek gerekirse, senkronizasyon bloğu veya senkronizasyon yöntemi yürütüldükten sonra, kilitli nesnede yapılacak herhangi bir değişiklik kilitten önce serbest bırakılmalıdır. Önceden ana belleğe geri yazılmıştır; kilidi elde etmek için senkronizasyon bloğuna girildikten sonra kilitli nesnenin verileri ana bellekten okunur ve kilidi tutan ipliğin veri kopyası ana bellekteki veri görünümü ile senkronize edilmelidir.

Java'nın orijinal sürümünde, basit bir eşzamanlı işleme mekanizması olan Volatile adlı bir anahtar kelime vardı, çünkü uçucu tarafından değiştirilen değişkenler aşağıdaki kuralları izler:

  • Değişkenin değeri kullanılmadan önce her zaman ana bellekten okunur.
  • Değişken değerlerde yapılan değişiklikler, tamamlandıktan sonra her zaman ana belleğe geri yazılacaktır.

Volatile anahtar sözcüğünü kullanmak, çok iş parçacıklı bir ortamda derleyicinin yanlış optimizasyon varsayımlarını önleyebilir (derleyici, değeri bir iş parçacığında değeri sabitlere dönüşmeyen değişkenleri optimize edebilir), ancak yalnızca değiştirildiklerinde mevcut duruma güvenmezler (okuyun Değişken, uçucu bir değişken olarak beyan edilmelidir.

Değişmez mod, eşzamanlı programlamada da düşünülebilecek bir tasarımdır. Nesnenin durumunu değiştirmeyin.Nesnenin durumunu değiştirmek istiyorsanız, nesnenin bir kopyasını oluşturacak ve değişiklikleri orijinal nesneyi değiştirmeden kopyaya yazacaksınız, böylece durumda hiçbir tutarsızlık olmayacak, böylece değişmez nesne İş parçacığı güvenli. Java'da çok sık kullandığımız String sınıfı bu tasarımı kullanır. Değişmez kalıba aşina değilseniz, Dr. Yan Hong'un "Java ve Kalıplar" kitabının 34. Bölümünü okuyabilirsiniz. Bundan bahsetmişken, son anahtar kelimelerin önemini de anlayabilirsiniz.

Java 5'te eşzamanlı programlama

Java'nın gelecekteki gelişimi veya çöküşü ne olursa olsun, Java 5 kesinlikle Java geliştirme tarihinde son derece önemli bir versiyondur.Bu sürümün sağladığı çeşitli dil özelliklerini burada tartışmayacağız. (İlgileniyorsanız diğer yazımı okuyabilirsiniz. "Javanın 20. Yılı: Java Sürümlerinin Evriminden Programlama Teknolojisinin Gelişimini Görüntülemek İçin"), ancak dönüm noktası başyapıtı java.util.concurrent paketini Java 5'te sağladığı için Doug Lea'ya teşekkür etmeliyiz. Görünüşü, Java'nın Eşzamanlı programlamanın daha fazla seçeneği ve daha iyi çalışma yolları vardır. Doug Leanın başyapıtları esas olarak şunları içerir:

  • Daha iyi iş parçacığı güvenli kaplar
  • İş parçacığı havuzu ve ilgili araçlar
  • İsteğe bağlı engellemesiz çözüm
  • Kilit ve semafor mekanizmasını göster

Bunları tek tek yorumlayalım.

Atomik sınıf

AtomicInteger ve AtomicLong gibi Atomic ile başlayan çeşitli sınıflara sahip Java 5'te java.util.concurrent paketi altında atomik bir alt paket bulunmaktadır. Atomik işlemleri engellemeden tamamlamak için modern işlemcilerin özelliklerinden yararlanırlar.Kod aşağıdaki gibidir:

/ ** Kimlik dizisi üreteci * / public class IdGenerator { özel final AtomicLong sıraNumarası = new AtomicLong (0); public long next () { return sequenceNumber.getAndIncrement (); } }

Kilidi göster

Senkronize anahtar kelimeye dayalı kilit mekanizması aşağıdaki problemlere sahiptir:

  • Yalnızca bir tür kilit vardır ve tüm senkronizasyon işlemlerinde aynı etkiye sahiptir.
  • Kilit yalnızca kod bloğunun veya yöntemin başlangıcında elde edilebilir ve sonunda serbest bırakılabilir
  • Konu ya kilitli ya da bloke edilmiş, başka bir olasılık yok

Java 5, kilit mekanizmasını yeniden düzenledi ve kilit mekanizmasını aşağıdaki yönlerden geliştirebilen açık kilitler sağlar:

  • Okuma kilitleri ve yazma kilitleri gibi farklı kilit türleri ekleyebilirsiniz
  • Bir yöntemi kilitleyebilir ve başka bir yöntemde kilidi açabilirsiniz
  • Kilidi almaya çalışmak için tryLock'u kullanabilirsiniz. Kilidi alamazsanız bekleyebilir, geri alabilir veya başka bir şey yapabilirsiniz. Elbette, zaman aşımından sonra da işlemden vazgeçebilirsiniz.

Görüntülenen kilitlerin tümü java.util.concurrent.Lock arayüzünü uygular ve iki ana uygulama sınıfı vardır:

  • ReentrantLock - senkronize olandan biraz daha esnek bir yeniden giriş kilidi
  • ReentrantReadWriteLock-Çok sayıda okuma işlemi ve az sayıda yazma işlemi olduğunda daha iyi performansa sahip bir evresel kilit

Ekran kilidinin nasıl kullanılacağını öğrenmek için, Java röportaj serisi makalemdeki "Java Görüşme Soru Seti 51-70" 60. soru koduna başvurabilirsiniz. Hatırlatılması gereken tek bir şey var, kilit açma yöntemi kilit açma çağrısının en sonunda blokta olması en iyisidir, çünkü burası dış kaynakları serbest bırakmak için en iyi yerdir, tabii ki, aynı zamanda kilidi serbest bırakmak için de en iyi yerdir, çünkü normal istisna ne olursa olsun kilidi açmak zorunda kalabilir. Diğer konulara çalışma şansı verin.

CountDownLatch

CountDownLatch, kritik kaynaklara eşzamanlı erişimin neden olduğu çeşitli sorunları önlemek için bir iş parçacığının bir veya daha fazla iş parçacığının işlerini tamamlamasını beklemesine izin veren basit bir eşitleme modudur. CountDownLatch'in nasıl çalıştığını göstermek için başka birinden bir kod parçası ödünç alalım (üzerinde biraz yeniden düzenleme yaptım).

import java.util.concurrent.CountDownLatch; / ** * İşçiler * @author Luo Hao * * / class Worker { private String adı; // adı özel uzun çalışma süresi; // çalışma süresi / ** * Yapıcı * / public Worker (Dize adı, uzun çalışma süresi) { this.name = isim; this.workDuration = workDuration; } / ** * İşi tamamla * / public void doWork () { System.out.println (isim + "çalışmaya başlar ..."); Deneyin { Thread.sleep (workDuration); // İşin yürütme zamanını simüle etmek için uykuyu kullan } catch (InterruptedException ex) { ex.printStackTrace (); } System.out.println (isim + "işi bitirdi ..."); } } / ** * Test ipliği * @author Luo Hao * * / class WorkerTestThread Runnable { özel işçi işçi; özel CountDownLatch cdLatch; public WorkerTestThread (Çalışan işçi, CountDownLatch cdLatch) { this.worker = işçi; this.cdLatch = cdLatch; } @Override public void run () { worker.doWork (); // Çalışanın çalışmaya başlamasına izin verin cdLatch.countDown (); // Çalışma tamamlandıktan sonra geri sayım sayacı 1 azaltılır } } class CountDownLatchTest { private static final int MAX_WORK_DURATION = 5000; // Maksimum çalışma süresi private static final int MIN_WORK_DURATION = 1000; // Minimum çalışma süresi // Rastgele çalışma saatleri oluşturun özel statik uzun getRandomWorkDuration (uzun min, uzun maks) { dönüş (uzun) (Math.random () * (maks-min) + min); } public static void main (String args) { CountDownLatch latch = new CountDownLatch (2); // Bir geri sayım mandalı oluşturun ve geri sayım sayacını 2 olarak belirtin İşçi w1 = yeni İşçi ("Luo Hao", getRandomWorkDuration (MIN_WORK_DURATION, MAX_WORK_DURATION)); Çalışan w2 = new Worker (" ", getRandomWorkDuration (MIN_WORK_DURATION, MAX_WORK_DURATION)); new Thread (new WorkerTestThread (w1, latch)). start (); new Thread (new WorkerTestThread (w2, latch)). start (); Deneyin { latch.await (); // Geri sayım mandalının 0'a düşmesini bekleyin System.out.println ("Tüm işler tamamlandı!"); } catch (InterruptedException e) { e.printStackTrace (); } } }

ConcurrentHashMap

ConcurrentHashMap, HashMap'in eşzamanlı ortamdaki sürümüdür. Collections.synchronizedMap aracılığıyla iş parçacığı güvenli eşlenmiş kapsayıcılar alabileceğiniz için, neden ConcurrentHashMap'e ihtiyacınız olduğunu sorabilirsiniz. Koleksiyonlar araç sınıfı aracılığıyla elde edilen iş parçacığı güvenli HashMap, verileri okurken ve yazarken tüm kapsayıcı nesnesini kilitleyeceğinden, kabı kullanan diğer iş parçacıkları artık nesnenin kilidini artık elde edemez, bu da sonsuza kadar beklemesi gerektiği anlamına gelir. Kilidi alan bir iş parçacığı, senkronize kod bloğunu terk ettikten sonra çalıştırma şansına sahiptir. Aslında HashMap, anahtar / değer çiftlerini depolayan grubu belirlemek için bir karma işlevi kullanır (paket, karma çakışmalarını çözmek için sunulur). HashMap'i değiştirirken, tüm kapsayıcıyı kilitlemenize gerek yoktur, yalnızca yaklaşan değişikliği kilitlemeniz gerekir. Bucket "yapacak. HashMap'in veri yapısı aşağıdaki şekilde gösterilmektedir.

Ek olarak, ConcurrentHashMap ayrıca aşağıda gösterildiği gibi atomik işlemler için yöntemler sağlar:

  • putIfAbsent: Karşılık gelen anahtar / değer çifti eşlemesi yoksa, bunu HashMap'e ekleyin.
  • remove: Anahtar varsa ve değer mevcut duruma eşitse (karşılaştırma sonucuna eşittir), anahtar / değer çifti eşlemesi atomik olarak kaldırılır
  • replace: Haritadaki öğeleri değiştiren atomik bir işlem

CopyOnWriteArrayList

CopyOnWriteArrayList, eşzamanlı bir ortamda ArrayList'e bir alternatiftir. CopyOnWriteArrayList, yazma üzerine kopyalama anlambilimlerini ekleyerek eşzamanlı erişimin neden olduğu sorunları önler; bu, herhangi bir değişiklik işleminin altta listenin bir kopyasını oluşturacağı anlamına gelir; bu, önceden var olan yineleyicinin beklenmedik değişikliklerle karşılaşmayacağı anlamına gelir. Bu yöntem, daha iyi performans sağladığı için katı okuma-yazma eşitlemesi gerektirmeyen senaryolar için çok kullanışlıdır. Kilit kullanımını en aza indirmeyi unutmayın, çünkü bu kaçınılmaz olarak performans düşüşüne neden olacaktır (veritabanındaki verilere eşzamanlı erişim için aynı değil mi? Yapabiliyorsanız, kötümser kilitlerden vazgeçmeli ve iyimser kilitler kullanmalısınız), CopyOnWriteArrayList de açıkça Zaman, yerden feda edilerek kazanılır (bilgisayar dünyasında, zaman ve mekan genellikle uzlaşmaz çelişkilerdir. Verimliliği artırmak için alan feda edilebilir ve zaman elde edilebilir. Elbette, alan kullanımını azaltmak için zaman da feda edilebilir).

Aşağıdaki iki kod parçasını çalıştırarak CopyOnWriteArrayList'in iş parçacığı açısından güvenli bir kapsayıcı olup olmadığını doğrulayabilirsiniz.

import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class AddThread Runnable'ı uygular { özel liste < Çift > liste; genel AddThread (Liste < Çift > liste) { this.list = liste; } @Override public void run () { for (int i = 0; i < 10000; ++ i) { list.add (Math.random ()); } } } public class Test05 { private static final int THREAD_POOL_SIZE = 2; public static void main (String args) { Liste < Çift > list = new ArrayList < > (); ExecutorService es = Executors.newFixedThreadPool (THREAD_POOL_SIZE); es.execute (yeni AddThread (liste)); es.execute (yeni AddThread (liste)); es.shutdown (); } }

Yukarıdaki kod, çalışma zamanında ArrayIndexOutOfBoundsException oluşturacaktır. Yukarıdaki kodun 25. satırındaki ArrayList'i CopyOnWriteArrayList ile değiştirmeyi deneyin ve tekrar çalıştırın.

Liste < Çift > list = new CopyOnWriteArrayList < > ();

Kuyruk

Sıra, her yerde bulunan güzel bir kavramdır. Kaynakları işleme birimlerine dağıtmanın basit ve güvenilir bir yolunu sağlar (soruna nasıl baktığınıza bağlı olarak iş birimlerini işlenecek kaynaklara tahsis ettiği de söylenebilir) . Uygulamadaki birçok eşzamanlı programlama modeli, iş parçacıkları arasında iş birimlerini geçirebildiğinden uygulanacak kuyruklara dayanır.

Java 5'te BlockingQueue, eşzamanlı ortamda çok kullanışlı bir araçtır Kuyruğa eleman eklemek için put yöntemini çağırırken, kuyruk doluysa, öğeyi ekleyen iş parçacığının kuyruğun yer açmasını beklemesine izin verir; take yöntemini çağırın Kuyruktan öğeler getirilirken, kuyruk boşsa, öğeleri getiren iş parçacığı engellenecektir.

BlockingQueue, üretici-tüketici eşzamanlılık modelini uygulamak için kullanılabilir (sonraki bölümde anlatılmıştır). Tabii ki, Java 5'ten önce, bekle ve bildir iş parçacığı zamanlamasını uygulamak için de kullanılabilir. Bilinmesi gereken iki kodu mevcut eşzamanlılık araçlarına göre karşılaştırın. Eşzamanlı kodu yeniden düzenlemek için sınıfları kullanmanın ne kadar iyi olduğu.

Bekle ve bildirime dayalı uygulama

import java.util.ArrayList; import java.util.List; java.util.UUID'yi içe aktarın; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; / ** * Kamu sabiti * @author Luo Hao * * / class Constants { public static final int MAX_BUFFER_SIZE = 10; public static final int NUM_OF_PRODUCER = 2; public static final int NUM_OF_CONSUMER = 3; } / ** * iş görevleri * @author Luo Hao * * / class Task { private String kimliği; // görev numarası public Task () { id = UUID.randomUUID (). toString (); } @Override public String toString () { "Görev" döndür; } } / ** * Tüketici * @author Luo Hao * * / class Tüketici Runnable { özel liste < Görev > tampon; public Consumer (Liste) < Görev > buffer) { this.buffer = arabellek; } @Override public void run () { while (true) { senkronize (arabellek) { while (buffer.isEmpty ()) { Deneyin { buffer.wait (); } catch (InterruptedException e) { e.printStackTrace (); } } Görev görevi = buffer.remove (0); buffer.notifyAll (); System.out.println ("Tüketici aldı" + görev); } } } } / ** * Üretici * @author Luo Hao * * / sınıf Yapımcı Runnable { özel liste < Görev > tampon; genel Yapımcı (Liste < Görev > buffer) { this.buffer = arabellek; } @Override public void run () { while (true) { senkronize (arabellek) { while (buffer.size () > = Sabitler.MAX_BUFFER_SIZE) { Deneyin { buffer.wait (); } catch (InterruptedException e) { e.printStackTrace (); } } Görev görevi = yeni Görev (); buffer.add (görev); buffer.notifyAll (); System.out.println ("Yapımcı koydu" + görev); } } } } public class Test06 { public static void main (String args) { Liste < Görev > buffer = new ArrayList < > (Sabitler.MAX_BUFFER_SIZE); ExecutorService es = Executors.newFixedThreadPool (Sabitler.NUM_OF_CONSUMER + Sabitler.NUM_OF_PRODUCER); for (int i = 1; i < = Sabitler.NUM_OF_PRODUCER; ++ i) { es.execute (yeni Üretici (arabellek)); } for (int i = 1; i < = Sabitler.NUM_OF_CONSUMER; ++ i) { es.execute (yeni Tüketici (tampon)); } } }

BlockingQueue'ya dayalı uygulama

java.util.UUID'yi içe aktarın; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; / ** * Kamu sabiti * @author Luo Hao * * / class Constants { public static final int MAX_BUFFER_SIZE = 10; public static final int NUM_OF_PRODUCER = 2; public static final int NUM_OF_CONSUMER = 3; } / ** * iş görevleri * @author Luo Hao * * / class Task { private String kimliği; // görev numarası public Task () { id = UUID.randomUUID (). toString (); } @Override public String toString () { "Görev" döndür; } } / ** * Tüketici * @author Luo Hao * * / class Tüketici Runnable { private BlockingQueue < Görev > tampon; public Consumer (BlockingQueue < Görev > buffer) { this.buffer = arabellek; } @Override public void run () { while (true) { Deneyin { Görev görev = buffer.take (); System.out.println ("Tüketici aldı" + görev); } catch (InterruptedException e) { e.printStackTrace (); } } } } / ** * Üretici * @author Luo Hao * * / sınıf Yapımcı Runnable { private BlockingQueue < Görev > tampon; public Producer (BlockingQueue < Görev > buffer) { this.buffer = arabellek; } @Override public void run () { while (true) { Deneyin { Görev görevi = yeni Görev (); buffer.put (görev); System.out.println ("Yapımcı koydu" + görev); } catch (InterruptedException e) { e.printStackTrace (); } } } } public class Test07 { public static void main (String args) { BlockingQueue < Görev > buffer = yeni LinkedBlockingQueue < > (Sabitler.MAX_BUFFER_SIZE); ExecutorService es = Executors.newFixedThreadPool (Sabitler.NUM_OF_CONSUMER + Sabitler.NUM_OF_PRODUCER); for (int i = 1; i < = Sabitler.NUM_OF_PRODUCER; ++ i) { es.execute (yeni Üretici (arabellek)); } for (int i = 1; i < = Sabitler.NUM_OF_CONSUMER; ++ i) { es.execute (yeni Tüketici (tampon)); } } }

BlockingQueue kullandıktan sonra kod çok daha zariftir.

Eşzamanlılık modeli

Aşağıdaki tartışmaya devam etmeden önce birkaç kavramı gözden geçirelim:

Kavram açıklaması Kritik kaynaklar Eşzamanlı bir ortamda, sabit sayıda birbirini dışlayan kaynak vardır. Kaynaklara erişim özel bir açlıktır. Bir veya bir grup iş parçacığı uzun bir süre veya hiçbir zaman ilerleme kaydedemez. Kilitlenme. İki veya daha fazla iş parçacığı birbirinin canlılığı bitirmesini bekler. Yürütülecek evre her zaman diğer evrelerin çalıştığını bulur, böylece uzun bir süre veya sonsuza kadar çalıştırılamaz.

Bu kavramları inceledikten sonra aşağıdaki eşzamanlılık modellerini keşfedebiliriz.

Üretici-tüketici

Bir veya daha fazla üretici belirli işler yaratır ve bunları bir ara belleğe veya kuyruğa yerleştirir ve bir veya daha fazla tüketici bu işleri kuyruktan alıp tamamlar. Buradaki tampon veya sıra kritik bir kaynaktır. Tampon veya kuyruk dolduğunda üretim engellenecek ve tampon veya kuyruk boş olduğunda tüketici bloke edilecektir. Üreticinin ve tüketicinin programlaması, ikisi arasındaki sinyal alışverişi ile tamamlanır.

Okuyucu-yazar

Okuyuculara temel olarak bilgi sağlayan bir paylaşılan kaynak olduğunda, zaman zaman yazarlar tarafından güncellenecektir, ancak sistemin verimi dikkate alınmalı ve açlık ve eski kaynakların güncellenmemesi sorunlarından kaçınılmalıdır. Bu eşzamanlılık modelinde okuyucu ile yazar arasında nasıl denge kurulacağı en zor olanıdır.Tabii ki bu konu hala çok tartışılan bir konu, korkarım bunları dünyanın her yerine koymak yerine belirli senaryolara göre uygun çözümler sunmalıyız. Tüm yöntemler (yerli bilimsel araştırma literatüründe gördüğümden farklı olarak).

Filozofun yemeği

1965'te Hollandalı Bilgisayar Bilimcileri Turing Ödülü'nü kazanan Edsger Wybe Dijkstra, filozofun yemekleri adını verdiği bir senkronizasyon problemini önerdi ve çözdü. Bu problem kısaca şöyle anlatılabilir: Beş filozof bir yuvarlak masa etrafında oturur ve her filozof bir tabak makarna alır. Makarna çok kaygan olduğu için tutmak için iki çatal gerekir. Aşağıdaki şekilde gösterildiği gibi iki bitişik plaka arasına bir çatal yerleştirilir. Bir filozofun hayatında iki alternatif etkinlik dönemi vardır: yemek yemek ve düşünme. Bir filozof aç hissettiğinde, sol ve sağ çatallarını birer birer, ancak belirli bir sıra olmadan iki kez almaya çalışır. İki çatal almayı başarırsanız, yemeye başlayın ve yedikten sonra çatallarınızı indirin ve düşünmeye devam edin.

Yukarıdaki sorudaki filozofu iplerle değiştirin ve çatalları rekabet eden kritik kaynaklarla değiştirin Yukarıdaki sorun, ipliklerin kaynaklar için rekabet etmesidir. Dikkatli bir şekilde tasarlanmazsa, sistemde kilitlenme, canlı kilitlenme ve verim düşüşü gibi sorunlar olacaktır.

Aşağıda, filozofun yemek problemini çözmek için semafor ilkellerini kullanan kod, Java 5 Eş Zamanlılık Araç Kitindeki Semafor sınıfını kullanarak (kod yeterince güzel değil, ancak sorunu göstermek için yeterlidir).

// java.util.concurrent.ExecutorService'i içe aktar; // java.util.concurrent.Executors; import java.util.concurrent.Semaphore; / ** * İş parçacığı paylaşımlı semaforları depolamanın üst ve alt soruları * @author Luo Hao * * / class AppContext { public static final int NUM_OF_FORKS = 5; // Çatal sayısı (kaynaklar) public static final int NUM_OF_PHILO = 5; // Filozofların sayısı (iş parçacığı) public static Semafor çatalları; // Çatalın semaforu public static Semafor sayacı; // filozofun semaforu statik { çatallar = yeni Semafor; for (int i = 0, len = forks.length; i < len; ++ i) { çatallar = new Semafor (1); // Her çatalın semaforu 1'dir } counter = new Semafor (NUM_OF_PHILO-1); // N filozof varsa, en fazla N-1 kişinin aynı anda çatal almasına izin verilir } / ** * Çatal alın * @param index Hangi filozof * @param leftİlk önce sol çatalı alıp almama * @throws InterruptedException * / public static void putOnFork (int index, boolean leftFirst) InterruptedException { eğer (leftFirst) { forks.acquire (); forks.acquire (); } Başka { forks.acquire (); forks.acquire (); } } / ** * Çatalı geri koyun * @param index Hangi filozof * @param leftFirst Çatalı önce sola mı koyacaksın * @throws InterruptedException * / public static void putDownFork (int index, boolean leftFirst) InterruptedException { eğer (leftFirst) { forks.release (); forks.release (); } Başka { forks.release (); forks.release (); } } } / ** * Filozof * @author Luo Hao * * / class Philosopher, Runnable { özel int indeksi; // sayı private String adı; // adı public Philosopher (int dizin, Dize adı) { this.index = dizin; this.name = isim; } @Override public void run () { while (true) { Deneyin { AppContext.counter.acquire (); boole leftFirst = dizin% 2 == 0; AppContext.putOnFork (dizin, solİlk); System.out.println (isim + "Makarna yemek (makarna) ..."); // İki çatal ile yiyebilirsiniz AppContext.putDownFork (dizin, solİlk); AppContext.counter.release (); } catch (InterruptedException e) { e.printStackTrace (); } } } } public class Test04 { public static void main (String args) { Dize adları = {"Luo Hao", "Wang Dahui", "Zhang Sanfeng", "Yang Guo", "Li Mochou"}; // 5 filozofun adı // ExecutorService es = Executors.newFixedThreadPool (AppContext.NUM_OF_PHILO); // Sabit boyutlu bir iş parçacığı havuzu oluştur // for (int i = 0, len = names.length; i < len; ++ i) { // es.execute (yeni Filozof (i, isimler )); // iş parçacığını başlat //} // es.shutdown (); for (int i = 0, len = names.length; i < len; ++ i) { yeni Konu (yeni Filozof (i, isimler )).Başlat(); } } }

Gerçekte eşzamanlılık problemleri temelde bu üç modelin bu üç modeli veya varyantlarıdır.

Eşzamanlı kodu test et

Eşzamanlı kodun test edilmesi de çok zordur ve açıklama olmaksızın herkes için açık olduğu ölçüde çok zordur, bu yüzden burada sadece bu zor problemi nasıl çözeceğimizi tartışacağız. Sorunları bulabilecek bazı testler yazmanızı ve bu testleri sık sık farklı konfigürasyonlar ve farklı yükler altında çalıştırmanızı öneririz. Başarısız olan herhangi bir testi görmezden gelmeyin İş parçacığı kodundaki bir hata on binlerce testte yalnızca bir kez görünebilir. Özellikle, birkaç husus vardır:

  • Sistemin arızasını tesadüfi olaylara bağlamayın, tıpkı boku çekemediğinizde olduğu gibi, yerçekimine sahip olmadığı için dünyayı suçlayamazsınız.
  • Önce eşzamanlı olmayan kodun çalışmasına izin verin ve aynı anda eşzamanlı ve eşzamanlı olmayan koddaki hataları bulmaya çalışmayın.
  • Farklı konfigürasyon ortamlarında çalışabilen iş parçacığı kodu yazın.
  • Performansı optimize etmek için iş parçacığını ayarlayabilmeniz için ayarlaması kolay iş parçacığı kodu yazın.
  • İşlemci sayısını CPU veya CPU çekirdeği sayısından daha fazla yapın, böylece CPU programlama anahtarlama işlemindeki olası sorunlar ortaya çıkacaktır.
  • Eşzamanlı kodun farklı platformlarda çalışmasına izin verin.
  • Otomasyon veya sabit kodlama yoluyla eşzamanlı koda bazı yardımcı test kodları ekleyin.

Java 7'de eşzamanlı programlama

Java 7, BlockingQueue'dan daha fazla transfer adı verilen bir yöntemi olan TransferQueue'yu tanıttı.Alıcı iş parçacığı bekleme durumundaysa, işlem görevi hemen ona devredebilir, aksi takdirde görevi alan iş parçacığı görünene kadar engelleyecektir. Daha iyi performans alabileceği için BlockingQueue yerine TransferQueue kullanabilirsiniz.

Az önce bir şeyi unuttum. Java 5 ayrıca Eşzamanlı uygulamalar da oluşturabileceğiniz Çağrılabilir arabirimi, Gelecek arabirimini ve FutureTask arabirimini tanıttı.Kod aşağıda gösterilmiştir.

import java.util.ArrayList; import java.util.List; java.util.concurrent.Callable dosyasını içe aktarın; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; java.util.concurrent.Future içe aktarma; public class Test07 { özel statik final int POOL_SIZE = 10; statik sınıf CalcThread Callable uygular < Çift > { özel liste < Çift > dataList = new ArrayList < > (); public CalcThread () { for (int i = 0; i < 10000; ++ i) { dataList.add (Math.random ()); } } @Override public Double call () İstisna {atar çift toplam = 0; for (Double d: dataList) { toplam + = d; } return total / dataList.size (); } } public static void main (String args) { Liste < Gelecek < Çift > > fList = new ArrayList < > (); ExecutorService es = Executors.newFixedThreadPool (POOL_SIZE); for (int i = 0; i < POOL_SIZE; ++ i) { fList.add (es.submit (yeni CalcThread ())); } Gelecek için < Çift > f: fList) { Deneyin { System.out.println (f.get ()); } catch (İstisna e) { e.printStackTrace (); } } es.shutdown (); } }

Çağrılabilir arabirim aynı zamanda tek yöntemli bir arabirimdir Açıkçası bu, işlevsel programlamadaki geri arama işlevine benzer bir geri arama yöntemidir.Java 8'den önce, Java'daki bu işlevsel programlamayı basitleştirmek için Lambda ifadeleri kullanılamazdı. Çalıştırılabilir arabirimden farkı, Çağrılabilir arabirimin geri çağrı yöntemi çağrı yönteminin, gelecekte iş parçacığı yürütmesinin sonunda bilgi elde edebilen bir nesne döndürmesidir. Yukarıdaki koddaki çağrı yöntemi, 0 ile 1 arasında 10.000 rastgele ondalık hesaplanan ortalama değeri döndürmektir. Bu dönüş değerini Future arayüzünün bir nesnesinden alıyoruz. En son Java sürümünde, hem Çağırılabilir arabirim hem de Çalıştırılabilir arabirim @FunctionalInterface açıklamaları ile işaretlenmiştir; bu, arabirim nesneleri oluşturmak için işlevsel programlamayı (Lambda ifadeleri) kullanabileceği anlamına gelir.

Aşağıdakiler, Gelecek arayüzünün ana yöntemleridir:

  • get (): Sonucu alın. Sonuç hazır değilse, get yöntemi sonuç alınana kadar bloke olacaktır; tabii ki, bloklama zaman aşımı süresi de parametrelerle ayarlanabilir.
  • cancel (): İşlem bitmeden iptal edin.
  • isDone (): işlemin bitip bitmediğini belirlemek için kullanılabilir.

Java 7 ayrıca, iş parçacığı havuzundaki görevlerin otomatik planlamasını gerçekleştirebilen bir çatal / birleştirme çerçevesi sağlar ve bu zamanlama, kullanıcılar için şeffaftır. Bu etkiyi elde etmek için, görevin kullanıcı tarafından belirlenen şekilde ayrıştırılması ve ardından ayrıştırılmış küçük görevlerin yürütme sonuçlarının orijinal görevin yürütme sonuçlarıyla birleştirilmesi gerekir. Bu açıkça böl ve yönet fikrini kullanır. Aşağıdaki kod, 1'den 10000'e kadar olan toplamı hesaplamak için dallanma / birleştirme çerçevesini kullanır. Elbette, böylesine basit bir görev için, dallanma / birleştirme çerçevesine hiç gerek yoktur, çünkü dallanma ve birleştirme de bazı ek yükler getirecektir, ancak burada sadece araştırıyoruz Kodumuzun modern çok çekirdekli CPU'ların güçlü bilgi işlem gücünden tam olarak yararlanabilmesi için kodda dallanma / birleştirme çerçevesi nasıl kullanılır.

import java.util.concurrent.ForkJoinPool; java.util.concurrent.Future içe aktarma; import java.util.concurrent.RecursiveTask; Class Calculator, RecursiveTask'ı genişletir < Tamsayı > { özel statik son uzun serialVersionUID = 7333472779649130114L; özel statik final int THRESHOLD = 10; özel int başlangıç; özel int end; public Calculator (int start, int end) { this.start = başlangıç; this.end = end; } @Override genel Tamsayı hesaplama () { int toplam = 0; eğer ((bitiş-başlangıç) < EŞİK) {// Sorun çözülebilir bir dereceye kadar ayrıştırıldığında sonucu doğrudan hesaplayın for (int i = start; i < = son; i ++) { toplam + = i; } } Başka { int middle = (başlangıç + bitiş) > > > 1; // Görevi ikiye ayırın Hesap makinesi sol = yeni Hesap Makinesi (başlangıç, orta); Hesap makinesi sağ = yeni Hesap Makinesi (orta + 1, son); left.fork (); right.fork (); // Not: Bu özyinelemeli bir görev ayrıştırması olduğundan, sonraki ikisinin dörde bölüneceği ve dördün sekize bölüneceği anlamına gelir ... sum = left.join () + right.join (); // İki alt görevin sonuçlarını birleştir } getiri toplamı; } } public class Test08 { public static void main (String args) Exception {atar ForkJoinPool forkJoinPool = new ForkJoinPool (); Gelecek < Tamsayı > sonuç = forkJoinPool.submit (yeni Hesap Makinesi (1, 10000)); System.out.println (sonuç.get ()); } }

Java 7'nin gelişiyle birlikte, Java'daki varsayılan dizi sıralama algoritması artık klasik hızlı sıralama (çift pivot hızlı sıralama) değildir.Yeni sıralama algoritmasına, birleştirme sıralama ve ekleme sıralamanın bir karışımı olan TimSort adı verilir. Daha iyi performans (daha kısa sıralama süresi) elde etmek için şube ve birleştirme çerçevesi aracılığıyla modern işlemcilerin çok çekirdekli özelliklerinden tam olarak yararlanabilirsiniz.

Üç ayda borsadan 1 milyon kazanmak için en "aptalca" yöntemi kullanmayı öğretin: açılıştan 30 dakika sonra, lütfen 6 "açılış dili" aklınızda bulundurun ve kaderi olanlara verin
önceki
Yılın ilk yarısında, Linyuanın özel sermaye ürünleri tekrar ikiye katlandı: Sonsuza kadar fakir olmak istemiyorsanız, fonunuzu ikiye katlamanın en iyi yolu olan kurumun "T yapmak için değişen pozisy
Sonraki
Xi'an adamı, tanıdık olmayan hesapları ve başka yerlerden ödenmemiş siparişleri göstermek için Didi'de oturum açmak için WeChat'i kullanıyor
Eski hissedarlar içki içtikten sonra itiraf ettiler: "İkinci kurul lideri belirler" 10 yönetim kurulu gerçek lideri fırlatmaya devam eden tek beceri, usta sonraki on tahtayı da yakalayabilirsiniz
La Liga bugünün ön sayfası: Atletico Madrid, M-Llorente ile sözleşme imzaladı, Coutinho Paris'e gitmek istiyor
Sci-tech Innovation Board bugün açılıyor ve bu tür hisse senetleri en büyük kazananlar olabilir
Liu Wenxi hakkında iki veya üç şey: Köylülerin peşine düşmek ve "Seni çizebilir miyim" diye sormak
Bu şimdiye kadar gördüğüm en mükemmel ticaret stratejisidir: "hareketli ortalama + MACD" nin mükemmel kombinasyonu borsada bir ders kitabı olarak adlandırılabilir ve borsada ustalaşmak için eti kesme
Borsada para kazananların önleyemeyeceği "sekiz kalıp" dan kaçarsan konuşalım, yoksa bir kez karşılaşınca kan nehre akar.
Sci-tech Innovation Board burada! Destansı "kataliz", bu tür stokların yükselmesi bekleniyor
A-hisseleri büyük yükselişten sonra iki gün için ayarlandı, yarın hala yükselebilirler mi?
Borsada 3 milyon kazanmanın en "aptalca" yolu: "MACD" nin 9 ana ticaret tekniğine tekrar tekrar uyun ve küçük fonlar yoksulluktan servete 10 kat ikiye katlanabilir
Bana bir numaralı yerli "T" adam tarafından verilen hisse senedi alım satım dersi: Sık işlem yapmayın, uzun süre hisse senedi tutun ve her gün T yapmakta ısrar edin, haftada en az 35 puan
"Kara Perşembe" den sonra yarın ne olacak?
To Top