معرفی نخ (Thread)
چندنخی به برنامه این امکان رو میده که چند کار رو به صورت همزمان انجام بده.
وظیفه ی هر نخ اجرای یک کار از اول تا اخرشه، هر برنامه میتونه یک یا چند نخ داشته باشه و با هر نخ برنامه میتونه یک کار جداگونه رو انجام بده، مثلا زمانی که تو برنامه دارید تایپ می کنید و همزمان یک فایل رو دانلود می کنید، هر کار توسط یک نخ داره انجام میشه و هر دو کار داره به صورت همزمان اتفاق می افته.
حتی زمانی که پردازنده فقط یک هسته و یک نخ فیزیکی داشته باشه، باز فرایند چند نخی میتونه اجرا بشه، سیستم عامل مدیریت منابع رو در دست میگیره و از فضای خالی پردازنده برای اجرای چندین نخ استفاده میکنه که اصطلاحا بهش تسهیم زمانی (time slicing) یا به اشتراک گذاری زمان (time sharing) میگیم، این فرایند همزمان نیست اما بخاطر سرعت بالا، همزمان به نظر میرسه؛ در مورد این موضوع به طور مفصل در قسمت نخ ها در برنامه نویسی صحبت شد.
جاوا به صورت پیشفرض از ویژگی چندنخی پشتیبانی میکنه و برای استفاده از قابلیت چندنخی در جاوا نیازی به اضافه کردن کتابخونه های جانبی نیست. اما در کاتلین علاوه بر امکان استفاده از ویژگی های چندنخی در جاوا یک کتابخونه ی اختصاصی و مدرن به نام کروتینز براش وجود داره که عمدتا از اون کتابخونه برای چندنخی در کاتلین استفاده میکنن اما یادگیری چندنخی در جاوا دید گسترده تری به برنامه نویس های کاتلین در مورد چندنخی میده.
اینترفیس Runnable
برای اجرای هر کاری توسط یک نخ ابتدا باید اون کار رو ایجاد کنیم، برای ایجاد یک کار از اینترفیس Runnable در جاوا و کاتلین استفاده می کنیم؛اینترفیس Runnable دارای یک متد (تابع) به نام run() است و اجرای کد ها داخل این متد اتفاق می افته، متد run مقداری رو بر نمیگردونه و تنها متد (تابع) اینترفیس Runnable است.
| <<java.lang.Runnable>> | |
|---|---|
| +run(): void | کد ها داخل این متد اجرا میشن. |
میخوایم یک کار تعریف کنیم که اعداد صفر تا n رو نمایش بده.
class PrintNumbersTask implements Runnable{
private int n;
public PrintNumbersTask(int n){
this.n = n;
}
@Override
public void run(){
for(int i=0; i<= n; i++){
if(i % 10 == 0) System.out.println();
System.out.print(i + " ");
}
}
}
class PrintNumbersTask(private val n: Int): Runnable{
override fun run() {
for (i in 0 until n){
if (i % 10 == 0) println()
print("$i ")
}
}
}
میخوایم یک کار تعریف کنیم که یک کاراکتر رو n بار نمایش بده.
public class PrintCharTask implements Runnable{
private int numberOfChars;
private char ch;
public PrintCharTask(char ch, int numberOfChars){
this.numberOfChars = numberOfChars;
this.ch = ch;
}
@Override
public void run(){
for(int i=0; i<numberOfChars; i++){
if(i % 10 == 0) System.out.println();
System.out.print(ch);
}
}
}
class PrintCharTask(private val ch: Char, private val n: Int): Runnable {
override fun run() {
for (i in 0 until n){
if (i % 10 == 0) println()
print("$ch ")
}
}
}
کلاس Thread
تمام نخ (Thread) هایی که در جاوا ایجاد می کنیم، مستقیم یا غیر مستقیم روی نخ (Thread) اصلی (main thread) شروع به کار میکنن.
برای اجرای کار های تعریف شده با Runnable، از کلاس Thread در جاوا و کاتلین استفاده می کنیم.
تا قبل از آشنایی با کلاس Thread هرچی کد اجرا میکردیم روی نخ (Thread) اصلی (main thread) برنامه اجرا میشد بدون اینکه بدونیم داریم توسط یک نخ کد ها رو اجرا میکنیم، با استفاده از کلاس Thread میتونیم نخ های مختلفی رو برای کار های مختلف ایجاد کنیم.
در زیر کلاس Thread با متد (تابع) های پرکاربرد و کانستراکتور هاش شرح داده شده است.
| java.lang.Thread | |
|---|---|
| Thread(runnable: Runnable) | یک نمونه از thread ایجاد می کنه و پیاده سازی Runnable رو به عنوان پارامتر بهش پاس میدیم. |
| Thread() | یک نمونه از Thread ایجاد می کنه. |
| +start():void | Thread رو شروع می کنه. |
| +isAlive():boolean | بررسی میکنه ببینه آیا Thread در حال اجراست یا کارش تموم شده. |
| +setPeriority(periority: int): void | برای شروع Thread نسبت به بقیه thread ها اولویت قرار میده، یک عدد بین 1 تا 10 رو به عنوان پارامتر میتونیم بهش پاس بدیم و هرچی این عدد کمتر باشه Thread زودتر از بقیه ی Thread ها اجرا میشه. |
| +join(): void | نخ های دیگه رو متوقف میکنه تا کار نخی که باهاش صداش زدیم تموم بشه؛ این متد رو باید داخل نخی که میخوایم به طور موقت متوقف بشه صدا بزنیم. |
| +interrupt(): void | اجرای نخ رو قطع می کنه. |
| +sleep(millis: long): void | کلاس Thread رو برای مدت زمان مشخص متوقف میکنه. (این متد استاتیک است) |
مثال
در زیر میخوایم دو کاری که در قسمت قبل تعریف کردیم رو با Thread اجرا کنیم.
Thread thread0 = new Thread(new PrintNumbersTask(200));
Thread thread1 = new Thread(new PrintCharTask('a', 100));
thread0.start();
thread1.start();
val thread0 = Thread(PrintNumbersTask(200))
val thread1 = ThreaD(PrintCharTask('a', 100)
داخل کلاس Thread اینترفیس Runnable وجود داره میتونیم از کلاس Thread مستقیم ارث بری کنیم و Runnable رو داخلش پیاده سازی کنیم.
public class PrintNumbersThread extends Thread{
private int n;
public PrintNumbersThread(int n){
this.n = n;
}
@Override
public void run() {
for (int i = 0; i < n; i++) {
if (i % 10 == 0) System.out.println();
System.out.print(i + " ");
}
}
}
public class PrintCharThread extends Thread {
private int n;
private char ch;
public PrintCharThread(char ch, int n){
this.n = n;
this.ch = ch;
}
public PrintCharThread()
@Override
public void run() {
for (int i=0; i<n; i++){
if(i % 10 == 0) System.out.println();
System.out.print(ch + "");
}
}
}
class PrintNumbersThread(private val n: Int): Thread(){
override fun run() {
for (i in 0 until n){
if (i % 10 == 0) println()
print("$i ")
}
}
}
class PrintCharThread(private val ch: Char, private val n: Int): Thread() {
override fun run() {
for (i in 0 until n){
if (i % 10 == 0) println()
print("$ch ")
}
}
}
متد (تابع) join
فرض کنید دوتا Thread داریم به نام های A و B، نخ A رو داخل نخ B اجرا کنیم و در جایی از اجرای کد میخوایم نخ B متوقف بشه تا اجرای نخ A کامل بشه، بنابراین از متد join در نخ A برای توقف موقت نخ B استفاده می کنیم.
میخوایم در کار PrintNumbersTask یک Thread از کار PrintRandomCharTask ایجاد کنیم و متد join رو صدا بزنیم؛ بنابراین کد PrintNumbersTask رو به صورت زیر تغییر میدیم.
private static class PrintNumbersTask implements Runnable {
private int n;
public PrintNumbersTask(int n){
this.n = n;
}
@Override
public void run() {
Thread printCharThread = new Thread(new PrintCharTask('a', 1000));
printCharThread.start();
try {
for (int i = 0; i <n; i++) {
if (i == 50)
printCharThread.join();
System.out.print(i + " ");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
private class PrintNumbersTask(private val n: Int) : Runnable {
override fun run() {
val printCharThread = Thread(PrintCharTask('a', 1000))
printCharThread.start()
try {
for (i in 0..<n) {
if (i == 50) printCharThread.join()
if(i % 10 == 0) println()
print("$i ")
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
سپس یک نخ برای اجرای کار PrintNumbersTask ایجاد میکنیم، زمانی که کار PrintNumbersTask شروع کنه به اجرا شدن کار PrintRandomCharsTask هم شروع به اجرا شدن میکنه چون داخل PrintNumbersTask با یک Thread اجراش کردیم.
public class TestJoin {
public static void main(String[] args) {
Thread t = new Thread(new PrintNumbersTask(1000));
t.start();
}
private static class PrintCharTask implements Runnable{
private int numberOfChars;
private char ch;
public PrintCharTask(char ch, int numberOfChars){
this.numberOfChars = numberOfChars;
this.ch = ch;
}
@Override
public void run(){
for(int i=0; i<numberOfChars; i++){
if(i % 10 == 0) System.out.println();
System.out.print(ch);
}
}
}
private static class PrintNumbersTask implements Runnable {
private int n;
public PrintNumbersTask(int n){
this.n = n;
}
@Override
public void run() {
Thread printCharThread = new Thread(new PrintCharTask('a', 1000));
printCharThread.start();
try {
for (int i = 0; i <n; i++) {
if (i == 50)
printCharThread.join();
if(i % 10 == 0) System.out.println();
System.out.print(i + " ");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
fun main() {
val t = Thread(PrintNumbersTask(1000))
t.start()
}
private class PrintCharTask(private val ch: Char, private val numberOfChars: Int) : Runnable {
override fun run() {
for (i in 0..<numberOfChars) {
if (i % 10 == 0) println()
print(ch)
}
}
}
private class PrintNumbersTask(private val n: Int) : Runnable {
override fun run() {
val printCharThread = Thread(PrintCharTask('a', 1000))
printCharThread.start()
try {
for (i in 0..<n) {
if (i == 50) printCharThread.join()
if(i % 10 == 0) println()
print("$i ")
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
ابتدا دو کار نمایش کاراکتر های تصادفی و چاپ اعداد تقریبا همزمان اجرا میشن (کار نمایش کاراکتر های تصادفی کمی زودتر)، سپس زمانی که چاپ اعداد به 50 میرسه متوقف شده، تا کار نمایش کاراکتر ها تموم بشه و بعد از تموم شدنش کار چاپ اعداد ادامه پیدا می کنه.
متد (تابع) sleep در Thread
با استفاده از متد (تابع) sleep میتونیم یک نخ (Thread) رو تا یک بازه ی زمانی مشخصی متوقف کنیم،، زمانی که داخل یک کار صدا زده بشه، Thread اجرا کننده ی کار رو متوقف میکنه و Thread هایی که داخل کار اجرا شدن هم به طور موقت متوقف میشن؛ متد sleep استاتیکه
در مثال قبل بجای join از Thread.sleep(millis) استفاده می کنیم.
public class TestSleep {
public static void main(String[] args) {
Thread t = new Thread(new SecondCounter());
t.start();
System.out.println("Hello from main thread");
}
private static class SecondCounter implements Runnable {
@Override
public void run() {
System.out.println("20 seconds to end of the loop");
try {
for (int i = 1; i <= 20; i++) {
Thread.sleep(1000);
System.out.print(i + " ");
}
Thread t = new Thread(() -> System.out.println("\nEnd of the loop reached"));
t.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Throws(InterruptedException::class)
fun main() {
val t = Thread(PrintNumbersTask(1000))
t.start()
println("Hello from main thread")
}
private class PrintNumbersTask : Runnable {
override fun run() {
try {
for (i in 0..<20) {
Thread.sleep(1000)
print("$i ")
}
val thread = Thread{
println("\nEnd of the loop reached")
}
} catch (e: InterruptedException) {
e.printStackTrace()
} }
}
مفهوم Thread Pool
زمانی که بخوایم یکی دو نخ ایجاد کنیم، کلاس Thread کاملا مناسبه ولی اگه بخوایم برای چند کار تعداد زیادی نخ ایجاد کنیم، ایجاد یک Thread Pool بهینه تره.
برای ایجاد Thread Pool در جاوا یک اینترفیس به نام ExecutorService وجود داره، میتونیم پیاده سازی های مختلف این اینترفیس رو با کلاس Executors به صورت استاتیک صدا بزنیم.
| java.util.concurrent.ExecutorService | |
|---|---|
| +shutdown(): void | اجرای کار ها توسط ExecutorService رو متوقف میکنه ولی به کار هایی که هم اکنون در حال اجرا شدن هستن اجازه میده تا فعالیتشون تموم بشه. |
| +shutdownNow(): List<Runnable> | اجرای تمام کار ها توسط ExecutorService رو فورا متوقف میکنه و در آخر یک لیست از کار های اجرا نشده بر میگردونه. |
| +isShutdown(): boolean | اگه ExecutorService متوقف شده باشه مقدار true رو بر میگردونه. |
| +isTerminated(): boolean | اگه همه ی کارها متوقف شده باشن یا به اتمام رسیده باشن مقدار true رو بر میگردونه. |
| +close(): void | بعد از اجرای کار های اضافه شده با صدا زدن close، کلاس ExecutorService بسته میشه و اگه بعد از close کاری رو بهش اضافه کنیم دچار RejectedExecutionException میشیم. (متد close از jdk 19 به بعد در دسترس است) |
پیاده سازی های مختلفی از ExecutorService در کلاس Executors وجود داره که بر اساس نیاز میتونیم از هرکدوم که خواستیم استفاده کنیم.
| java.util.concurrent.Executors | |
|---|---|
| +newFixedThreadPool(numberOfThreads: int): ExecutorService | یک ExecutorService ایجاد میکنه با تعداد مشخصی نخ (Thread) زمانی که یک نخ (Thread) کارش تموم میشه، میتونه کار جدید رو شروع کنه. |
| +newCachedThreadPool(): ExecutorService | یک ExecutorService ایجاد میکنه و به اندازه ی نیاز برنامه نخ (Thread) ایجاد میکنه، زمانی که یک نخ (Thread) کارش تموم بشه کار جدید رو میتونه با نخی که خالی شده شروع کنه. |
| +newSingleThreadPool(): ExecutorService | یک نمونه از ExecutorService ایجاد میکنه با فقط یک نخ، زمانی که کار نخ تموم بشه، کار جدید رو همون نخ شروع میکنه. |
در بخش اینترفیس Runnable در جاوا دو کار PrintNumbersTask و PrintCharTask رو پیاده کردیم، حالا میخوایم این دو کار رو با ExecutorService اجرا کنیم.
در مثال زیر بعد از تموم شدن یکی از کارها، کار مربوط به نمایش حرف d شروع به اجرا شدن میکنه.
public class ThreadPool {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(new PrintNumbers(1000));
service.execute(new PrintChar('a',400));
service.execute(new PrintChar('b', 300));
service.execute(new PrintChar('c', 200));
service.execute(new PrintChar('d', 100));
service.close();
//RejectedExecutionException
service.execute(new PrintChar('e', 100));
}
private static class PrintChar implements Runnable{
private final int n;
private final char ch;
PrintChar(char ch, int n){
this.n = n;
this.ch = ch;
}
@Override
public void run() {
for (int i=0; i<n; i++) {
if (i % 10 == 0) System.out.println();
System.out.print(ch + "");
}
}
}
private static class PrintNumbers implements Runnable{
private final int n;
PrintNumbers(int n){
this.n = n;
}
@Override
public void run() {
for (int i=0; i<n; i++){
if (i % 10 == 0) System.out.println();
System.out.print(i);
}
}
}
}
@Throws(InterruptedException::class)
fun main() {
val service = Executors.newFixedThreadPool(3)
service.execute(PrintNumbers(1000))
service.execute(PrintChar('a', 400))
service.execute(PrintChar('b', 300))
service.execute(PrintChar('c', 200))
service.execute(PrintChar('d', 100))
service.close()
//RejectedExecutionException
service.execute(PrintChar('e', 100))
}
private class PrintChar(private val ch: Char, private val n: Int) : Runnable {
override fun run() {
for (i in 0..<n) {
if (i % 10 == 0) println()
print(ch.toString() + "")
}
}
}
private class PrintNumbers(private val n: Int) : Runnable {
override fun run() {
for (i in 0..<n) {
if (i % 10 == 0) println()
print(i)
}
}
}
خلاصه
- از نخ ها برای انجام چند کار به صورت همزمان در برنامه استفاده می کنیم.
- برای ایجاد کار در جاوا از اینترفیس Runnable استفاده میکنیم.
- برای اجرای کار ها در جاوا از کلاس Thread استفاده می کنیم.
- زمانی که بخوایم تعداد زیادی کار رو اجرا کنیم بجای کلاس Thread با یکی از پیاده سازی های ExecutorService یک Thread Pool ایجاد میکنیم.
- زمانی که چند نخ به صورت همزمان بخوان یک داده رو تغییر بدن دچار race condition میشیم.
- از کلیدواژه ی synchronized در جاوا برای جلوگیری از race condition استفاده می کنیم.
بازخورد و دیدگاه ها
اولین نفری باشید که در این صفحه نظر میدهید