pengenalan
Jadi, kami tahu bahawa terdapat benang dalam Java, yang boleh anda baca dalam ulasan " Anda Tidak Boleh Merosakkan Java dengan Benang: Bahagian I - Benang ". Benang diperlukan untuk melakukan kerja secara serentak. Oleh itu, kemungkinan besar benang akan berinteraksi antara satu sama lain. Mari kita fahami bagaimana ini berlaku dan apakah kawalan asas yang kita ada.hasil
Kaedah Thread.yield() adalah misteri dan jarang digunakan. Terdapat banyak variasi penerangannya di Internet. Sehinggakan ada yang menulis tentang beberapa jenis barisan benang, di mana benang itu akan bergerak ke bawah dengan mengambil kira keutamaan mereka. Seseorang menulis bahawa utas akan menukar statusnya daripada berjalan kepada runnable (walaupun tiada pembahagian ke dalam status ini, dan Java tidak membezakan antara mereka). Tetapi pada hakikatnya, semuanya lebih tidak diketahui dan, dalam erti kata lain, lebih mudah. Mengenai topik dokumentasi kaedah,yield
terdapat pepijat " JDK-6416721: (benang spesifikasi) Betulkan Thread.yield() javadoc ". Jika anda membacanya, adalah jelas bahawa sebenarnya kaedah itu yield
hanya menyampaikan beberapa cadangan kepada penjadual benang Java bahawa benang ini boleh diberi masa pelaksanaan yang lebih sedikit. Tetapi apa yang sebenarnya akan berlaku, sama ada penjadual akan mendengar cadangan dan apa yang akan dilakukan secara umum bergantung pada pelaksanaan JVM dan sistem pengendalian. Atau mungkin dari beberapa faktor lain. Semua kekeliruan itu berkemungkinan besar disebabkan oleh pemikiran semula multithreading semasa pembangunan bahasa Jawa. Anda boleh membaca lebih lanjut dalam ulasan " Pengenalan Ringkas kepada Java Thread.yield() ".
Tidur - Benang tertidur
Seutas benang mungkin tertidur semasa pelaksanaannya. Ini adalah jenis interaksi yang paling mudah dengan utas lain. Sistem pengendalian di mana mesin maya Java dipasang, di mana kod Java dilaksanakan, mempunyai penjadual benang sendiri, dipanggil Penjadual Benang. Dialah yang memutuskan benang mana yang hendak dijalankan. Pengaturcara tidak boleh berinteraksi dengan penjadual ini terus daripada kod Java, tetapi dia boleh, melalui JVM, meminta penjadual untuk menjeda utas untuk seketika, untuk "meletakkannya." Anda boleh membaca lebih lanjut dalam artikel " Thread.sleep() " dan " How Multithreading works ". Selain itu, anda boleh mengetahui cara benang berfungsi dalam Windows OS: " Dalaman Windows Thread ". Sekarang kita akan melihatnya dengan mata kita sendiri. Mari simpan kod berikut pada failHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Seperti yang anda lihat, kami mempunyai tugasan yang menunggu 60 saat, selepas itu program tamat. Kami menyusun javac HelloWorldApp.java
dan menjalankan java HelloWorldApp
. Adalah lebih baik untuk melancarkan dalam tetingkap berasingan. Sebagai contoh, pada Windows ia akan menjadi seperti ini: start java HelloWorldApp
. Menggunakan arahan jps, kami mengetahui PID proses dan membuka senarai utas menggunakan jvisualvm --openpid pidПроцесса
: Seperti yang anda lihat, utas kami telah memasuki status Tidur. Malah, tidur benang semasa boleh dilakukan dengan lebih indah:
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Anda mungkin perasan bahawa kami memproses di mana-mana sahaja InterruptedException
? Mari kita fahami mengapa.
Mengganggu benang atau Benang.gangguan
Masalahnya ialah semasa benang menunggu dalam tidur, seseorang mungkin mahu mengganggu penantian ini. Dalam kes ini, kami mengendalikan pengecualian sedemikian. Ini dilakukan selepas kaedah ituThread.stop
diisytiharkan Dihentikan, i.e. ketinggalan zaman dan tidak diingini untuk digunakan. Sebabnya ialah apabila kaedah itu dipanggil, stop
benang itu hanya "dibunuh", yang sangat tidak dapat diramalkan. Kami tidak dapat mengetahui bila aliran akan dihentikan, kami tidak dapat menjamin ketekalan data. Bayangkan anda sedang menulis data ke fail dan kemudian aliran itu dimusnahkan. Oleh itu, mereka memutuskan bahawa adalah lebih logik untuk tidak membunuh aliran, tetapi memaklumkannya bahawa ia harus diganggu. Bagaimana untuk bertindak balas terhadap perkara ini terpulang kepada aliran itu sendiri. Butiran lanjut boleh didapati dalam Oracle " Mengapa Thread.stop ditamatkan? " Mari lihat contoh:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
Dalam contoh ini, kami tidak akan menunggu 60 saat, tetapi akan segera mencetak 'Terganggu'. Ini kerana kami memanggil kaedah benang interrupt
. Kaedah ini menetapkan "bendera dalaman dipanggil status gangguan". Iaitu, setiap utas mempunyai bendera dalaman yang tidak boleh diakses secara langsung. Tetapi kami mempunyai kaedah asli untuk berinteraksi dengan bendera ini. Tetapi ini bukan satu-satunya cara. Benang boleh dalam proses pelaksanaan, tidak menunggu sesuatu, tetapi hanya melakukan tindakan. Tetapi ia boleh memberikan bahawa mereka akan mahu menyelesaikannya pada titik tertentu dalam kerjanya. Sebagai contoh:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
//Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
Dalam contoh di atas, anda dapat melihat bahawa gelung while
akan berjalan sehingga benang terganggu secara luaran. Perkara penting yang perlu diketahui tentang bendera isInterrupted ialah jika kita menangkapnya InterruptedException
, bendera itu isInterrupted
ditetapkan semula, dan kemudian isInterrupted
ia akan kembali palsu. Terdapat juga kaedah statik dalam kelas Thread yang hanya digunakan pada thread semasa - Thread.interrupted() , tetapi kaedah ini menetapkan semula bendera kepada false! Anda boleh membaca lebih lanjut dalam bab " Gangguan Benang ".
Sertai — Menunggu urutan lain selesai
Jenis menunggu yang paling mudah ialah menunggu urutan lain selesai.public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
Dalam contoh ini, urutan baharu akan tidur selama 5 saat. Pada masa yang sama, benang utama akan menunggu sehingga benang tidur bangun dan menyelesaikan kerjanya. Jika anda melihat melalui JVisualVM, keadaan benang akan kelihatan seperti ini: Terima kasih kepada alat pemantauan, anda boleh melihat apa yang berlaku dengan benang. Kaedah ini join
agak mudah, kerana ia hanyalah kaedah dengan kod java yang dilaksanakan wait
semasa benang yang dipanggil masih hidup. Sebaik sahaja benang mati (semasa penamatan), penantian ditamatkan. Itulah keseluruhan keajaiban kaedah join
. Oleh itu, mari kita beralih ke bahagian yang paling menarik.
Pemantau Konsep
Dalam multithreading terdapat perkara seperti Monitor. Secara umum, perkataan monitor diterjemahkan daripada bahasa Latin sebagai "pengawas" atau "pengawas." Dalam rangka artikel ini, kami akan cuba mengingati intipatinya, dan bagi mereka yang mahu, saya meminta anda menyelami bahan dari pautan untuk butirannya. Mari kita mulakan perjalanan kita dengan spesifikasi bahasa Java, iaitu dengan JLS: " 17.1. Penyegerakan ". Ia mengatakan perkara berikut: Ternyata untuk tujuan penyegerakan antara benang, Java menggunakan mekanisme tertentu yang dipanggil "Monitor". Setiap objek mempunyai monitor yang dikaitkan dengannya, dan benang boleh menguncinya atau membuka kuncinya. Seterusnya, kami akan menemui tutorial latihan di laman web Oracle: " Kunci Intrinsik dan Penyegerakan ". Tutorial ini menerangkan bahawa penyegerakan dalam Java dibina di sekeliling entiti dalaman yang dikenali sebagai kunci intrinsik atau kunci monitor. Selalunya kunci sedemikian hanya dipanggil "monitor". Kami juga melihat sekali lagi bahawa setiap objek di Jawa mempunyai kunci intrinsik yang dikaitkan dengannya. Anda boleh membaca " Java - Kunci Intrinsik dan Penyegerakan ". Seterusnya, adalah penting untuk memahami bagaimana objek dalam Java boleh dikaitkan dengan monitor. Setiap objek dalam Java mempunyai pengepala - sejenis metadata dalaman yang tidak tersedia kepada pengaturcara daripada kod, tetapi mesin maya perlu berfungsi dengan objek dengan betul. Pengepala objek termasuk MarkWord yang kelihatan seperti ini:https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
Jadi, menggunakan kata kunci, synchronized
utas semasa (di mana baris kod ini dilaksanakan) cuba menggunakan monitor yang dikaitkan dengan objek object
dan "mendapatkan kunci" atau "menangkap monitor" (pilihan kedua adalah lebih baik). Jika tiada pertikaian untuk monitor (iaitu tiada orang lain yang mahu menyegerakkan pada objek yang sama), Java boleh cuba melakukan pengoptimuman yang dipanggil "penguncian berat sebelah". Tajuk objek dalam Mark Word akan mengandungi teg yang sepadan dan rekod benang mana monitor dilampirkan. Ini mengurangkan overhed apabila menangkap monitor. Sekiranya monitor telah diikat ke benang lain sebelum ini, maka penguncian ini tidak mencukupi. JVM bertukar kepada jenis penguncian seterusnya - penguncian asas. Ia menggunakan operasi bandingkan-dan-tukar (CAS). Pada masa yang sama, pengepala dalam Mark Word tidak lagi menyimpan Mark Word itu sendiri, tetapi pautan ke storannya + teg ditukar supaya JVM memahami bahawa kami menggunakan penguncian asas. Jika terdapat pertikaian untuk monitor beberapa utas (satu telah menangkap monitor, dan yang kedua sedang menunggu monitor dikeluarkan), maka teg dalam Mark Word berubah, dan Mark Word mula menyimpan rujukan kepada monitor sebagai objek - beberapa entiti dalaman JVM. Seperti yang dinyatakan dalam JEP, dalam kes ini, ruang diperlukan dalam kawasan memori Native Heap untuk menyimpan entiti ini. Pautan ke lokasi storan entiti dalaman ini akan terletak dalam objek Mark Word. Oleh itu, seperti yang kita lihat, monitor adalah benar-benar mekanisme untuk memastikan penyegerakan akses berbilang benang kepada sumber yang dikongsi. Terdapat beberapa pelaksanaan mekanisme ini yang JVM bertukar antara. Oleh itu, untuk kesederhanaan, apabila bercakap tentang monitor, kita sebenarnya bercakap tentang kunci.
Disegerakkan dan menunggu dengan kunci
Konsep monitor, seperti yang kita lihat sebelum ini, berkait rapat dengan konsep "blok penyegerakan" (atau, kerana ia juga dipanggil, bahagian kritikal). Mari lihat contoh:public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized (lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized (lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
Di sini, utas utama mula-mula menghantar tugas ke utas baru, dan kemudian segera "menangkap" kunci dan melakukan operasi yang panjang dengannya (8 saat). Selama ini, tugas itu tidak boleh memasuki blok untuk pelaksanaannya synchronized
, kerana kunci sudah diduduki. Jika benang tidak dapat memperoleh kunci, ia akan menunggu di monitor untuknya. Sebaik sahaja ia menerimanya, ia akan meneruskan pelaksanaan. Apabila benang meninggalkan monitor, ia melepaskan kunci. Dalam JVisualVM ia akan kelihatan seperti ini: Seperti yang anda lihat, status dalam JVisualVM dipanggil "Monitor" kerana benang disekat dan tidak boleh menduduki monitor. Anda juga boleh mengetahui keadaan benang dalam kod, tetapi nama keadaan ini tidak bertepatan dengan istilah JVisualVM, walaupun ia serupa. Dalam kes ini, th1.getState()
gelung for
akan kembali BLOCKED , kerana Semasa gelung berjalan, monitor lock
diduduki main
oleh benang, dan benang th1
disekat dan tidak boleh terus berfungsi sehingga kunci dikembalikan. Sebagai tambahan kepada blok penyegerakan, keseluruhan kaedah boleh disegerakkan. Sebagai contoh, kaedah dari kelas HashTable
:
public synchronized int size() {
return count;
}
Dalam satu unit masa, kaedah ini akan dilaksanakan oleh hanya satu utas. Tetapi kita memerlukan kunci, bukan? Ya saya perlukannya. Dalam kes kaedah objek, kunci akan this
. Terdapat perbincangan menarik mengenai topik ini: " Adakah terdapat kelebihan untuk menggunakan Kaedah Disegerakkan dan bukannya Blok Disegerakkan? ". Jika kaedah statik, maka kunci tidak akan menjadi this
(kerana untuk kaedah statik ia tidak boleh this
), tetapi objek kelas (Sebagai contoh, Integer.class
).
Tunggu dan tunggu di monitor. Kaedah memberitahu dan memberitahuSemua
Thread mempunyai kaedah tunggu lain, yang disambungkan ke monitor. Tidak sepertisleep
dan join
, ia tidak boleh dipanggil begitu sahaja. Dan namanya ialah wait
. Kaedah ini dilaksanakan wait
pada objek pada monitor yang kita mahu tunggu. Mari lihat contoh:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// task будет ждать, пока его не оповестят через lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// После оповещения нас мы будем ждать, пока сможем взять лок
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// Ждём и после этого забираем себе лок, оповещаем и отдаём лок
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
Dalam JVisualVM ia akan kelihatan seperti ini: Untuk memahami cara ini berfungsi, anda harus ingat bahawa kaedah wait
merujuk notify
kepada java.lang.Object
. Nampaknya pelik bahawa kaedah berkaitan benang berada dalam Object
. Tetapi di sinilah jawapannya. Seperti yang kita ingat, setiap objek dalam Java mempunyai pengepala. Pengepala mengandungi pelbagai maklumat perkhidmatan, termasuk maklumat tentang monitor—data tentang keadaan penguncian. Dan seperti yang kita ingat, setiap objek (iaitu setiap contoh) mempunyai perkaitan dengan entiti JVM dalaman yang dipanggil kunci intrinsik, yang juga dipanggil monitor. Dalam contoh di atas, tugas itu menerangkan bahawa kita memasuki blok penyegerakan pada monitor yang dikaitkan dengan lock
. Jika mungkin untuk mendapatkan kunci pada monitor ini, maka wait
. Benang yang melaksanakan tugas ini akan melepaskan monitor lock
, tetapi akan menyertai baris gilir benang yang menunggu pemberitahuan pada monitor lock
. Barisan urutan ini dipanggil WAIT-SET, yang lebih tepat mencerminkan intipati. Ia lebih kepada satu set daripada barisan. Benang itu main
mencipta benang baharu dengan tugasan tugasan, memulakannya dan menunggu selama 3 saat. Ini membolehkan, dengan tahap kebarangkalian yang tinggi, benang baharu untuk mengambil kunci sebelum benang main
dan beratur pada monitor. Selepas itu benang itu main
sendiri memasuki blok penyegerakan lock
dan melakukan pemberitahuan benang pada monitor. Selepas pemberitahuan dihantar, utas main
melepaskan monitor lock
, dan utas baharu (yang sebelum ini menunggu) lock
terus dilaksanakan selepas menunggu monitor dikeluarkan. Adalah mungkin untuk menghantar pemberitahuan kepada hanya satu daripada utas ( notify
) atau kepada semua utas dalam baris gilir sekali gus ( notifyAll
). Anda boleh membaca lebih lanjut dalam " Perbezaan antara notify() dan notifyAll() dalam Java ". Adalah penting untuk ambil perhatian bahawa susunan pemberitahuan bergantung pada pelaksanaan JVM. Anda boleh membaca lebih lanjut dalam " Bagaimana untuk menyelesaikan kebuluran dengan memberitahu dan memberitahu semua? ". Penyegerakan boleh dilakukan tanpa menentukan objek. Ini boleh dilakukan apabila bukan bahagian kod yang berasingan disegerakkan, tetapi keseluruhan kaedah. Sebagai contoh, untuk kaedah statik kunci akan menjadi objek kelas (diperolehi melalui .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
Dari segi penggunaan kunci, kedua-dua kaedah adalah sama. Jika kaedah tersebut tidak statik, maka penyegerakan akan dilakukan mengikut arus instance
, iaitu mengikut this
. Ngomong-ngomong, sebelum ini kami mengatakan bahawa menggunakan kaedah itu getState
anda boleh mendapatkan status benang. Oleh itu, berikut adalah urutan yang beratur oleh monitor, statusnya akan MENUNGGU atau TIMED_WAITING jika kaedah wait
menetapkan had masa menunggu.
Kitaran hayat benang
Seperti yang kita lihat, aliran berubah statusnya dalam perjalanan kehidupan. Pada dasarnya, perubahan ini adalah kitaran hayat benang. Apabila benang baru dibuat, ia mempunyai status BARU. Dalam kedudukan ini, ia belum bermula dan Java Thread Scheduler belum mengetahui apa-apa tentang thread baru. Untuk penjadual benang mengetahui tentang benang, anda mesti memanggilthread.start()
. Kemudian benang akan masuk ke keadaan RUNNABLE. Terdapat banyak skim yang tidak betul di Internet di mana keadaan Runnable dan Running dipisahkan. Tetapi ini adalah kesilapan, kerana... Java tidak membezakan antara status "sedia untuk dijalankan" dan "berjalan". Apabila benang hidup tetapi tidak aktif (tidak Boleh Dijalankan), ia berada dalam salah satu daripada dua keadaan:
- DISEKAT - menunggu kemasukan ke bahagian yang dilindungi, i.e. ke
synchonized
blok. - MENUNGGU - menunggu thread lain berdasarkan syarat. Jika keadaan itu benar, penjadual benang memulakan benang.
getState
. Benang juga mempunyai kaedah isAlive
yang mengembalikan benar jika utas tidak Ditamatkan.
LockSokongan dan letak benang
Sejak Java 1.6 terdapat mekanisme menarik yang dipanggil LockSupport . Kelas ini mengaitkan "permit" atau kebenaran dengan setiap urutan yang menggunakannya. Panggilan kaedahpark
kembali serta-merta jika permit tersedia, menduduki permit yang sama semasa panggilan. Jika tidak ia disekat. Memanggil kaedah unpark
membuat permit tersedia jika ia belum tersedia. Hanya terdapat 1 Permit. Dalam Java API , LockSupport
. Semaphore
Mari lihat contoh mudah:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Просим разрешение и ждём, пока не получим его
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
Kod ini akan menunggu selama-lamanya kerana semafor kini mempunyai 0 permit. Dan apabila dipanggil dalam kod acquire
(iaitu, minta kebenaran), utas menunggu sehingga ia menerima kebenaran. Memandangkan kami sedang menunggu, kami diwajibkan untuk memprosesnya InterruptedException
. Menariknya, semaphore melaksanakan keadaan benang yang berasingan. Jika kita melihat dalam JVisualVM, kita akan melihat bahawa negeri kita bukanlah Tunggu, tetapi Park. Mari lihat contoh lain:
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
//Запаркуем текущий поток
System.err.println("Will be Parked");
LockSupport.park();
// Как только нас распаркуют - начнём действовать
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
Status urutan akan MENUNGGU, tetapi JVisualVM membezakan wait
antara dari synchronized
dan park
dari LockSupport
. Mengapa yang ini sangat penting LockSupport
? Mari beralih lagi ke Java API dan lihat Thread State WAITING . Seperti yang anda lihat, terdapat hanya tiga cara untuk masuk ke dalamnya. 2 cara - ini wait
dan join
. Dan yang ketiga ialah LockSupport
. Kunci dalam Java dibina berdasarkan prinsip yang sama LockSupport
dan mewakili alat peringkat lebih tinggi. Jom cuba guna satu. Mari lihat, sebagai contoh, di ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
Seperti dalam contoh sebelumnya, semuanya mudah di sini. lock
menunggu seseorang mengeluarkan sumber. Jika kita melihat dalam JVisualVM, kita akan melihat bahawa utas baharu akan diletak sehingga main
utas itu menguncinya. Anda boleh membaca lebih lanjut tentang kunci di sini: " Pengaturcaraan berbilang benang dalam Java 8. Bahagian dua. Menyegerakkan akses kepada objek boleh ubah " dan " Java Lock API. Teori dan contoh penggunaan ." Untuk lebih memahami pelaksanaan kunci, adalah berguna untuk membaca tentang Phazer dalam gambaran keseluruhan " Kelas Phaser ". Dan bercakap tentang pelbagai penyegerak, anda mesti membaca artikel tentang Habré “ Java.util.concurrent.* Synchronizers Reference ”.
Jumlah
Dalam ulasan ini, kami melihat cara utama benang berinteraksi di Jawa. Bahan tambahan:- Monitor – Idea Asas Penyegerakan Java
- Rujukan penyegerak java.util.concurrent.*
- Jawapan kepada soalan tentang multithreading pada temu bual
GO TO FULL VERSION