Java 101: Kesesuaian Java tanpa rasa sakit, Bahagian 1

Dengan bertambahnya kerumitan aplikasi serentak, banyak pembangun mendapati bahawa kemampuan threading tahap rendah Java tidak mencukupi untuk keperluan pengaturcaraan mereka. Sekiranya demikian, mungkin sudah waktunya untuk menemui Java Concurrency Utilities. Mulakan dengan java.util.concurrent, dengan pengenalan terperinci Jeff Friesen ke rangka Executor, jenis penyegerakan, dan pakej Java Concurrent Collections.

Java 101: Generasi seterusnya

Artikel pertama dalam siri JavaWorld baru ini memperkenalkan Java Date and Time API .

Platform Java menyediakan keupayaan threading peringkat rendah yang membolehkan pembangun menulis aplikasi serentak di mana thread yang berbeza dijalankan secara serentak. Benang Java standard mempunyai beberapa kelemahan, namun:

  • Primitif peringkat rendah keserentakan Jawa ( synchronized, volatile, wait(), notify(), dan notifyAll()) tidak mudah untuk digunakan dengan betul. Bahaya threading seperti kebuntuan, kelaparan benang, dan keadaan perlumbaan, yang disebabkan oleh penggunaan primitif yang salah, juga sukar untuk dikesan dan disahpepijat.
  • Mengandalkan synchronizedkoordinat akses antara utas membawa kepada masalah prestasi yang mempengaruhi skalabiliti aplikasi, syarat untuk banyak aplikasi moden.
  • Keupayaan asas threading Java terlalu rendah. Pembangun selalunya memerlukan konstruksi peringkat lebih tinggi seperti semafor dan kumpulan benang, yang tidak ditawarkan oleh keupayaan utas peringkat rendah Java. Hasilnya, pembangun akan membina konstruk mereka sendiri, yang memakan masa dan ralat.

Rangka kerja JSR 166: Concurrency Utilities dirancang untuk memenuhi keperluan kemudahan threading peringkat tinggi. Dimulai pada awal tahun 2002, kerangka ini diformalkan dan dilaksanakan dua tahun kemudian di Java 5. Peningkatan telah dilakukan di Java 6, Java 7, dan Java 8 yang akan datang.

Java 101 dua bahagian ini : Siri generasi seterusnya memperkenalkan pembangun perisian yang biasa dengan asas Java threading ke pakej dan rangka kerja Java Concurrency Utilities. Pada Bahagian 1, saya menyajikan gambaran keseluruhan kerangka Java Concurrency Utilities dan memperkenalkan kerangka Executornya, utiliti sinkronisasi, dan paket Java Concurrent Collections.

Memahami benang Java

Sebelum menyelami siri ini, pastikan anda mengetahui asas-asas utas. Mulakan dengan pengenalan Java 101 untuk keupayaan threading peringkat rendah Java:

  • Bahagian 1: Memperkenalkan benang dan runnables
  • Bahagian 2: Penyegerakan benang
  • Bahagian 3: Penjadualan benang, tunggu / maklumkan, dan gangguan urutan
  • Bahagian 4: Kumpulan benang, turun naik, pemboleh ubah benang-tempatan, pemasa, dan kematian benang

Di dalam Utiliti Java Concurrency

Kerangka Java Concurrency Utilities adalah perpustakaan jenis yang dirancang untuk digunakan sebagai blok bangunan untuk membuat kelas atau aplikasi bersamaan. Jenis-jenis ini selamat untuk benang, telah diuji secara menyeluruh, dan menawarkan prestasi tinggi.

Jenis-jenis dalam Java Concurrency Utilities disusun dalam kerangka kecil; iaitu, rangka pelaksana, penyegerakan, koleksi serentak, kunci, pemboleh ubah atom, dan Fork / Join. Mereka disusun lebih lanjut ke dalam pakej utama dan sepasang paket kecil:

  • java.util.concurrent mengandungi jenis utiliti peringkat tinggi yang biasanya digunakan dalam pengaturcaraan serentak. Contohnya termasuk semaphores, halangan, kumpulan benang, dan hashmap serentak.
    • The java.util.concurrent.atomic subpakej mengandungi kelas utiliti peringkat rendah yang sokongan kunci bebas thread selamat pengaturcaraan pada pembolehubah tunggal.
    • The java.util.concurrent.locks subpakej mengandungi jenis utiliti peringkat rendah untuk mengunci dan menunggu keadaan, yang berbeza daripada menggunakan penyegerakan peringkat rendah dan monitor Java.

Rangka kerja Java Concurrency Utilities juga memperlihatkan arahan perkakasan perbandingan dan pertukaran (CAS) tahap rendah , varian yang biasanya disokong oleh pemproses moden. CAS jauh lebih ringan daripada mekanisme penyegerakan berbasis monitor Java dan digunakan untuk menerapkan beberapa kelas serentak yang sangat berskala. java.util.concurrent.locks.ReentrantLockKelas berdasarkan CAS , misalnya, lebih berprestasi daripada synchronizedprimitif berasaskan monitor yang setara . ReentrantLockmenawarkan lebih banyak kawalan terhadap penguncian. (Di Bahagian 2 saya akan menerangkan lebih lanjut mengenai bagaimana CAS berfungsi java.util.concurrent.)

System.nanoTime ()

Rangka kerja Java Concurrency Utilities merangkumi long nanoTime(), yang merupakan ahli java.lang.Systemkelas. Kaedah ini membolehkan akses ke sumber masa nanodetik-butiran untuk membuat pengukuran masa relatif.

Di bahagian seterusnya saya akan memperkenalkan tiga ciri berguna Java Concurrency Utilities, pertama menerangkan mengapa mereka sangat penting untuk kesesuaian moden dan kemudian menunjukkan bagaimana ia berfungsi untuk meningkatkan kelajuan, kebolehpercayaan, kecekapan, dan skalabilitas aplikasi Java serentak.

Kerangka Pelaksana

Dalam utas, tugas adalah unit kerja. Satu masalah dengan threading tingkat rendah di Java adalah bahwa pengiriman tugas digabungkan erat dengan kebijakan pelaksanaan tugas, seperti yang ditunjukkan oleh Penyenaraian 1.

Penyenaraian 1. Server.java (Versi 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

Kod di atas menerangkan aplikasi pelayan mudah (dengan doWork(Socket)kiri kosong untuk ringkas). Benang pelayan berulang kali memanggil socket.accept()untuk menunggu permintaan masuk, dan kemudian memulakan utas untuk melayani permintaan ini ketika tiba.

Kerana aplikasi ini membuat utas baru untuk setiap permintaan, aplikasi ini tidak sesuai dengan skala ketika menghadapi sejumlah besar permintaan. Sebagai contoh, setiap utas yang dibuat memerlukan memori, dan terlalu banyak utas mungkin menghabiskan memori yang ada, sehingga aplikasi berhenti.

Anda boleh menyelesaikan masalah ini dengan mengubah dasar pelaksanaan tugas. Daripada selalu membuat utas baru, anda dapat menggunakan kumpulan utas, di mana sejumlah utas akan melayani tugas yang masuk. Anda harus menulis semula aplikasi untuk membuat perubahan ini.

java.util.concurrentmerangkumi rangka Pelaksana, kerangka kecil jenis yang memisahkan penyerahan tugas dari dasar pelaksanaan tugas. Dengan menggunakan rangka kerja Pelaksana, adalah mungkin untuk menyesuaikan dasar pelaksanaan tugas program dengan mudah tanpa perlu menulis semula kod anda dengan ketara.

Di dalam kerangka Pelaksana

Kerangka Pelaksana didasarkan pada Executorantara muka, yang menggambarkan pelaksana sebagai objek apa pun yang mampu melaksanakan java.lang.Runnabletugas. Antaramuka ini menyatakan kaedah soliter berikut untuk melaksanakan Runnabletugas:

void execute(Runnable command)

Anda menyerahkan Runnabletugas dengan menyerahkannya execute(Runnable). Sekiranya pelaksana tidak dapat menjalankan tugas dengan alasan apa pun (misalnya, jika pelaksana telah dimatikan), kaedah ini akan membuang a RejectedExecutionException.

Konsep utamanya ialah penyerahan tugas dipisahkan dari kebijakan pelaksanaan tugas , yang dijelaskan oleh Executorpelaksanaan. The runnable tugas juga dapat melaksanakan melalui thread baru, thread terkumpul, benang memanggil, dan sebagainya.

Perhatikan bahawa Executorsangat terhad. Sebagai contoh, anda tidak boleh mematikan pelaksana atau menentukan sama ada tugas tidak segerak telah selesai. Anda juga tidak dapat membatalkan tugas yang sedang berjalan. Atas sebab-sebab ini dan lain-lain, kerangka Executor menyediakan antara muka ExecutorService, yang meluas Executor.

Lima ExecutorServicekaedah yang sangat penting:

  • boolean awaitTermination (timeout yang panjang, unit TimeUnit) menyekat thread panggilan sehingga semua tugas selesai dilaksanakan setelah permintaan shutdown, timeout berlaku, atau thread semasa terganggu, mana yang berlaku terlebih dahulu. Masa maksimum untuk menunggu ditentukan oleh timeout, dan nilai ini dinyatakan dalam unitunit yang ditentukan oleh TimeUnitenum; sebagai contoh , TimeUnit.SECONDS. Kaedah ini melemparkan java.lang.InterruptedExceptionapabila utas semasa terganggu. Ia kembali benar apabila pelaksana diberhentikan dan salah ketika tamat waktu tamat sebelum penamatan.
  • boolean isShutdown () kembali benar apabila pelaksana telah dimatikan.
  • void shutdown () memulakan penutupan yang teratur di mana tugas yang diserahkan sebelumnya dilaksanakan tetapi tidak ada tugas baru yang diterima.
  • Penyerahan masa depan (tugas yang dapat dipanggil) menyerahkan tugas pengembalian nilai untuk pelaksanaan dan mengembalikan Futuremewakili hasil tugas yang belum selesai.
  • Penyerahan masa depan (tugas yang dapat dijalankan) menyerahkan Runnabletugas untuk pelaksanaan dan mengembalikan Futuremewakili tugas itu.

Antara Futuremuka mewakili hasil pengiraan tak segerak. Hasilnya dikenali sebagai masa depan kerana biasanya tidak akan tersedia sehingga beberapa saat di masa depan. Anda boleh menggunakan kaedah untuk membatalkan tugas, mengembalikan hasil tugas (menunggu selama-lamanya atau waktu tunggu yang berlalu ketika tugas belum selesai), dan menentukan apakah tugas telah dibatalkan atau selesai.

Yang Callableantara muka adalah sama dengan Runnableantara muka yang mana ia menyediakan kaedah tunggal yang menerangkan tugas untuk melaksanakan. Tidak seperti Runnable's void run()kaedah, Callable' s V call() throws Exceptionkaedah boleh mengembalikan nilai dan membuang pengecualian.

Kaedah kilang pelaksana

At some point, you'll want to obtain an executor. The Executor framework supplies the Executors utility class for this purpose. Executors offers several factory methods for obtaining different kinds of executors that offer specific thread-execution policies. Here are three examples:

  • ExecutorService newCachedThreadPool() creates a thread pool that creates new threads as needed, but which reuses previously constructed threads when they're available. Threads that haven't been used for 60 seconds are terminated and removed from the cache. This thread pool typically improves the performance of programs that execute many short-lived asynchronous tasks.
  • ExecutorService newSingleThreadExecutor() creates an executor that uses a single worker thread operating off an unbounded queue -- tasks are added to the queue and execute sequentially (no more than one task is active at any one time). If this thread terminates through failure during execution before shutdown of the executor, a new thread will be created to take its place when subsequent tasks need to be executed.
  • ExecutorService newFixedThreadPool(int nThreads) creates a thread pool that re-uses a fixed number of threads operating off a shared unbounded queue. At most nThreads threads are actively processing tasks. If additional tasks are submitted when all threads are active, they wait in the queue until a thread is available. If any thread terminates through failure during execution before shutdown, a new thread will be created to take its place when subsequent tasks need to be executed. The pool's threads exist until the executor is shut down.

The Executor framework offers additional types (such as the ScheduledExecutorService interface), but the types you are likely to work with most often are ExecutorService, Future, Callable, and Executors.

See the java.util.concurrent Javadoc to explore additional types.

Bekerja dengan kerangka Pelaksana

Anda akan dapati bahawa rangka kerja Pelaksana cukup mudah untuk digunakan. Dalam Penyenaraian 2, saya telah menggunakan Executordan Executorsuntuk mengganti contoh pelayan dari Penyenaraian 1 dengan alternatif berasaskan kumpulan utas yang lebih berskala.

Penyenaraian 2. Server.java (Versi 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

Penyenaraian 2 menggunakan newFixedThreadPool(int)untuk mendapatkan pelaksana berasaskan kumpulan utas yang menggunakan semula lima utas. Ia juga diganti new Thread(r).start();dengan pool.execute(r);untuk menjalankan tugas yang dapat dijalankan melalui salah satu utas ini.

Penyenaraian 3 menyajikan contoh lain di mana aplikasi membaca kandungan laman web sewenang-wenangnya. Ini mengeluarkan baris yang dihasilkan atau pesan kesalahan jika isinya tidak tersedia dalam waktu maksimum lima detik.