Memprogram benang Java di dunia nyata, Bahagian 1

Semua program Java selain aplikasi berasaskan konsol sederhana adalah multithreaded, sama ada anda suka atau tidak. Masalahnya adalah bahawa Abstract Windowing Toolkit (AWT) memproses peristiwa sistem operasi (OS) pada utasnya sendiri, jadi kaedah pendengar anda benar-benar dijalankan pada thread AWT. Kaedah pendengar yang sama biasanya mengakses objek yang juga diakses dari utas utama. Mungkin menggoda, pada saat ini, untuk menguburkan kepala anda di pasir dan berpura-pura anda tidak perlu bimbang tentang masalah threading, tetapi biasanya anda tidak dapat melepaskannya. Dan, sayangnya, hampir tidak ada buku di Java yang membahas masalah utas dengan cukup mendalam. (Untuk senarai buku bermanfaat mengenai topik ini, lihat Sumber.)

Artikel ini adalah yang pertama dalam siri yang akan memberikan penyelesaian dunia nyata untuk masalah pemrograman Java dalam lingkungan multithreaded. Ini ditujukan untuk pengaturcara Java yang memahami hal-hal peringkat bahasa ( synchronizedkata kunci dan pelbagai kemudahan Threadkelas), tetapi ingin belajar bagaimana menggunakan ciri bahasa ini dengan berkesan.

Pergantungan platform

Sayangnya, janji Java mengenai kemerdekaan platform jatuh di wajahnya di arena utas. Walaupun mungkin untuk menulis program Java multithreaded yang tidak bergantung pada platform, anda harus melakukannya dengan mata terbuka. Ini sebenarnya bukan kesalahan Java; hampir mustahil untuk menulis sistem threading yang bebas platform. (Rangka kerja Doug Schmidt's ACE [Adaptive Communication Environment] adalah usaha yang baik, walaupun rumit. Lihat Sumber untuk pautan ke programnya.) Jadi, sebelum saya dapat membincangkan masalah pengaturcaraan Java inti keras dalam ansuran berikutnya, saya harus bincangkan kesukaran yang diperkenalkan oleh platform di mana mesin maya Java (JVM) mungkin berjalan.

Tenaga atom

Konsep tahap OS pertama yang penting untuk difahami adalah atomisme. Operasi atom tidak dapat diganggu oleh utas lain. Java memang menentukan sekurang-kurangnya beberapa operasi atom. Khususnya, pemberian kepada pemboleh ubah dari jenis apa pun kecuali longatau doublebersifat atom. Anda tidak perlu bimbang tentang utas memilih kaedah di tengah tugas. Dalam praktiknya, ini bermaksud bahawa anda tidak perlu menyegerakkan kaedah yang tidak lain selain mengembalikan nilai (atau memberikan nilai ke) pemboleh ubah booleanatau intcontoh. Begitu juga, kaedah yang melakukan banyak pengiraan dengan hanya menggunakan pemboleh ubah dan argumen tempatan, dan yang memberikan hasil pengiraan tersebut kepada pemboleh ubah contoh sebagai perkara terakhir yang dilakukannya, tidak perlu diselaraskan. Sebagai contoh:

kelas some_class {int some_field; void f (some_class arg) // sengaja tidak diselaraskan {// Lakukan banyak perkara di sini yang menggunakan pemboleh ubah tempatan // dan argumen kaedah, tetapi tidak mengakses // mana-mana bidang kelas (atau memanggil kaedah apa pun // yang mengakses mana-mana bidang kelas). // ... some_field = nilai_ baru; // buat ini terakhir. }}

Sebaliknya, semasa melaksanakan x=++yatau x+=y, anda mungkin akan diprediksi setelah kenaikan tetapi sebelum penugasan. Untuk mendapatkan keberanian dalam situasi ini, anda perlu menggunakan kata kunci synchronized.

Semua ini penting kerana overhead sinkronisasi boleh menjadi tidak biasa, dan boleh berbeza dari OS ke OS. Program berikut menunjukkan masalahnya. Setiap gelung berulang kali memanggil kaedah yang melakukan operasi yang sama, tetapi salah satu kaedah ( locking()) diselaraskan dan yang lain ( not_locking()) tidak. Menggunakan VM "prestasi-paket" JDK yang berjalan di bawah Windows NT 4, program ini melaporkan perbezaan 1.2 saat dalam jangka masa antara kedua gelung, atau kira-kira 1.2 mikrodetik setiap panggilan. Perbezaan ini mungkin tidak banyak, tetapi ia menunjukkan peningkatan 7.25 peratus dalam waktu panggilan. Sudah tentu, kenaikan peratusan menurun kerana kaedah ini lebih banyak berfungsi, tetapi sebilangan besar kaedah - sekurang-kurangnya dalam program saya - hanya beberapa baris kod.

import java.util. *; class synch { int synchronized int locking (int a, int b) {return a + b;} int not_locking (int a, int b) {return a + b;} persendirian akhir statik int ITERASI = 1000000; main public void statik (String [] args) {synch tester = synch baru (); permulaan berganda = Tarikh baru (). getTime (); untuk (panjang i = ITERASI; --i> = 0;) tester.locking (0,0); double end = Tarikh baru (). getTime (); double locking_time = end - mula; bermula = Tarikh baru (). getTime (); untuk (long i = ITERATIONS; --i> = 0;) tester.not_locking (0,0);tamat = Tarikh baru (). getTime (); double not_locking_time = end - mula; double time_in_synchronization = locking_time - not_locking_time; System.out.println ("Masa hilang untuk penyegerakan (milis.):" + Time_in_synchronization); System.out.println ("Mengunci overhead setiap panggilan:" + (time_in_synchronization / ITERATIONS)); System.out.println (not_locking_time / locking_time * 100.0 + "% kenaikan"); }}

Walaupun HotSpot VM seharusnya mengatasi masalah sinkronisasi-overhead, HotSpot bukan percuma - anda harus membelinya. Kecuali anda melesenkan dan menghantar HotSpot dengan aplikasi anda, tidak ada yang tahu apa VM akan berada di platform sasaran, dan tentu saja anda mahu sekecil mungkin kelajuan pelaksanaan program anda bergantung pada VM yang melaksanakannya. Walaupun masalah kebuntuan (yang akan saya bincangkan dalam ansuran siri ini) tidak wujud, tanggapan bahawa anda harus "menyegerakkan segalanya" hanyalah salah.

Serentak berbanding paralelisme

Isu berkaitan OS-next (dan masalah utama ketika datang untuk menulis Java bebas platform) mempunyai kaitan dengan tanggapan keserentakan dan keselarian. Sistem multithreading serentak memberikan kemunculan beberapa tugas yang dijalankan sekaligus, tetapi tugas-tugas ini sebenarnya dibahagikan kepada potongan yang berkongsi pemproses dengan potongan dari tugas lain. Gambar berikut menggambarkan permasalahannya. Dalam sistem selari, dua tugas sebenarnya dilakukan secara serentak. Parallelism memerlukan sistem multi-CPU.

Melainkan jika anda menghabiskan banyak masa yang disekat, menunggu operasi I / O selesai, program yang menggunakan banyak utas serentak akan berjalan lebih perlahan daripada program utas tunggal yang sama, walaupun selalunya akan lebih teratur daripada single yang setara -kain versi. Program yang menggunakan pelbagai utas berjalan selari pada beberapa pemproses akan berjalan lebih pantas.

Walaupun Java membenarkan utas diimplementasikan sepenuhnya dalam VM, setidaknya secara teori, pendekatan ini akan menghalangi adanya paralelisme dalam aplikasi anda. Sekiranya tidak ada utas peringkat sistem operasi yang digunakan, OS akan melihat contoh VM sebagai aplikasi utas tunggal, yang kemungkinan besar akan dijadwalkan ke pemproses tunggal. Hasilnya adalah bahawa tidak ada dua utas Java yang berjalan di bawah instance VM yang sama yang akan berjalan selari, walaupun anda mempunyai banyak CPU dan VM anda adalah satu-satunya proses aktif. Dua contoh VM yang menjalankan aplikasi berasingan boleh berjalan secara selari, tentu saja, tetapi saya ingin melakukan lebih baik daripada itu. Untuk mendapatkan paralelisme, VM mestimemetakan utas Java hingga ke utas OS; jadi, anda tidak mampu mengabaikan perbezaan antara pelbagai model threading sekiranya kebebasan platform penting.

Luruskan keutamaan anda

Saya akan menunjukkan bagaimana masalah yang baru saya bincangkan dapat mempengaruhi program anda dengan membandingkan dua sistem operasi: Solaris dan Windows NT.

Java, secara teori sekurang-kurangnya, menyediakan sepuluh tahap keutamaan untuk utas. (Sekiranya dua atau lebih utas menunggu untuk berjalan, satu dengan tahap keutamaan tertinggi akan dilaksanakan.) Di Solaris, yang menyokong 231 tahap keutamaan, ini tidak menjadi masalah (walaupun keutamaan Solaris sukar digunakan - lebih banyak lagi mengenai ini sebentar). NT, di sisi lain, memiliki tujuh tingkat keutamaan yang tersedia, dan ini harus dipetakan menjadi sepuluh Java. Pemetaan ini belum ditentukan, jadi banyak kemungkinan ada pada diri mereka. (Contohnya, tahap keutamaan Java 1 dan 2 mungkin memetakan ke tahap keutamaan NT 1, dan tahap keutamaan Java 8, 9, dan 10 mungkin semuanya memetakan ke tingkat NT 7.)

Kekurangan tahap keutamaan NT adalah masalah jika anda ingin menggunakan keutamaan untuk mengawal penjadualan. Perkara dibuat lebih rumit oleh fakta bahawa tahap keutamaan tidak tetap. NT menyediakan mekanisme yang disebut prioritas meningkatkan, yang dapat Anda matikan dengan panggilan sistem C, tetapi bukan dari Java. Apabila peningkatan prioriti diaktifkan, NT meningkatkan keutamaan utas dengan jumlah yang tidak ditentukan untuk jumlah waktu yang tidak ditentukan setiap kali melaksanakan panggilan sistem yang berkaitan dengan I / O. Dalam praktiknya, ini bermaksud bahawa tahap keutamaan utas mungkin lebih tinggi daripada yang anda fikirkan kerana benang itu kebetulan melakukan operasi I / O pada waktu yang canggung.

Inti penekanan keutamaan adalah untuk mengelakkan utas yang melakukan pemprosesan latar belakang mempengaruhi tindak balas yang jelas dari tugas berat UI. Sistem operasi lain mempunyai algoritma yang lebih canggih yang biasanya menurunkan keutamaan proses latar belakang. Kelemahan skema ini, terutama ketika dilaksanakan pada per-utas dan bukan pada tahap setiap proses, adalah bahawa sangat sukar untuk menggunakan keutamaan untuk menentukan kapan utas tertentu akan dijalankan.

Ia menjadi lebih teruk.

Di Solaris, seperti yang berlaku di semua sistem Unix, proses mempunyai keutamaan dan juga utas. Urutan proses keutamaan tinggi tidak dapat diganggu oleh utas proses keutamaan rendah. Lebih-lebih lagi, tahap keutamaan proses tertentu dapat dibatasi oleh pentadbir sistem sehingga proses pengguna tidak akan mengganggu proses OS kritikal. NT menyokong semua ini. Proses NT hanyalah ruang alamat. Tidak mempunyai keutamaan per se, dan tidak dijadualkan. Sistem menjadualkan utas; kemudian, jika utas tertentu berjalan di bawah proses yang tidak ada dalam ingatan, prosesnya ditukar. Keutamaan utas NT tergolong dalam pelbagai "kelas keutamaan," yang diedarkan di seluruh rangkaian keutamaan sebenar. Sistem ini kelihatan seperti ini:

The columns are actual priority levels, only 22 of which must be shared by all applications. (The others are used by NT itself.) The rows are priority classes. The threads running in a process pegged at the idle priority class are running at levels 1 through 6 and 15, depending on their assigned logical priority level. The threads of a process pegged as normal priority class will run at levels 1, 6 through 10, or 15 if the process doesn't have the input focus. If it does have the input focus, the threads run at levels 1, 7 through 11, or 15. This means that a high-priority thread of an idle priority class process can preempt a low-priority thread of a normal priority class process, but only if that process is running in the background. Notice that a process running in the "high" priority class only has six priority levels available to it. The other classes have seven.

NT provides no way to limit the priority class of a process. Any thread on any process on the machine can take over control of the box at any time by boosting its own priority class; there is no defense against this.

The technical term I use to describe NT's priority is unholy mess. In practice, priority is virtually worthless under NT.

So what's a programmer to do? Between NT's limited number of priority levels and it's uncontrollable priority boosting, there's no absolutely safe way for a Java program to use priority levels for scheduling. One workable compromise is to restrict yourself to Thread.MAX_PRIORITY, Thread.MIN_PRIORITY, and Thread.NORM_PRIORITY when you call setPriority(). This restriction at least avoids the 10-levels-mapped-to-7-levels problem. I suppose you could use the os.name system property to detect NT, and then call a native method to turn off priority boosting, but that won't work if your app is running under Internet Explorer unless you also use Sun's VM plug-in. (Microsoft's VM uses a nonstandard native-method implementation.) In any event, I hate to use native methods. I usually avoid the problem as much as possible by putting most threads at NORM_PRIORITY and using scheduling mechanisms other than priority. (I'll discuss some of these in future installments of this series.)

Cooperate!

There are typically two threading models supported by operating systems: cooperative and preemptive.

The cooperative multithreading model

Dalam sistem koperasi , utas mengekalkan kawalan pemprosesnya sehingga memutuskan untuk melepaskannya (yang mungkin tidak pernah). Berbagai utas harus bekerjasama antara satu sama lain atau semua tetapi salah satu utas akan "kelaparan" (bermaksud, tidak pernah diberi peluang untuk berlari). Penjadualan di kebanyakan sistem koperasi dilakukan dengan ketat mengikut tahap keutamaan. Apabila utas semasa melepaskan kawalan, utas menunggu dengan keutamaan tertinggi dapat dikawal. (Pengecualian untuk peraturan ini adalah Windows 3.x, yang menggunakan model koperasi tetapi tidak memiliki banyak penjadwal. Tingkap yang mempunyai fokus dapat dikendalikan.)