2007年9月25日 星期二

Java 學習筆記 (10) - Reflection

class 的載入與檢視

class 載入

為了妥善使用有限的資源,Java 在真正需要使用到 class 的時候才會將其載入,當每一個 class 被載入時,JVM 就會為其自動產生一個 Class object。

【註】每一個 class 載入僅會有一個 Class object。因此即使使用同一個 class 產生多次 object,也只會有一個與其對應 Class object。

每一個 object 都可以透過 getClass() 的方式取得 Class object,以下為一個簡單的範例:
public class ClassDemo {
public static void main(String args[]) {
String name = "godleon";
Class stringClass = name.getClass();

System.out.println("類別名稱:" + stringClass.getName());
System.out.println("是否為介面:" + stringClass.isInterface());
System.out.println("是否為基本型態:" + stringClass.isPrimitive());
System.out.println("是否為陣列物件:" + stringClass.isArray());
System.out.println("父類別名稱:" + stringClass.getSuperclass().getName());
}
}

而 Java 只有在真正要用到 class 的時候才會將其載入,而真正用到的時候是指以下情況:
  1. 使用 class 生成 object 時
  2. 使用者指定要載入 class (利用 Class.forName() 或是 ClassLoader.loadClass())

若僅是宣告並不會載入 class,以下用一段程式碼來測試:

TestClass.java
public class TestClass {
static {
System.out.println("類別被載入");
}
}
LoadClassTest.java
public class LoadClassTest {
public static void main(String args[]) {
TestClass test = null; //class不會載入,因此不會顯示「類別被載入」
System.out.println("宣告 TestClass 參考名稱");
test = new TestClass(); //class被載入,顯示「類別被載入」
System.out.println("生成 TestClass 實例");
}
}

此外,由於 Java 支援 Run-Time Type Identification(RTTI,執行時期型別辨識),因此 Class 的訊息在編譯時期就會被加入 .class 檔案中;而執行時,JVM 則會在使用某 class 時,會先檢查相對應的 Class object 是否已經載入,如果沒有載入,則會尋找相對應的 .class 檔案載入。

而一個 class 在 JVM 中只會有一個 Class object,所以每個 object 都可以透過 getClass() 或是 .class 屬性取得 Class object。

之前有說過,在 Java 中任何東西皆為 object,因此 Array 也不例外,甚至如 primitive type、關鍵字 void 都有相對應的 Class object,以下用一段程式來說明:
public class ClassDemo2 {
public static void main(String args[]) {
System.out.println(boolean.class); //boolean
System.out.println(void.class); //void

int[] intAry = new int[10];
System.out.println(intAry.getClass().toString()); //class [I

double[] dblAry = new double[10];
System.out.println(dblAry.getClass().toString()); //class [D
}
}
Class 類別中的 forName 與 newInstance 方法

若要動態載入類別,可以使用 Class 類別中的 forName() method,此為 static method,因此可以直接透過 Class 類別使用;另外,若要取得類別的 instance,可使用 Class 類別中的 newInstance() method,以下用依段程式來說明兩個 method 的使用方法:
import java.util.*;

public class ClassForNameDemo {
public static void main(String[] args) {
try {
//forName的使用
Class c = Class.forName(args[0]);
System.out.println("getName : " + c.getName());
System.out.println("isInterface : " + c.isInterface());
System.out.println("isPrimitive : " + c.isPrimitive());
System.out.println("isArray : " + c.isArray());
System.out.println("SuperClass : " + c.getCanonicalName());

//newInstance的使用
List lst = (List) c.newInstance();
for(int i = 0 ; i < 10 ; i++)
lst.add("element" + i);
for(Object o : lst.toArray())
System.out.println(o);
}
catch(Exception e) {
e.printStackTrace();
}
}
}
而當使用 newInstance() 取得 instance 後,就可以使用 getClassLoader()、getConstructor()、getFields()、getMethods()、getModifiers()、getPackage() ..... 等方法,取得 Class 所產生的 instance 的相關資訊,而類別檢視器就是利用這樣子的方式所做出來的!


Class Loader

當 Java 需要使用到 Class 時,才會將 Class 載入;而載入 Class 的工作是由 Class Loader(類別載入器) 所負責。

每當 Java 程式啟動後,會有以下三種 Class Loader 進行載入 Class 的工作:
  1. Bootstrap Loader
    由 C++ 開發,會去搜尋 JRE 目錄($JAVA_HOME/jre/)中的 class 以及 lib 資料夾中的 *.jar 檔,檢查是否有指定的 class 需要載入。
  2. ExtClassLoader
    會去搜尋 JRE 目錄($JAVA_HOME/jre/)中的 lib/ext 資料夾,檢查其中的 class 與 *.jat 檔案,是否有指定的 class 需要載入。
  3. AppClassLoader
    搜尋 classpath 中是否有指定的 class 需要載入。
以下用一張圖形來說明三個 Class Loader 的階層關係:

其中 Bootstrap Loader 為 ExtClassLoader 的 Parent Class;而 ExtClassLoader 則是 AppClassLoader 的 Parent Class。

而從 JVM 啟動後,載入的順序為 Bootstrap Loader -> ExtClassLoader -> AppClassLoader。

而每個 class 所產生出來的 instance 都可以使用 getClassLoader() 知道負責載入 class 的是哪一個 class loader。


動態載入類別、產生物件、呼叫方法

接著在這個部分,要介紹的是如何動態的載入將已經寫好的 class,並產生物件來呼叫 class 中所包含的 method,以下用兩段程式說明:

Student.java
public class Student {
private String name;
private int score;

public Student() {
name = "N/A";
}
public Student(String name, int score) {
this.name = name;
this.score = score;
}

public void setName(String name) {
this.name = name;
}
public void setScore(int score) {
this.score = score;
}

public String getName() {
return name;
}
public int getScore() {
return score;
}

public void showData() {
System.out.println("Name : " + this.name);
System.out.println("Score : " + this.score);
}
}
newInstanceDemo.java
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class newInstanceDemo {
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName(args[0]);

/* (Begin)=============== 生成物件 ===============(Begin) */

//指定Constructor所使用的參數型態
Class[] oParam = new Class[2];
oParam[0] = String.class;
oParam[1] = Integer.TYPE;

//產生Constructor
Constructor constructor = c.getConstructor(oParam);

//指定參數的內容
Object[] paramObjs = new Object[2];
paramObjs[0] = "godleon";
paramObjs[1] = new Integer(90);

//透過Constructor產生物件
Object obj = constructor.newInstance(paramObjs);
System.out.println(obj);

/* (End)=============== 生成物件 ===============(End) */


/* (Begin)=============== 呼叫方法 ===============(Begin) */

//指定Method所使用的參數類型
Class[] mParam1 = {String.class}; //只有一個參數

//產生Method(指定method名稱與參數)
Method setName = c.getMethod("setName", mParam1);

//指定參數內容
Object[] mParamObjs1 = {"godleon"};

//呼叫方法
setName.invoke(obj, mParamObjs1); //呼叫setName方法

//指定Method所使用的參數類型
Class[] mParam2 = {Integer.TYPE};

//產生Method(指定method名稱與參數)
Method setScore = c.getMethod("setScore", mParam2);

//指定參數內容
Object[] mParamObjs2 = {new Integer(90)};

//呼叫方法
setScore.invoke(obj, mParamObjs2); //呼叫setScore方法

//產生Method(指定method名稱與參數)
Method showData = c.getMethod("showData", null);

//呼叫方法
showData.invoke(obj, null); //呼叫showData方法

/* (End)=============== 呼叫方法 ===============(End) */
}
catch(Exception e) {
e.printStackTrace();
}
}
}
由上面的程式碼可以知道,若是有執行時期動態載入 class,並呼叫其 method 的需求,透過使用 reflection,可以很簡單達成目的。

另外也可以動態的修改 Field 的值,可透過 getField() method 來達成,使用方式跟上面的程式大同小異,而若是要瞭解詳細的使用方式,可以參考此篇文章。

最後,此篇筆記就是著重在瞭解 Java 如何在執行期間動態載入 class 並使用,未來有機會用到的時候再來繼續深入探討.....

2007年9月20日 星期四

.NET DateTime 與 Unix Timestamp

剛剛在研究如何將 Unix Timestamp 與 .NET 中的 DateTime 物件互轉!

找到了以下幾篇文章:
  1. 瓶水相逢 - C# - Timestamp 與 DateTime 互轉
  2. UNIX timestamp to System.DateTime

然後還需要跟 JavaScript Date 物件中的 getTime() 方法進行轉換,然後用了以下程式碼:(忘記從哪一篇找來的.....)
//得到1970年的 timestamp
DateTime timeStamp = new DateTime(1970, 1, 1);

//注意這裡有時區問題,用now就要減掉8個小時
DateTime realNow = (new DateTime(2007, 9, 20, 12, 0, 0)).AddHours(-8);

//jsTimeStick即等於用JavaScript Date物件的getTime()方法跑出來的值
long jsTimeStick = (realNow.Ticks - timeStamp.Ticks) / 10000;

2007年9月17日 星期一

Windows Scripting - 簡易資料備份

今天收到命令,要做定時的備份工作,備份的目的地要在遠端的網路磁碟機上

掛載、卸載網路磁碟機

因此就去查詢如何在 Windows 上寫 script 掛載網路磁碟機,以下找到一段語法:
WshNetwork.MapNetworkDrive
maps a remote drive onto a local drive letter

Syntax:
WshNetwork.MapNetworkDrive (strLocalName, strRemoteName [,bUpdateProfile] [,strUser] [,strPassword])
strLocalName
Receives the drive letter to which to map the remtoe share point in strRemoteName.
strRemoteName
Receives the name ofd the remote share point that will be mapped to the drive letter in strLocalName.
bUpdateProfile
If this optional parameter is True, the mapping will be saved in the current user's profile.
strUser
When combined with strPassword, this parameter can be used to conenct to a remote drive using someone else's credentials.
strPassword
When combined with strUser, this parameter can be used to conenct to a remote drive using someone else's credentials.
接著是卸載網路磁碟機的語法:
WshNetwork.RemoveNetworkDrive
removes a previously mapped network drive

Syntax:
WshNetwork.RemoveNetworkDrive (strName, [,bForce] [,bUpdateProfile])
strName
Receives the name of a previously mapped share point to be removed.
bForce
If this optional parameter is set to True, then this method will remove the connection even if the resource is currently being used.
bUpdateProfile
If this optional parameter is set to True, the mapping will be removed from the current user's profile.

以下是他的 sample code:
Dim objNetwork
Set objNetwork = WScript.CreateObject("WScript.Network")
strLocalDrive = "H:"
strRemoteShare = "\\myserver\users"
objNetwork.MapNetworkDrive strLocalDrive, strRemoteShare, False '沒有第四、五個參數,表示不需帳號密碼
objNetwork.RemoveNetworkDrive strLocalDrive '卸載網路磁碟機

資料複製

另外還要將本地資料複製到網路磁碟機上,可用以下一段簡單的語法達成:(也是範例語法)
set fs = CreateObject("Scripting.FileSystemObject")

'將 c:\win98 中所有的內容複製到 z:\backup (不包含 c:\win98 資料夾)
fs.copyFolder "c:\win98","z:\win99"

'將 c:\win98 中所有的內容複製到 z:\backup (包含 c:\win98 資料夾)
fs.copyFolder "c:\win98","z:\win99\"

參考資料

  1. WSH >> Objects >> wshnetwork
  2. WSH >> wshnetwork >> MapNetworkDrive
  3. WSH >> wshnetwork >> RemoveNetworkDrive
  4. WshNetwork.MapNetworkDrive
  5. 資安論壇 - 求助!有時需要reboot或re-log才看見net use的network drive
  6. Windows Script 介紹 - 檔案及資料夾的處理

在 Ubuntu 中建置 mono 開發環境

在 Ubuntu 中建置 mono 的開發環境相當簡單,需要以下三個要素:
  1. mono (Runtime)
  2. mono-gmcs (compiler)
  3. monodevelop (IDE develop tool)
所以下達以下指令通通解決:
shell> sudo apt-get -y install mono monodevelop mono-gmcs
接著就可以使用 monodevelop 來開發 .NET 程式囉!

2007年9月6日 星期四

SAMBA + FTP 在 SELinux 的調整方式

要針對同一個目錄,讓可以透過 SAMBA 連入! 也可讓 FTP 連入!

在 SELinux 開啟的情況下,要將該目錄的 security context 設定為 「samba_share_t」

如此一來當權限設定好之後,就可以正常的透過服務存取目錄的資料了!

2007年9月4日 星期二

Linux SATA HowTo

關於 Linux 對於 SATA chipset 的支援,隨著 kernel 版本的持續更新,以及社群的強大協助開發下,陸陸續續將許多廠商所生產的 SATA chipset driver 都已經加入,因此在目前比較新的 Linux distribution 如 Fedora 7、Ubuntu 7.04、OpenSUSE 10.2、Mandriva Free 2007.....等,對於 SATA 的支援度都已經相當的好。

此外,若想要清楚了解目前 Linux 已經支援了哪些 SATA chipset,可以參考以下兩個網站,資料整理的相當詳細:

因此在購買硬體前,事先進行確認,可以避免掉後續許多硬體不支援的麻煩。

但若是因為一時不察,買到了內建目前各大 Linux distribution 都不支援的 SATA chiset 的產品(例如:主機板、SATA RAID Card .... 等等),雖然麻煩了點,但還是可以透過幾個方式解決:

1、進入 BIOS 設定為相容模式
在 BIOS 中尋找 SATA 的設定,假設在 IBM X60 上,可以找到 Config -> Serial ATA(SATA),設定為 Compatibility。

在其他不同廠牌的 BIOS 中,可能會有 Legacy ATA Mode 可供設定!

透過此種方式,BIOS 會將 SATA Device 視為 PATA 處理,當然效能會有所折損,不過可以在順利安裝 Linux 後,可以透過以下方式讓 Linux 支援 SATA 的裝置:
  1. 透過線上更新將 kernel 換成最新版的
    在 Fedora 中可以使用 yum;在 Ubuntu 中可以使用 apt。kernel 更新完成後,可能就支援原本不支援的 SATA 了,但也有可能一樣不支援!
  2. 重新編譯 kernel
    若是在 kernel 官方網站中,發現其實在新版的 kernel 中已經支援了該 SATA 裝置,就可以下載 kernel 的 source code 後,開啟 SATA 的支援並重新進行編譯,如此一來就可以正確驅動該 SATA chipset,接著置換原本的 kernel,就可以解決 SATA 裝置無法驅動的問題。而編譯 kernel 的方式可以參考此篇文章(鳥哥的 Linux 私房菜 - 核心編譯)。


2、使用廠商提供的 SATA chipset driver

若是很清楚 SATA chipset 是哪家廠商所生產的,並找到該廠商所提供的官方版 driver,就可以在安裝時,使用「linux dd」指令在安裝前載入 SATA chipset driver,如此一來安裝時,就可以正確的驅動 SATA device。

PS. 其實此種解決方式跟在 Windows 的模式是差不多的,在安裝 Windows 前若要載入 SATA driver,必須按 F6;只是在 Linux 的安裝中,必須下達指令「linux dd」。


結論
假設廠商沒有提供任何的 driver,線上更新 kernel 後也還是不支援,甚至在 Linux kernel 官方網站中都沒有提供支援該 SATA chipset 的計劃,就表示目前該 SATA chipset 在 Linux 是無法正常運作的,因此可能要重新購買其他已經支援的產品。


參考資訊

2007年9月3日 星期一

SQL Server 學習筆記 (1)

今天 K SQL server 的書,學到了幾個新的小技巧,因此作個記錄~

指令檢視 Table 詳細資訊

透過以下指令可以檢視 Table 的詳細資訊:
--指定 Database
USE studentPartTime
go
--查詢 Table 資訊
sp_help 'tbpwWork'
如此一來,table 的 schema、key、relation 都會很詳實的呈現出來,若是有 Date 相關欄位,還會有相對應的資訊顯示出來。


Index

除了 schema 設計的好壞之外,Index 的建置對於資料庫 performance tunning 是相當重要的一環,以下找到幾篇文章說明 index 設計時所必須考量的因素:
這幾篇看完應該會很清楚 index 應該要怎麼用了!

之後研究有其他更深入的心得,就再貼上來......


Set-Oriented (資料集導向)

在 RDBMS 中,最佳化都是針對 Data Set (資料集),若是用程序式的方式進行資料的操作,所得到的效能反而不好。

假設有個範例,要搜尋出住在各縣市的學生人數的統計資料!

若是用程序式的處理方式,就必須使用 cursor,並透過變數暫存數值,一筆一筆資料從頭掃瞄到尾,以便統計出完整資訊。

而用 Data Set 的處理方式,則是利用 select、count、group by 等關鍵字即可完成,遠遠比起使用 cursor 的方式來的輕鬆有效率,因此在 DML 的部分還是要多加鑽研才行!

2007年8月24日 星期五

XML 在 .NET 中的處理

今天偶然聽見可以用更快的方法處理 XML! (原本用的是 XMLDocument)

後來想想要怎麼樣可以讓程式處理 XML 的效率最好呢? 當然是 in-memory 的處理啦!

不然就像 DataReader 一樣,僅提供 forward-only traverse,當然我需要完整 DOM 的功能,forward-only 的方案(XMLTextReader)就完全不考慮啦!

接著,google 到幾篇覺得還蠻不錯的文章.....就留下來參考參考了

2007年8月14日 星期二

Offline Web Application

今天跟廠商開會,聽到了 Offline Web Application....

於是上網查了一些資料,原來這種機制是透過在 client 端放個小型資料庫達成的 (一般來說是 SQLite) .....

目前支援這種運作模式的工具有 Google Gears 、Adobe Integrated Runtime(AIR,前身為 Apollo)、Dojo Offline Toolkit .... 等等

看來這些東西未來會掀起一番革命....

有看到幾篇不錯的文章,先留下來作紀念.....

2007年8月13日 星期一

將 MP3 ID3 資訊轉為 Unicode 編碼

剛從 Windows 跳到 Linux 的使用者通常都會有這個問題...

為什麼聽 mp3 時,檔名都沒辦法正確顯示呢?

答案就是因為在 Linux 中的音樂播放軟體,顯示的不是歌曲的名稱,而是 ID3 資訊,不過通常 ID3 資訊都不是以 Unicode 編碼(因為 Windows 搞出來的 MP3 的 ID3 都不會是 Unicode 編碼),因此顯示出來都會是亂碼...

那.......要怎麼處理呢?

在 Windows 上,可以用 ConvertZ 去轉換! 這一套軟體很強,檔名可以轉,ID3 資訊也可以轉。

在 Linux 上,檔名可以用 iconv 轉換,而 ID3 資訊可以用 python-mutagen 套件轉換!

以下說明轉換 ID3 資訊的方式,假設 ID3 編碼為 Big5,只要進入要轉換的目錄,下達以下指令即可:
shell> find . -iname "*.mp3" -execdir mid3iconv -e Big5 {} ;
如此一來就解決 ID3 編碼非 Unicode 的問題囉!

PS. 還是 Unicode 比較好.......畢竟支援多國語言嘛!


參考資訊
  1. UbuntuHelp:ConvertingMP3Tags
  2. 子凌的linux筆記 - 將所有mp3的id3資訊轉為utf-8


Windows 2003 自動清除遠端 login session

一直以來都要手動去關閉遠端登入者的 session,現在知道怎麼讓系統在特定時間後自動清除囉! 感謝陳展今天提供這個有用的資訊!

設定方式如下:(For Windows 2003)
群組原則gpedit.msc
=> 電腦設定
=> 系統管理範本
=> windows元件
=> 終端機服務
=> 工作階段
=> 設定已斷線工作階段的時間限制
=> 啟用並限制時間

2007年8月12日 星期日

Java 學習筆記 (9) - Thread

現在 CPU 速度越來越快啦! 若是要更妥善利用他,希望他可以同時間多做一些事情,程式要怎麼寫呢?? 答案就是「執行緒(Thread)」。

Thread 基本入門

在 Java 中,可以透過繼承 java.lang.Thread class 或是實作 java.lang.Runnable interface 的方式,讓 object 具有多執行緒(Multi-Thread)的功能。

而實際上,CPU 並非真的實際"同時"作多項工作,而是在 processs 中的多個 thread 之間快速的轉換(代表一個 process 可以包含多個 thread),而由於轉換速度很快,因此就感覺向是同時在作多項工作般。

以下用個範例來說明:(使用繼承 java.lang.Thread 的方式)

EraseThread.java
public class EraseThread extends Thread {
private boolean active;
private String mask;


public EraseThread() {
this('*');
}

public EraseThread(char maskChar) {
active = true;
mask = "\010" + maskChar;
}

public void setActive(boolean active) {
this.active = active;
}

public boolean isActive() {
return active;
}

//重新定義 run 方法
public void run() {
while(isActive()) {
System.out.print(mask);
try {
// sleep 方法會丟出 InterruptedException 例外
Thread.currentThread().sleep(50);
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
EraseThreadDemo.java
import java.util.Scanner;

public class EraseThreadDemo {
public static void main(String args[]) {
Scanner scanner = new Scanner(System.in);

while(true) {
System.out.print("輸入名稱:");
String name = scanner.next();

System.out.print("輸入密碼:");
EraseThread thread = new EraseThread('#');
thread.start();
String passwd = scanner.next();
thread.setActive(false);

if(name.equals("godleon") && passwd.equals("1234"))
System.out.println("歡迎 godleon");
else
System.out.println(name + ",名稱或密碼錯誤,請重新輸入!");
}
}
}

另外還有一種方法可以讓 object 擁有 Multi-Thread 的能力,則是實作 java.lang.Runnable interface,並實作其中的 run() method,而程式碼的部分,跟上面則是大同小異,主要只是繼承 class 與實作 interface 的差異而已。

不過建議多使用實作 Runnable interface 的方式,如此一來可擁有 Multi-Thread 的功能,又保留了繼承其他 class 的彈性。

Daemon

除了 main thread 之外,還可以運用多個 thread 在背景進行其他工作,此種 thread 即稱為 daemon。

例如 Linux 中許多 listen tcp port 的服務即為 daemon,而一般來說,當 main thread 結束後,則其他附屬於 main thread 中的 daemon 也應該會自動終止,以下用一個簡單程式來示範:
public class DaemonThread {
public static void main(String args[]) {
Thread thread = new Thread(new Runnable() {
public void run() {
while(true)
System.out.print("T");
}
});

//必須使用 setDaemon() method 將 thread 設定為 daemon
//否則 main thread 結束後,此 thread 還是會繼續執行
thread.setDaemon(true);
thread.start();

執行緒生命週期

thread 的狀態有四種,分別是:(如下圖,節錄自 Java Gossip)
  1. New Thread (建立執行緒)
  2. Runnable
  3. Not Runnable
  4. Dead

當 new 一個 thread 物件並執行 start() method 後,會進入 Runnable 狀態,而 JVM 中會有一個 scheduler 專門負責處理所有狀態為 Runnable 的 thread 排程,因此即使狀態是 Runnable 的 thread,也必須要被排入執行才會真的執行 run() method 中的程式。而 thread 之間工作切換的速度很快,因此看來就像在同時執行一般。

而 thread 執行時也有其優先順序,可以利用 setPriority() method 去進行調整(1~10),數值越大優先權越高。

其中比較需要注意的是 yield() method,這是在不支援 Timeslicing 的 OS 上撰寫 multi-thread 程式才需要用到的 method,可讓目前執行中的 thread 暫停,去執行另外一個 thread 的工作;而當 thread 使用 yield() 讓出執行權時,他會再度回到 Runnable 的狀態等待 scheduler 的排程。

接著,如上圖所示,有幾個狀況會讓 thread 進入 Not Runnable 的狀態:
  1. 等待 I/O 完成
  2. 呼叫 sleep() 、wait() ....等方法
  3. 嘗試取得物件鎖定

會從 Runnable 轉換為 Not Runnable 的情形大多是發生在 thread 等待使用者的文字輸入、傾聽網路連線.....等情況時,而若是發生以下情形,則 thead 會再度回到 Runnable 的狀態:
  1. 完成 I/O 的動作
  2. 呼叫 interrupt() method()
  3. 呼叫 notify()、notifyAll() .... 等 method
  4. 取得物件鎖定

最後,當工作完成或發生例外,而離開了 run() method,則進入 Dead 狀態,可透過 isAlive() method 來測試 thread 是否還存活。

以下用一個簡單範例來說明:
public class InterruptDemo {
public static void main(String args[]) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
//使用 sleep(),進入 Not Runnable 狀態
Thread.sleep(99999);
}
catch(InterruptedException e) {
System.out.println("I'm interrupted !!");
}
}
});

thread.start();
//使用 interrupt(),回到 Runnable 狀態
//但會丟出 InterruptedException 例外
thread.interrupt();
}
}
若要暫停 thread 的執行,但時間不確定是多久,使用 sleep() 就不是一個好方式了,此時可以用 wait() 讓 thread 進入 Not Runnable 的狀態,並透過別的 thread 使用 notify() or notifyAll() 讓 thread 的狀態回到 Runnable。

執行緒的加入

若 Thread A 正在執行,臨時需要加入 Thread B,並優先處理,此時可以用 join() method 來處理此種情況。以下用個範例程式來說明:
public class ThreadA {
public static void main(String args[]) {
System.out.println("Thread A 執行");

Thread threadB = new Thread(new Runnable() {
public void run() {
try {
System.out.println("Thread B 開始");
for(int i = 0 ; i < 5 ; i++) {
Thread.sleep(1000);
System.out.println("Thread B 執行.......");
}
System.out.println("Thread B 即將結束");
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
});

threadB.start();

try {
threadB.join(); //使用 join()
}
catch(InterruptedException e) {
e.printStackTrace();
}

//threadB 透過 join() 加入,因此必須等 threadB 執行完後才會執行此行
//若沒用 join(),很快就會執行到這邊
System.out.println("Thread A 執行");
}
}

執行緒的停止

在 J2SE 6.0 API 中,Thread class 中的 stop()、suspend()、resume() ....等 method,分別都被註記為「Deprecated」,因此表示使用者若需要這些功能,就必須自己實作囉!

以 stop 來說,若需要讓 thread 停止,就必須要想辦法讓 thread 完成 run() 中的流程,以下用個程式作範例:
public class SomeThread implements Runnable {
public static void main(String args[]) {
Thread thread = new Thread(new SomeThread());
thread.start();
//透過 interrupt() 丟出 InterruptedException 例外
//讓 run() 順利結束
thread.interrupt();
}

public void run() {
System.out.println("sleep......至 block 狀態");
try {
Thread.sleep(9999);
}
catch(InterruptedException e) {
System.out.println("I am interrupted");
}
}

ThreadGroup

每一個 thread 都隸屬於某一個 ThreadGroup,若產生的 thread 沒有指定 ThreadGroup,則此 thread 則屬於產生它的 thread (一般來說是 main thread)。而必須注意的是,當 thread 歸入某個 ThreadGroup 後,就無法再更換到其他 ThreadGroup 了!

而透過 java.lang.ThreadGroup,可以管理整個 group 中的 thread,例如 interrupt 群組中所有的 thread,或是設定優先權....等等,詳細的操作方式可以參考官方文件。

此外,若在 ThreadGroup 中有 thread 發生了例外,可透過實作 Thread.UncaughtExceptionHandler 的方式來進行處理,以下用一段簡單的程式碼來介紹:

ThreadExceptionHandler.java
public class ThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " : " + e.getMessage());
}
}
ThreadGroupDemo.java
public class ThreadGroupDemo {
public static void main(String args[]) {
ThreadExceptionHandler handler = new ThreadExceptionHandler();
ThreadGroup group1 = new ThreadGroup("group1");

//指定 thread 屬於 group1
Thread thread1 = new Thread(group1, new Runnable() {
public void run() {
//丟出例外由指定的 Handler 處理
//但也同時結束了 thread
throw new RuntimeException("測試例外");
}
});

thread1.setUncaughtExceptionHandler(handler); //設定例外處理的 Hanlder
thread1.start();
}
}


同步(Synchronized)

執行緒同步

當有一個物件,同時可能有多個 thread 進行存取時,就會可能有資料不同步的情況發生,此時在寫程式時就必須考慮到這個問題並加以解決,以下用一個範例程式來說明:

PersonalInfo.java
public class PersonalInfo {
private String name;
private String id;
private int count;

public PersonalInfo() {
name = "nobody";
id = "N/A";
}

public synchronized void setNameAndID(String name, String id) {
this.name = name;
this.id = id;

if(!checkNameAndIDEqual())
System.out.println(count + ") illegal name or ID.....");

count++;
}

private boolean checkNameAndIDEqual() {
return (name.charAt(0) == id.charAt(0)) ? true : false;
}
}
PersonalInfoTest.java
public class PersonalInfoTest {
public static void main(String args[]) {
final PersonalInfo person = new PersonalInfo();

Thread t1 = new Thread(new Runnable() {
public void run() {
while(true)
person.setNameAndID("Tzeng Chien Ming", "TCM");
}
});

Thread t2 = new Thread(new Runnable() {
public void run() {
while(true)
person.setNameAndID("Leon Tzeng", "LT");
}
});

//這邊多跑幾次就會出錯了
//但若在 method 前加入「synchronized」關鍵字就可以將物件鎖定
System.out.println("開始測試....");
t1.start();
t2.start();
}
}
在這邊,所使用的關鍵字就是「synchronized」,透過此關鍵字,就可以將 PersonalInfo 物件鎖定,確保不會有同時有多個 thread 執行使用關鍵字 synchronized 設定過的 setNameAndID() method。

此外,synchronized 關鍵字不僅可以用於於 method,也可以用在任何程式片段中,有興趣的人就自行 google 囉!

wait()、notify()、notifyAll()

首先先來一張圖(節錄自 JavaWorld@TW):

從圖中可以看出,wait()、notify()、notifyAll() 三個 method 都是從 java.lang.Object class 繼承下來的,而這幾個 method 在定義時已經使用了 final 關鍵字,因此也沒辦法 override 囉!

首先,先說明 wait() 的部分:

若要使用 wait(),必須是在有標註 synchronized 的 method 中才可以呼叫此方法,而當 wait() 被呼叫後,目前 thread 的狀態會從 Running 變成 Waiting。

接著是 notify() 與 notifyAll() 的部分:

使用 notify()、notifyAll() 則會讓 thread 從 Waiting 狀態回到 ready-to-run;但其中的差異在於,notify() 僅會一次影響 1 個 thread,而 notifyAll() 則是一次將 Waiting 狀態的 thread 通通變成 ready-to-run 的狀態。

因此,簡單來說,當 thread 呼叫 object 的 wait() method 時,表示 thread 必須讓出物件的鎖定並等待通知,直到再被通知(notify or notifyAll)時,就會回到 ready-to-run 的狀態與其他 thread 一同競爭物件的鎖定。

以下用一個範例來說明:

Clerk.java
public class Clerk {
//-1 表示無商品
private int product = -1;

public synchronized void setProduct(int product) {
if(this.product != -1) {
try {
//目前店員沒有空間收產品,請稍後
wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}

this.product = product;
System.out.println("生產者設定 " + this.product);

//通知等待中的消費者可以來消費了!
notify();
}

public synchronized int getProduct() {
if(this.product == -1) {
try {
wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}

int p = this.product;
System.out.println("消費者取走 " + this.product);
this.product = -1;

//通知生產者工作
notify();

return p;
}
}
Producer.java
public class Producer implements Runnable {
private Clerk clerk;

public Producer(Clerk clerk) {
this.clerk = clerk;
}

public void run() {
System.out.println("生產者開始生產整數......");

for(int product = 1 ; product <= 10 ; product++) {
try {
//暫停隨機時間
Thread.sleep((int)(Math.random() * 3000));
}
catch(InterruptedException e) {
e.printStackTrace();
}

//將產品交給店員
clerk.setProduct(product);
}
}
}
Consumer.java
public class Consumer implements Runnable {
private Clerk clerk;

public Consumer(Clerk clerk) {
this.clerk = clerk;
}

public void run() {
System.out.println("消費者開始消耗整數......");
for(int i = 1 ; i <= 10 ; i++) {
try {
//等待隨機時間
Thread.sleep((int)Math.random() * 3000);
}
catch(InterruptedException e) {
e.printStackTrace();
}

//從店員處取走整數
clerk.getProduct();
}
}
}
ProductTest.java
public class ProductTest {
public static void main(String args[]) {
Clerk clerk = new Clerk();

//生產者 thread
Thread producerThread = new Thread(new Producer(clerk));

//消費者 thread
Thread ConsumerThread = new Thread(new Consumer(clerk));

producerThread.start();
ConsumerThread.start();
}
}
以上這些程式碼,其實也只是兩個 thread 狀態的處理而已,因此比較簡單;而若是 thread 數量又更多且必須有資料同步的考量時,狀態的處理就必須要更小心了。

資源共用的考量

基本上,要撰寫 multi-thread 且安全的程式是很不容易的,因為有共用資源的問題,為了讓資源共用不產生衝突,必須作很多額外的檢查工作以及 thread 的狀態轉換。

而若是從另外一種角度來思考,每一個 thread 都擁有自己的資源,那程式開發難度相對就減少許多了!

而在 J2SE 中提供了 java.lang.ThreadLocal class,就是用來實現這個概念。

而這個部分,就下次有用到的時候再來探討好了............有興趣的人就自己網路找文件看吧!


java.util.concurrent package

在 J2SE 5.0 以後,新增了 java.util.concurrent package,裡面提供了許多 class,讓撰寫 multi-thread 的程式更為方便,而這個部分較為深入,這邊就不探討了,以後如果有需要使用到這一塊,再來慢慢研究....


參考資料
  1. 同步化Thread
  2. Thread 淺介
  3. 多執行緒: Multi-Threading

2007年8月8日 星期三

[AWStats] IIS Log Analyzer 拾遺補缺

之前用了這套去處理 ADOC 的網頁流量分析....發現其實所呈現出來的訊息還蠻詳細的!

不過測試到後來有發現幾個問題,不過有逐一解決了...

無法 parse 之前的 log file

這個問題聽起來似乎很可怕.....其實還好...

假設在 awstats.testSite.conf 設定檔中,節錄設定如下:
# 指定所要分析的 log 檔案
LogFile="D:/webLog/W3SVC1/ex%YY-0%MM-0%DD-0.log"

# 分析結果(summary)檔案存放的位置
DirData="C:/Inetpub/wwwroot/awstats_data"

假設只透過以下指令:
perl awstats.pl -config=testSite -update
則僅僅是是分析當天的 log 檔案,而假設當天為 2007/08/08,那分析完該檔案後,若要分析該日之前的 log 檔案,則會被認為是 old 的資料而不會將分析結果加入 summary 檔案中。

假設我們要從八月初開始到今日的所有 log 分析,就必須做兩個動作:
  1. 刪除 summary 檔案
  2. 再執行以下指令:
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070801.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070802.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070803.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070804.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070805.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070806.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070807.log" -update
perl awstats.pl -config=testSite -LogFile="D:/webLog/W3SVC1/ex20070808.log" -update
當然,也可以寫個批次檔(.bat)去跑,這樣就省事多了。

【註】summary 檔案是以一個月為單位作儲存。


程式不會自動每日分析 log

這個部分真是始料未及,原本以為他會自己做,到後來國貿局的小姐發現了跟我說,我才知道這回事.....真是汗顏....@_@

解決的方式很容易,就只要設定工作排程,每日的 23:59 讓他分析 log 就好了,不過程式執行的路徑要完整指定清楚就是了!

以下給個範例:
C:\Perl\bin\perl.exe D:\myweb\log\cgi-bin\awstats.pl -config=myweb -update
記得要加入例行性的工作囉!