مقایسه Java NIO و IO

در آموزشهای جاوا دیدیم که API جدید java.NIO به جاوا اضافه شده و با قابلیتهایش میتواند به جای API قدیمی java.IO استفاده شود. حالا سوالی که پیش میآید این است که چه زمانی باید از java.NIO استفاده شود و چه زمانی بهتر است همچنان از API قدیمی java.IO استفاده کنیم؟
در این متن، تلاش میکنم تفاوتهای بین این دو API را شفافسازی کنم و در آخر بتوانیم نتیجه بگیریم که از هر کدام کجا استفاده کنیم و اینکه این انتخاب چگونه طراحی کد ما را دستخوش تغییر میکند.
تفاوتهای اصلی بین NIO و IO
جدول زیر تفاوتهای اصلی بین این دو API را مشخص میکند. در ادامه به صورت دقیقتر این تفاوتها را بررسی میکنیم.
NIO | IO |
بافرمحور | جویبارمحور |
غیر مسدودکننده | مسدودکننده |
Selectorها |
جویبارمحور در مقابل بافرمحور
اولین تفاوت عمده بین java NIO و IO این است که IO جویبارمحور است ولی NIO بافرمحور است. اما این به چه معناست؟
زمانی که میگوییم Java IO جویبارمحور است، منظورمان این است که شما میتوانید در یک بار خواندن، یک یا چند بایت از جویبار ورودی را بخوانید. اینکه چگونه از این بایتها استفاده کنید بستگی به خودتان دارد. اما باید در نظر داشته باشید که این بایتها جایی ذخیره نشدهاند. شما نمیتوانید روی دادههای این جویبار جلو و عقب بروید و برای این منظور، باید ابتدا دادهها را ذخیره (کش) کنید.
رویکرد بافرمحور در Java NIO اندکی متفاوت است. دادهها در بافری ذخیره میشوند و آماده پردازشهای بعدی هستند. هر زمان که نیاز داشته باشید، میتوانید روی بافر به جلو و عقب بروید. وجود این بافر به شما انعطاف بیشتری میدهد اما همواره باید بررسی کنید که بافر تمام دادهای که برای پردازش نیاز دارید را دریافت کرده باشد. همچنین مهم است دقت داشته باشید که با خواندن داده بیشتر در بافر، دادههای موجود در بافر را که هنوز پردازش نکردهاید بازنویسی نکنید.
مسدودکننده در مقابل غیرمسدودکننده
جویبارها در Java IO به صورت مسدودکننده هستند. به این معنی که وقتی نخ پردازشیای متدهای ()read یا ()write را صدا میزند، نخ باید منتظر بماند تا داده مورد نظر به صورت کامل خوانده (یا نوشته) شود و آن نخ در آن زمان هیچ کار دیگری نمیتواند انجام دهد.
اما حالت غیرمسدودکننده در Java NIO اجازه میدهد که نخ پردازشی، درخواست خواندن اطلاعات از کانال داده را بدهد و فقط چیزی را دریافت کند که همین الان در دسترس است، یا اینکه اصلا چیزی در حال حاضر در دسترس نباشد و چیزی دریافت نکند. اکنون نخ پردازشی میتواند به جای معطل شدن برای در دسترس قرار گرفتن داده برا خواندن، سراغ کار دیگری برود.
همین وضعیت برای نوشتن در حالت غیرمسدودکننده نیز برقرار است. یک نخ پردازشی میتواند درخواست دهد که دادهای در کانال نوشته شود اما منتظر نماند تا عمل نوشتن تمام شود، بلکه میتواند در این زمان سراغ پردازش دیگری برود.
در اکثر اوقات، در حینی که یک نخ پردازشی درخواستی برای IO داده ولی نتیجه هنوز انجام نشده، از این زمان برای درخواست دادن روی کانالهای دیگر استفاده میکند. به این وسیله تنها یک نخ پردازشی قابلیت مدیریت چندین کانال ورودی/خروجی را پیدا میکند.
انتخابکننده (Selector)ها
انتخابکنندهها اجازه میدهند یک نخ پردازشی چندین کانال ورودی/خروجی را مدیریت کند. شما میتوانید کانالهای مختلف را روی یک انتخابکننده تنظیم کنید و از یک نخ پردازشی برای «انتخاب» بین کانالهای ورودیای که داده آماده برای پردازش دارند یا آماده نوشتن هستند استفاده کنید. این سازوکار انتخابکننده، کار مدیریت چند کانال را تسهیل میکند.
چگونه انتخاب بین IO و NIO طراحی برنامه ما را دستخوش تغییر میکند
هر کدام از IO یا NIO را که به عنوان ابزار ورودی/خروجی خود انتخاب کنید ممکن است این جوانب از طراحی برنامه شما را تحت تاثیر قرار دهد:
- فراخوانی API های کلاسهای NIO یا IO.
- پردازش داده.
- تعداد نخهای پردازشی استفادهشده برای پردازش داده.
فراخوانی API
تعجبی ندارد که فراخوانی APIها برای استفاده از کلاسهای NIO با فرخوانی API برای استفاده از کلاسهای IO متفاوت است. مثلا به جای اینکه بایت به بایت از InputStream داده بخوانیم، ابتدا داده در بافر نوشته میشود و سپس از بافر پردازش میشود.
پردازش داده
انتخاب بین IO و NIO پردازش داده را نیز تحت تاثیر قرار میدهد.
در طراحی مبتنی بر IO، داده به صورت بایت به بایت از InputStream یا یک Reader خوانده میشود. فرض کنید که در حال پردازش جویباری از داده متنی به صورت زیر هستیم.
Name: Anna Age: 25 Email: anna@mailserver.com Phone: 1234567890
این جویبار متنی باید به شکل زیر پردازش شود:
InputStream input = ... ; // get the InputStream from the client socket BufferedReader reader = new BufferedReader(new InputStreamReader(input)); String nameLine = reader.readLine(); String ageLine = reader.readLine(); String emailLine = reader.readLine(); String phoneLine = reader.readLine();
به این توجه کنید که پیشرفت روال کلی برنامه ارتباط مستقیمی با دریافت و پردازش از جویبار ورودی دارد. به بیان دیگر زمانی که اولین ()reader.readLine به اتمام برسد و خروجی برگرداند، مطمئن هستیم که یک خط از ورودی خوانده شده است. در حقیقت متد readLine منتظر میماند (مسدود میشود) تا یک خط ورودی دریافت و خوانده شود. همچنین برای ما مشخص است که این خط شامل اسم است. همچنین زمانی که ()readLine دوم تمام شود، میدانیم که خط دوم که شامل سن بوده به صورت کامل دریافت و پردازش شده است.
همانطور که میبینید برنامه فقط زمانی پردازش را انجام میدهد که دادهای برای خواندن وجود داشتهباشد و برای هر مرحله شما دقیقا میدانید چه دادهای قرار است پردازش شود. زمانی که نخ پردازشی دادهای که آمده را پردازش کرد، (عموما) قرار نیست به عقب برگردد. در تصویر زیر روال کار به صورت گرافیکی نیز نمایش داده شده است:

اما پیادهسازی با کمک Java NIO ظاهر متفاوتی دارد. یک مثال سادهشده مشابه زیر است:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
به خط دوم دقت کنید که دادهها را از کانال داخل بافر میخواند. زمانی که متد تمام میشود و برمیگردد، شما نمیدانید که آیا تمام دادهای که نیاز دارید داخل بافر هست یا نه. تمام چیزی که میدانیم این است که مقداری داده در بافر وجود دارد. این امر کار پردازش اطلاعات را سخت میکند.
فرض کنید زمانی که کار متد (read(buffer تمام میشود، به جای تمام خط، تنها نصف خط در بافر موجود باشد، مثلا تنها رشته “Name: an” وجود داشته باشد. آیا میتوانید آن را پردازش کنید؟ نه واقعا. شما ناچارید صبر کنید که حداقل یک خط تمام داده دریافت شود تا بتوانید چیزی پردازش کنید.
خب از کجا باید بفهمیم که بافر به اندازه کافی داده دارد که پردازش را شروع کنیم؟ تنها راه فهمیدنش این است که داخل بافر را نگاه کنیم. نتیجه این میشود که ناچاریم چندینبار داخل بافر را نگاه کنیم تا ببینیم داده مورد نظر را دارد یا خیر. اینکار هم غیربهینه است و هم ظاهر کد را کثیف میکند. برای مثال:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
متد ()bufferFull وظیفه بررسی این را دارد که چه مقدار داده داخل بافر خوانده شده است و در صورت پر بودن بافر مقدار true برمیگرداند. به بیان دیگر اگر بافر به اندازهای داده داشته باشد که قابل پردازش باشد، پر درنظر گرفته میشود.
باید دقت داشت که متد bufferFull باید بافر را همانگونه که تحویل گرفته تحویل دهد و هیچگونه تغییری در آن ندهد، چرا که اگر تغییری بدهد موقع خواندن از بافر ممکن است از محل صحیحی داده خوانده نشود. البته که اینکار غیرممکن نیست ولی باید آن را در نظر داشته باشیم.
اگر بافر پر باشد، میتواند پردازش شود، اما اگر پر نباشد، شما ممکن است بسته به نیاز خود بتوانید همین مقدار داده را نیز پردازش کنید. اینکار در برخی کاربردها منطقی است و در بسیاری دیگر نه!
سازوکار حلقه چککننده پر بودن بافر در شکل زیر نشان داده شده است.

خلاصه
پکیج NIO اجازه مدیریت چندین کانال (فایل یا اتصال شبکه) را با تنها یک (یا تعداد اندکی) نخ پردازشی میدهد. اما استفاده از آن هزینهای هم دارد. در ازای استفاده از تعداد نخهای کمتر، عملیات پردازش داده، نسبت به «حالت جویبار مسدودکننده در IO» پیچیدهتر میشود.
اگر نیاز دارید که هزاران اتصال همزمان را مدیریت کنید که هر کدام مقدار داده اندکی ارسال میکنند (مثلا سرور چت) احتمالا استفاده از NIO بهصرفه باشد. همچنین زمانی که نیاز دارید تعداد زیادی اتصال را همواره برقرار داشتهباشید، مثلا شبکه P2P، مدیریت کردن همه اتصالات با یک نخ پردازشی یک مزیت محسوب میشود. این مکانیسمِ یک نخ پردازشی و چندین اتصال در تصویر زیر نمایش دادهشده است.

در عوض اگر اتصالات کمی برای مدیریت دارید که پهنای باند بسیار بالایی دارند و مقدار داده زیادی را ارسال و دریافت میکنند، احتمالا استفاده از یک کلاس IO انتخاب بهتری است. نمودار زیر استفاده از یک کلاس IO قدیمی را در طراحی سرور نشان میدهد:

منبع: tutorials.jenkov.com
.
.
با ما همراه باشید
آدرس کانال تلگرام: JavaCupIR@
آدرس اکانت توییتر: JavaCupIR@
آدرس صفحه اینستاگرام: javacup.ir
آدرس صفحه ویرگول: javcup
آدرس گروه لینکدین: Iranian Java Developers
سلام
ممنون از برگردان مقاله. پیشنهاد می کنم واژه های تخصصی همچون Thread و Stream را به صورت انگلیسی استفاده کنید. به این دلیل که واژه های تخصصی کلمات شناخته شده ای هستند که در بخصوص کد های برنامه و مستندات API ها به همان صورت (انگلیسی) بکار برده می شوند.
با احترام
سلام
کاملا درست میفرمایید. اما واژههای «نخ» و «جویبار» برای Thread و Stream چندان غیرمصطلح و کمکاربرد نیستند و در فیلمهای آموزشی جاواکاپ و درسهای دانشگاهی، از این معادلهای فارسی کموبیش استفاده میشه و به گوش شنوندگان آشنا هستند.
ضمن اینکه وقتی در یک متن فارسی، واژههای انگلیسی زیاد تکرار و دیده بشن، خوانایی نوشته کم میشه. این دو واژه هم چون زیاد در این مقاله تکرار شدن، ترجیح دادیم از معادل فارسیشون استفاده کنیم تا یکدست بودن نوشته حفظ بشه.
خیلی ممنون که نظرتون رو با ما در میون گذاشتید.