Mövzu ilə qarşılıqlı əlaqənin xüsusiyyətlərinin qısa icmalı. Əvvəllər mövzuların bir-biri ilə necə sinxronlaşdığına baxdıq. Bu dəfə mövzuların qarşılıqlı əlaqəsi zamanı yarana biləcək problemlərə nəzər salacağıq və onlardan necə qaçınmaq barədə danışacağıq. Biz həmçinin daha dərindən öyrənmək üçün bəzi faydalı bağlantılar təqdim edəcəyik.
Super nümunəni burada tapa bilərsiniz: " Java - Thread Starvation and Fairness ". Bu nümunə mövzuların Aclıqda necə işlədiyini və Thread.sleep-dən Thread.wait-ə kiçik bir dəyişikliyin yükü necə bərabər paylaya biləcəyini göstərir.
Yəqin ki, bu videonun bu barədə heç nə deməməsi daha yaxşıdır. Ona görə də videonun linkini buraxacağam. Siz " Java - Münasibətlərdən əvvəl baş verənləri anlamaq " kitabını oxuya bilərsiniz .
Giriş
Beləliklə, biz bilirik ki, Java-da " Mövzu Java'yı korlaya bilməz: I Hissə - Mövzular " icmalında oxuya biləcəyiniz mövzular var və mövzular bir-biri ilə sinxronlaşdırıla bilər, biz nəzərdən keçirdik " Mövzu Java-nı korlaya bilməz ” Korlamaq: II hissə - Sinxronizasiya ." Mövzuların bir-biri ilə necə qarşılıqlı əlaqəsi haqqında danışmağın vaxtı gəldi. Onlar ümumi resursları necə bölüşürlər? Bununla bağlı hansı problemlər ola bilər?Çıxılmaz vəziyyət
Ən pis problem çıxılmaz vəziyyətdir. İki və ya daha çox mövzu bir-birini əbədi gözlədikdə, buna Çıxılmaz Kilit deyilir. Gəlin Oracle veb saytından " Çıxılmaz " konsepsiyasının təsvirindən bir nümunə götürək :public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
Burada çıxılmaz vəziyyət ilk dəfə görünməyə bilər, lakin proqramınızın icrası ilişibsə, işə başlamağın vaxtıdır jvisualvm
: JVisualVM-də plagin quraşdırılıbsa (Alətlər -> Pluginlər vasitəsilə), biz çıxılmazlığın harada baş verdiyini görə bilərik:
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Mövzu 1 0-dan kilid gözləyir. Bu niyə baş verir? Thread-1
icraya başlayır və metodu icra edir Friend#bow
. Açar sözü ilə qeyd olunur synchronized
, yəni monitoru ilə götürürük this
. Metodun girişində başqa birinə keçid aldıq Friend
. İndi ip Thread-1
başqa bir metodu yerinə yetirmək istəyir Friend
və bununla da ondan bir kilid əldə edir. Ancaq başqa bir mövzu (bu vəziyyətdə Thread-0
) metoda daxil ola bilsə bow
, kilid artıq məşğuldur və Thread-1
gözləyir Thread-0
və əksinə. Bloklama həll edilmir, ona görə də Ölüdür, yəni ölüdür. Həm ölüm tutuşu (buraxmaq mümkün olmayan), həm də qaça bilməyəcəyi ölü blok. Çıxılma mövzusunda videoya baxa bilərsiniz: " Deadlock - Paralellik #1 - Təkmil Java ".
Canlı kilid
Əgər çıxılmaz vəziyyət varsa, deməli Livelock varmı? Bəli, var) Livelock budur ki, iplər zahirən canlı görünür, amma eyni zamanda heç bir şey edə bilmirlər, çünki... onların işlərini davam etdirməyə çalışdıqları şərt yerinə yetirilə bilməz. Əslində, Livelock dalana oxşayır, lakin iplər monitoru gözləyən sistemdə “asılmır”, lakin həmişə nəsə edirlər. Misal üçün:import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); //like Thread-1 or Thread-0
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
Bu kodun müvəffəqiyyəti Java mövzu planlayıcısının mövzuları başlatma ardıcıllığından asılıdır. Əvvəlcə başlasa Thead-1
, Livelock əldə edəcəyik:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Nümunədən göründüyü kimi, hər iki ip növbə ilə hər iki kilidi tutmağa çalışır, lakin uğursuz olur. Üstəlik, onlar dalana dirənmirlər, yəni vizual olaraq onlarda hər şey qaydasındadır və öz işlərini görürlər. JVisualVM-ə görə, biz yuxu dövrlərini və park dövrünü görürük (bu, ipin bir kilidi tutmağa çalışdığı zaman, ipin sinxronizasiyası haqqında danışarkən əvvəllər müzakirə etdiyimiz kimi, park vəziyyətinə keçir ). Livelock mövzusunda bir nümunə görə bilərsiniz: " Java - Thread Livelock ".
Aclıq
Bloklamaya əlavə olaraq (çıxmaz və canlı kilid), çox iş parçacığı ilə işləyərkən başqa bir problem var - Aclıq və ya "aclıq". Bu fenomen bloklamadan fərqlidir ki, iplər bloklanmır, lakin onların hər kəs üçün kifayət qədər resursları yoxdur. Buna görə də, bəzi iplər bütün icra müddətini götürsə də, digərləri icra edilə bilməz:https://www.logicbig.com/
Yarış Vəziyyəti
Multithreading ilə işləyərkən "yarış şərti" kimi bir şey var. Bu fenomen, mövzuların müəyyən bir mənbəni öz aralarında paylaşması və kodun bu vəziyyətdə düzgün işləməsini təmin etməyən şəkildə yazılmasından ibarətdir. Bir misala baxaq:public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Bu kod ilk dəfə xəta yaratmaya bilər. Və bu belə görünə bilər:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
Gördüyünüz kimi, təyin edilərkən newValue
nəsə səhv getdi və newValue
daha çox şey var idi. Yarış vəziyyətindəki bəzi ipliklər value
bu iki komanda arasında dəyişməyi bacardı. Gördüyümüz kimi, iplər arasında bir yarış meydana çıxdı. İndi təsəvvür edin ki, pul əməliyyatlarında oxşar səhvlərə yol verməmək nə qədər vacibdir... Nümunələr və diaqramlar burada da tapıla bilər: “ Java thread-də yarış vəziyyətini simulyasiya etmək üçün kod ”.
Uçucu
Mövzuların qarşılıqlı əlaqəsi haqqında danışarkən, açar sözü xüsusilə qeyd etmək lazımdırvolatile
. Sadə bir misala baxaq:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
Ən maraqlısı odur ki, yüksək ehtimalla işləməyəcək. Yeni mövzu dəyişikliyi görməyəcək flag
. Bunu düzəltmək üçün sahə üçün flag
açar söz təyin etməlisiniz volatile
. Necə və niyə? Bütün hərəkətlər prosessor tərəfindən həyata keçirilir. Ancaq hesablama nəticələrini bir yerdə saxlamaq lazımdır. Bu məqsədlə prosessorda əsas yaddaş və hardware keşi var. Bu prosessor keşləri verilənlərə əsas yaddaşa daxil olmaqdan daha sürətli daxil olmaq üçün kiçik yaddaş parçası kimidir. Ancaq hər şeyin bir mənfi tərəfi də var: keşdəki məlumatlar cari olmaya bilər (yuxarıdakı nümunədə olduğu kimi, bayraq dəyəri yenilənmədikdə). Beləliklə, açar söz volatile
JVM-ə dəyişənimizi keş etmək istəmədiyimizi bildirir. Bu, bütün mövzularda faktiki nəticəni görməyə imkan verir. Bu çox sadələşdirilmiş formuladır. Bu mövzuda " JSR 133 (Java Yaddaş Modeli) FAQvolatile
" tərcüməsini oxumaq çox tövsiyə olunur . Həm də sizə “ Java Yaddaş Modeli ” və “ Java Uçucu Açar Sözü ” materialları haqqında ətraflı oxumağı məsləhət görürəm . Bundan əlavə, bunun dəyişikliklərin atomikliyi ilə bağlı deyil, görmə qabiliyyəti ilə bağlı olduğunu xatırlamaq lazımdır . Kodu "Yarış Vəziyyəti"ndən götürsək, IntelliJ Idea-da bir işarə görəcəyik: Bu yoxlama (Yoxlama) 2010-cu ildə Buraxılış Qeydlərində sadalanan IDEA-61117 buraxılışının bir hissəsi kimi IntelliJ Idea-ya əlavə edilmişdir .volatile
Atomluq
Atom əməliyyatları bölünə bilməyən əməliyyatlardır. Məsələn, dəyişənə dəyər təyin etmək əməliyyatı atomikdir. Təəssüf ki, artım atom əməliyyatı deyil, çünki artım üç əməliyyat tələb edir: köhnə dəyəri əldə edin, ona bir əlavə edin və dəyəri yadda saxlayın. Nə üçün atomiklik vacibdir? Artım nümunəsində, yarış şərti baş verərsə, istənilən vaxt paylaşılan resurs (yəni, paylaşılan dəyər) qəfil dəyişə bilər. Bundan əlavə, 64 bitlik strukturların da atomik olmaması vacibdir, məsələnlong
və double
. Daha ətraflı burada oxuya bilərsiniz: " 64 bit dəyərləri oxuyarkən və yazarkən atomikliyi təmin edin ". Atomluq problemlərinə misal aşağıdakı nümunədə görünə bilər:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
Atomla işləmək üçün xüsusi sinif Integer
bizə həmişə 30000 göstərəcək, lakin value
zaman zaman dəyişəcək. Bu mövzuda " Java-da Atom Dəyişənlərinə Giriş " adlı qısa icmal var . Atomic Müqayisə et və dəyişdir alqoritminə əsaslanır. Bu barədə daha ətraflı Habré-dəki " JDK 7 və 8 nümunəsindən istifadə edərək Kilidsiz alqoritmlərin - CAS və FAA-nın müqayisəsi " məqaləsində və ya Vikipediyada " Mübadilə ilə müqayisə " haqqında məqalədə oxuya bilərsiniz .
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Əvvəl baş verir
Maraqlı və sirli bir şey var - Əvvəllər olur. Axınlar haqqında danışarkən, bu barədə oxumağa dəyər. Əvvəl baş verir əlaqəsi mövzular arasında hərəkətlərin görünmə sırasını göstərir. Çoxlu təfsir və təfsirlər var. Bu mövzuda ən son hesabatlardan biri bu hesabatdır:Nəticələr
Bu baxışda biz iplə qarşılıqlı əlaqənin xüsusiyyətlərinə baxdıq. Yarana biləcək problemləri və onların aşkarlanıb aradan qaldırılması yollarını müzakirə etdik. Mövzu ilə bağlı əlavə materialların siyahısı:- Bir daha ikiqat yoxlanılan kilidləmə haqqında
- JSR 133 (Java Yaddaş Modeli) Tez-tez verilən suallar (tərcümə)
- Təkmil Java - Parametrlik (Yuri Tkach)
- Duqlas Hawkins tərəfindən Java-da paralellik anlayışları (2017)
GO TO FULL VERSION