به چه صورت Java Servlet رو Thread Safe کنیم

وقتی مشغول طراحی وب‌سایت با استفاده از JSP/Servlet هستیم، با متدهای ()doGet و ()doPost و ()service به وفور برخورد داریم. و بحث Thread-Safety در مورد طراحی servletها بسیار مهم و حائز اهمیته، به این دلیل که کاربران به اپلیکیشن ما از هر نقطه از دنیا و در هر لحظه امکان دسترسی دارند و احتمال یا به عبارت بهتر امکان فراخوانی «دقیقا در یک لحظه» صفحه مورد نظر بسیار زیاده و برای جلوگیری از باگ‌های ناخواسته در این زمینه می‌بایست حتما سرولت‌های ما thread safed باشن.

قبل از ادامه صحبت بد نیست که در مورد thread یه توضیحی بدم. Thread یک پروسه اجرای تکی برنامه‌س. به عبارت دیگه یک زنجیره پشت سر هم اجرای قسمتی از برنامه ماست. وقتی ما می‌گیم یه برنامه multithreadه به این معنی نیست که ۲ تا نسخه از برنامه به صورت همزمان در حال اجراس، بلکه برنامه یکبار فقط اجرا شده و چندین بار توسط threadهای مختلف قسمت‌های مخلف اون دارن اجرا می‌شن.

وقتی برنامه threadهای مختلف داشته باشه عبارت Thread Safe به این تعریفه که وقتی threadهای مختلف یک بخش مشخص برنامه رو دارن اجرا می‌کنن مقداری از حافظه رو دارن به اشتراک می‌ذارن و threadهای مختلف اون بخش مشخص حافظه رو می‌تونن بخونن، داخلش بنویسن. مثال زیر رو در نظر بگیرین:
public class NotThreadSafe() {
   private int variable = 0;

   public void doSomething() {
      System.out.println("1) variable: " + variable);

      variable += 1;

      System.out.println("2) variable: " + variable);
   }
}
شرایطی را در نظر بگیرین که Thread شماره ۱ الان تو خط در حال اجرای خط ۹ام برنامه‌س و در همین لحظه هم Thread شماره ۲ خط ۷ام برنامه رو اجرا کرده باشه. خروجی که خواهیم داشت به این ترتیب می‌شه:
1) variable: 0
1) variable: 0
2) variable: 2 <---
2) variable: 1
و خب مشخصا ما انتظار این رو نداریم. اینجاس که به این کلاس Thread Safe نیست. یعنی threadهای مختلفی که به صورت نسبتا همزمان این کلاس رو صدا می‌زنن دارن روی همدیگه تاثیر می‌ذارن.

چه جوری این مشکل رو حل کنیم؟
اولین راه حل این می‌تونه باشه که متغیرهایی که امکان خوانده و نوشتن شدن دارن رو به صورت عمومی تعریف نکنیم و فقط جایی که احتیاج به تعریف و مقداردهی داره اونارو ایجاد بکنیم. مثال بالا رو به صورت زیر تغییر می‌دیم:
public class IsThreadSafe() {
   public void doSomething() {
      private int variable = 0;

      System.out.println("1) variable: " + variable);

      variable += 1;

      System.out.println("2) variable: " + variable);
   }
}
همونطور که می‌بینین ما variable رو به جای اینکه تو scope کلاس تعریف کنیم، تو scope متد ()doSomething تعریف کردیم. بنابراین variable بین threadهای مختلف کلاس مشترک نیست. بلکه داخل متدی که تعریف شده مشترک‌ه. که محتویات این داخل هم اثر روی threadهای دیگه نمی‌ذاره.

راه حل دوم رو می‌تونین به وسیله synchronize کردن مقطعی ایجاد بکنین. یعنی اون بخشی از کد برنامه رو که احتمال می‌دین ممکنه باعث اختلال تو thread safety بشه رو داخل بلاک synchronize بذارین. به این صورت:
public class IsThreadSafe() {
   private int variable = 0;
   private String mutex = "";

   public void doSomething() {
      synchronized (mutex) {
         System.out.println("1) variable: " + variable);

         variable += 1;

         System.out.println("2) variable: " + variable);
      }
   }
}
به این ترتیب مشکل ما نیز حل می‌شه ولی یه نکته خیلی مهم وجود داره که هر بخشی که داخل بلاک synchronize قرار بگیره فقط و فقط توسط یه thread در آن واحد امکان اجرا داره. و برای یه برنامه که به صورت multithread آماده شده JVM بقیه threadها رو مجبور به صبر می‌کنه که threadی که الان داخل اون بلاک قرار داره کارش تموم شه و از اون خارج بشه و بعد thread بعدی وارد اون بخش بشه. مشخصه که این وضعیت بعضی وقتها ممکنه مطلوب ما نباشه و باعث latency می‌شه.


پیاده‌سازی فرم ورود توسط ExtJS

برای استفاده از فریموورک ExtJS جهت پیاده‌سازی رابط گرافیکی فرض می‌کنم صفحه ورودی شما به این صورت می‌باشد:
<%@ page contentType="text/html; charset=utf-8" language="java" import="java.util.*"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>ExtJS Log in Sample</title>
   <link rel="stylesheet" type="text/css" href="http://localhost:8080/coder/web/theme/css/ext-all.css" />

   <script type="text/javascript" src="http://localhost:8080/coder/web/script/ext/core/ext-base.js"></script>
   <script type="text/javascript" src="http://localhost:8080/coder/web/script/ext/core/ext-all.js"></script>
   <script type="text/javascript" src="http://localhost:8080/coder/web/script/ext/user/login/login.js"></script>
</head>

<body>
</body>
   <div id="login"></div>
</html>
مشخص می‌باشد که آدرس‌دهی‌ها با توجه به شرایط شما می‌بایست تغییر یابد.

فایل login.js به صورت زیر می‌باشد :
Ext.onReady(function(){
   Ext.QuickTips.init();

   // Generating Log In Form
   var bd = Ext.get('login');

   var login_form = new Ext.FormPanel({
      id: login_form,
      monitorValid: true,
      labelWidth: 100,
      url: 'http://localhost:8080/coder/process/user/login',
      frame: true,
      monitorResize: true,
      bodyStyle: 'padding:5px 5px 0;',
      anchor: '50%',
      defaults: {
         width: 200,
         msgTarget: 'side'
      },
      defaultType: 'textfield',

      items: [{
         labelAlign: 'right',
         fieldLabel: 'نام کاربری',
         name: 'username',
         allowBlank: false,
      }, {
         fieldLabel: 'رمز عبور',
         name: 'password',
         allowBlank:false,
         inputType: 'password'
      }],

      buttons: [{
         text: 'انصراف',
         handler: function() {
            login_form.getForm().reset();
         }
      }, {
         text: 'ورود',
         handler: function() {
            if( login_form.getForm().isValid() ) {
               login_form.getForm().submit({
                  method:'POST', 

                  success: function() {
                     var nextScreen = 'http://localhost:8080/coder/';
                     window.location = nextScreen;
                  },
                  failure: function(form, action) {
                     if( action.failureType == 'server' ) {
                        obj = Ext.util.JSON.decode(action.response.responseText);
                        Ext.Msg.alert('نام کاربری و یا رمز عبور، نادرست می باشد.', obj.errors.reason);
                     } else {
                        Ext.Msg.alert('خطا!', 'در ارتباط با سرور اشکال رخ داده است : ' + action.response.responseText);
                     }
                  }, 
                  scope: this
               });
            }
         }
      }]
   });

   login_form.render(bd);

   Ext.fly('login').show();
});
آدرسی که فرم ورود به آن صفحه Submit می‌شود، و در فایل بالا به آن اشاره شده است،
http://localhost:8080/coder/process/user/login
در web.xml داریم :
<web-app>
   ...
   <servlet>
      <servlet-name>LogInController<servlet-name>
      <servlet-class>com.blogspot.coderspulse.web.user.LoginHandler</servlet-class>
   </servlet>
   <servlet-mapping>
      <servlet-name>LogInController</servlet-name>
      <url-pattern>/process/user/login</url-pattern>
   </servlet-mapping>
   ...
</web-app>
فایل LoginHandler.java
package com.blogspot.coderspulse.web.user;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginHandler extends HttpServlet {
   public void init(ServletConfig config) throws ServletException {
      super.init(config);
   }

   public void destroy() {
   }

   protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      boolean userValidated = false;

      // Do User validating  here

      response.setContentType("text/html");
      PrintWriter out = response.getWriter();

      if ( userValidated ) {
         // If User was validated, this is the Format to tell the Ext that it is a Success
         out.println("{success:true}");
      } else {
         // If User was NOT validated, this is the Format to tell the Ext that it is a Failure, and Why
         out.println("{success:false, errors:{reason:'Login failed.Try again'}}");
      }
      out.close();
   }
}
قابل ذکر است که Response ای که شما آماده کرده‌اید، می‌بایست، دقیقا با فرمت JSON بدون وجود ] در ابتدا و [ در انتهایش باشد.


راه‌حل‌هایی برای افزایش و بهبود کارایی وب‌سایت

Memory Leak
وب‌اپلیکیشن‌تون رو با هر زبان برنامه‌نویسی، پلتفرم پیاده‌سازی، معماری و ... که پیاده‌سازی می‌کنین، در وهله اول [که خب واضح و مبرهن‌ه] Error یا Exception نباید داشته باشه، ولی خب نکته مهم‌تر اینه که برنامه شما تحت هیچ شرایطی Memory Leak نباید داشته باشه. Memory Leak سرطان یه نرم‌افزاره. اگه یه جایی تو اپلیکیشن شما leak وجود داشته باشه، شما عملا با دست خودتون دارین بار اضافی به سرورتون تحمیل می‌کنین، باعث شدین Response Time صفحاتون بیشتر بشه، و خب احتمالا مجبور می‌شین به طور متناوب سرور رو restart کنین و ...

خب پس فرض ما الان اینه که برنامه شما به خودی خود کاملا صحیح و درست و بدون اشکال داره کار می‌کنه. مواردی که باید در نظر بگیرین که کارایی (Performance) وب‌سایت خودتون رو به مراتب افزایش بدین شامل این موارده:

۱) DNS Lookup
به ازای هر دومینی که محتویات صفحه شما از اونجاها آورده می‌شه یه Request و بالطبع یه DNS Lookup باید انجام بشه. هر چی تعداد این «گشتن»‌ها کمتر باشه، responseی که به براوزر می‌رسه کاهش پیدا می‌کنه. پس بنابراین هرجا که امکان‌ش رو داشته باشین بهتره که از آدرس‌های relative استفاده کنین تا آدرس‌های کامل. مثلا foo/bar.js/.. به جای http://www.domain.com/foo/bar.js

۲) کم کردن تعداد HTTP Requestها
٪۸۰ response time که کاربر باید صبر کنه تا صفحه براش کامل لود بشه، زمانیه که براوزر داره متحویات صفحه اعم از عکس و فایل‌های جاوااسکرپیت و css و فلش و ... رو دانلود می‌کنه. پس هر جا که امکان‌ش رو دارین باید این فایل‌ها رو ترکیب کنین. یعنی تمام فایل‌های css رو با هم ترکیب و به صورت یک فایل آپلود کنین، فایل‌های مختلف تصویری رو ترکیب کنین و یه دونه Image Sprite آپلود کنین و از طریق attributeهای css اونها رو تفکیک کنین.

۳) استفاده نکردن از URL Redirect
استفاده از redirect با تگ Meta موردی‌ه که باعث بالا رفتن زمان ریسپانس می‌شه. بنابراین شما اگه به هر دلیلی لازم دارین که کاربر رو از یه صفحه به صفحه دیگه به صورت اتوماتیک ریدایرکت کنین این عمل رو از طریق وب‌سرور (mod_rewrite) انجام بدین.

۴) عدم وجود Bad Request
تحت هیچ شرایطی به فایل یا آدرسی که وجود نداره ارجاع نکنین. یعنی شما 404 Error رو فقط و فقط باید برای آدرس صفحه داشته باشین (یعنی باید در نظر بگیرین که این error رو handle کنین) و برای فایل‌های javascript، css، عکس و ... تحت هیچ شرایطی نباید HTTP Code 404 برگرده از سرور به براوزر کاربر. به خاطر اینکه زمان نسبتا زیادی براوزر تلاش می‌کنه اون فایل رو پیدا کنه و در نهایت هم موفق نمی‌شه.

۵) استفاده از CDN
هر براوزر به صورت هم‌زمان امکان دانلود کردن نهایتا ۴ فایل به ازای هر هاست در آن واحد رو داره. پس اگه فرض کنیم شما تو صفحتون ۲۸ تا فایل css، javascript، عکس، فلش و ... داشته باشین و تمام این موارد روی یک دومین قرار گرفته باشه براوزر طی ۷ بار هر دفعه ۴ فایل، می‌تونه تمام اونارو دانلود کنه. ولی اگه شما این فایل‌ها رو روی چندین دومین مختلف توزیع کرده باشین سرعت دانلود شدن‌تون به مراتب بیشتر می‌شه. به این توزیع کردن فایل‌ها اصطلاحا CDN یا Content Delivery Network می‌گن. مثلا سایت گوگل تقریبا تمام فایل‌هاشو داره از gstatic.com لود می‌کنه، یاهو yimage.com فیس‌بوک fbcdn.com و ... که این دومین مجزا، عموما دارای چندین subdomain هم هست برای افزایش قابلیت توزیع فایل‌ها روی دومین‌های مختلف.

۶) استفاده از دومین بدون Cookie
وقتی شما یه CDN راه‌اندازی کردین، نکته بسیار مهم برای اون دومین(ها) اینه که در تمام اونها شما هیچ گونه cookie تعریف نکنین. به خاطر اینکه کوکی یه حجم (هرچند ناچیزی) به هر کدوم از اون فایل‌ها اضافه می‌کنه که اصولا لزومی برای اینکار وجود نداره و صرفا در درازمدت پهنای باند اون CDN رو بی‌جهت مصرف می‌کنه

۷) مشخص کردن Expire و Cache-Control
وقتی وب‌سرور شما داره به یه ریکوئست جواب می‌ده یه سری اطلاعات رو روی Header صفحه قرار می‌ده که یکی از اونها مشخص کردن وضعیت Expire شدن و Cache شدن محتویات اون صفحه‌س. شما برای افزایش سرعت لود شدن صفحات می‌تونین خیلی از محتویات صفحه رو که در زمان تغییر نمی‌کنن، یا قابلیت cache شدن دارن رو اونجا مشخص کنین. یکی از راه‌های این کار استفاده از mod_expires برای آپاچی‌ه به ترتیب زیر:
ExpiresActive On
ExpiresDefault A604800
ExpiresByType image/x-icon A2592000
ExpiresByType image/gif A2592000
ExpiresByType image/jpg A2592000
ExpiresByType image/jpeg A2592000
ExpiresByType image/png A2592000
ExpiresByType text/css A1209600
ExpiresByType application/x-javascript A1209600

604800 یعنی یک هفته، 2592000 یعنی یک ماه و ...

۸) GZip کردن محتویات
تقریبا تمام براوزرهای جدید این قابلیت را دارن که محتوای Zip شده از وب‌سرور تحویل بگیرن، به صورت داخلی اون رو Unzip کنن و بعد اون رو برای کاربر نمایش بدن. خاصیت استفاده از این کار اینه که به جای اینکه مثلا یه فایل ۱۰۰ کیلوبایتی دانلود بشه، همون فایل با حجم مثلا ۳۰ کیلوبایت دانلود می‌شه که خب اولا هم سریع‌تره هم پهنای باند کمتری از سرورتون مصرف می‌کنه. برای این کار می‌تونین از mod_deflate آپاچی استفاده کنین به این صورت:
SetOutputFilter DEFLATE
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
Header append Vary User-Agent

۹) Minify کردن تمام فایل‌ها
وقتی در محیط develop شما مشغول آماده‌سازی وب‌سایت هستید این:
   <div>
      <div>
         Something
      </div>
   </div>

هیچ ایرادی نداره، ولی وقتی شما می‌خواین سایت رو آپلود کنین بهتره که تمام این Spaceها، Tabهای اضافه رو پاک کنین و به این صورت آپلود کنین:
<div><div>Something</div></div>

چون مسلمه که برای یه وب سایت تو محیط Production خوانایی اینقدر مهم نیست که تو محیط Develop. و در نظر بگیرین که این عمل Minify کردن رو برای تمام فایل‌ها مخصوصا css و javascript انجام بدین.

نهایتا برای اطلاعات کامل‌تر، توضیحات رو از راهنمای گوگل و یاهو بخونین و همچنین برای تست کردن وضعیت وب‌سایت‌تون از Add-on های Page Speed و YSlow که برای Firebug طراحی شدن استفاده کنین.


سلام دنیا

public void main(String args[]) {
   System.out.println("Hello World!");
}

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes