2014/01/07

Android SQLite Version Update

Android SQLite 是 Android 提供的簡易型 DataBase,相信對於有在開發 android app 的朋友們一定不陌生。
以前在開發 android app 時,只要 DB 欄位更新,就必須將 app 重新安裝,但在上架後呢?總不可能請使用者移除在安裝吧!因此 android 提供了一個 SQLite 的更新方式。下面就來介紹一下關於 SQLite 的版本更新。(本章節不討論如何建立 SQLite )
首先,您一定會有一個繼承 SQLiteOpenHelper 的物件,這裡暫時稱為 DBHelper,在此物間中必須有一個 DATABASE_VERSION 的 field,這個 field 將記錄您當前 SQLite 的版本號。
在您呼叫 super(context, DB_NAME, null, DATABASE_VERSION); 時必須將此版本號帶入。系統會根據此版本號判斷 DB 是否為新版本,在做相對應的更新。
接著,就來介紹一下 DBHelper 的內容:

DBHelper 幾個 method 介紹

  • onCreate()DB 尚未建立時執行,只會執行一次
  • onUpgrade() DB version 升版時執行
  • onDowngrade() DB version 降版時執行
  • getReadableDatabase() 建立或開啟 DB ,呼叫後才會執行 onUpgrade()onDowngrade()
  • getWritableDatabase () 建立或開啟一個可供讀寫的 DB ,執行較 getReadableDatabase() 久,呼叫後才會執行 onUpgrade()onDowngrade()

SQLite 基本更新流程

  1. init 時,呼叫 super(context, DB_NAME, null, DATABASE_VERSION); 帶入當前 version
  2. 呼叫 getReadableDatabase()getWritableDatabase ()
  3. 系統發現 version 有異動後,會自動呼叫 onUpgrade()onDowngrade() 更新 DB

Code 解說

init

public DBHelper(Context context) {
 super(context, DB_NAME, null, DATABASE_VERSION);  //呼叫父類別並將當前 version 帶入
 mContext = context;

 File cache = mContext.getCacheDir();
 File appDir = new File(cache.getParent());
 dbPath = appDir.getAbsolutePath() + "/databases/";

}
createDatabase

public void createDatabase() throws IOException {
 boolean dbExist = checkDataBase();
 if (dbExist) {
  // do nothing - database already exist
  SQLiteDatabase sqlitedb = this.getReadableDatabase();
  
  Log.i("ruby", "createDatabase Version="+sqlitedb.getVersion());
  //onUpgrade或onDowngrade寫入DB錯誤時要把版本號改回舊的版本,不然會有一版沒更新到
  if(!upgradeSuccess || !downSuccess){
   Log.i("ruby", "升版或降版失敗 oldVersion=" + oldVersion);
   sqlitedb.setVersion(oldVersion);
  }
  sqlitedb.close();
 } else {
  // By calling this method and empty database will be created into
  // the default system path
  // of your application so we are gonna be able to overwrite that
  // database with our database.
  SQLiteDatabase sqlitedb = this.getReadableDatabase();  //再dbPath下建立SQLiteDatabase(.sqlite跟.journal),路徑folder不存在會自動建立

  try {
   sqlitedb.close();
  } catch (Exception e) {
  }
 }
}
DB 升版

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 Log.i("ruby", "====================================================================================================");
 Log.i("ruby", "onUpgrade oldVersion="+oldVersion+" newVersion="+newVersion+" DATABASE_VERSION="+DATABASE_VERSION);
 this.oldVersion = oldVersion;
 
 //*********************************************************************************************************************
 //不會執行...
 if(oldVersion < 1) {
  Log.i("ruby", "oldVersion < 1");
  db.execSQL("DROP TABLE IF EXISTS profile");  //刪除profile table
 }
 //*********************************************************************************************************************
 
 if(oldVersion < 2){
  try {
   Log.i("ruby", "oldVersion < 2");
   db.execSQL("DROP TABLE IF EXISTS profile");  //刪除table1 table
   db.execSQL("CREATE TABLE profile (id INTEGER PRIMARY KEY  NOT NULL , name VARCHAR NOT NULL , addr VARCHAR, phone VARCHAR, email VARCHAR)");
  }catch(SQLiteException e) {
   Log.i("ruby", "oldVersion < 2 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS profile");  //新增失敗,刪除table1 table
   upgradeSuccess = false;  //設定之後DB準備降回舊版,onUpgrade後android會自動setVersion(newVersion)
  }
 }
 if(oldVersion < 3) {
  try {
   Log.i("ruby", "oldVersion < 3");
   db.execSQL("DROP TABLE IF EXISTS table1");  //刪除table1 table
   db.execSQL("Create TABLE table1 (First_Name char(50), Last_Name char(50), Address char(50), City char(50), Country char(25), Birth_Date datetime)");
  }catch(SQLiteException e) {
   Log.i("ruby", "oldVersion < 3 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS table1");  //新增失敗,刪除table1 table
   upgradeSuccess = false;  //設定之後DB準備降回舊版,onUpgrade後android會自動setVersion(newVersion)
  }
 }
 if(oldVersion < 4) {
  try {
   Log.i("ruby", "oldVersion < 4");
   db.execSQL("DROP TABLE IF EXISTS table2");  //刪除table2 table
   db.execSQL("Create TABLE table2 (Company char(50), Phone char(50), co_addr char(50))");
  }catch(SQLiteException e) {
   Log.i("ruby", "oldVersion < 4 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS table2");  //新增失敗,刪除table2 table
   upgradeSuccess = false;  //設定之後DB準備降回舊版,onUpgrade後android會自動setVersion(newVersion)
  }
 }
 if(oldVersion < 5) {
  try {
   Log.i("ruby", "oldVersion < 5");
   db.execSQL("DROP TABLE IF EXISTS table3");  //刪除table3 table
   db.execSQL("Create TABLE table3 (Company char(50), Phone char(50), co_addr char(50))");
  }catch(SQLiteException e) {
   Log.i("ruby", "oldVersion < 5 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS table3");  //新增失敗,刪除table3 table
   upgradeSuccess = false;  //設定之後DB準備降回舊版,onUpgrade後android會自動setVersion(newVersion)
  }
 }
 newExecSQL(db);
}

private void newExecSQL(SQLiteDatabase db){
 Log.i("ruby", "newExecSQL");
 //add DB新版本語法
}
(為方便測試每個 version 更新前會先刪除 table,避免出現Exception)
DB 降版

/**
 * API level 11
 * @see android.database.sqlite.SQLiteOpenHelper#onDowngrade(android.database.sqlite.SQLiteDatabase, int, int)
 */
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 Log.i("ruby", "====================================================================================================");
 Log.i("ruby", "onDowngrade oldVersion="+oldVersion+" newVersion="+newVersion+" DATABASE_VERSION="+DATABASE_VERSION);
 this.oldVersion = oldVersion;
 
 if(newVersion < 2){
  try {
   Log.i("ruby", "newVersion < 2");
   db.execSQL("DROP TABLE IF EXISTS profile");  //刪除table1 table
   db.execSQL("CREATE TABLE profile (id INTEGER PRIMARY KEY  NOT NULL , name VARCHAR NOT NULL , addr VARCHAR, phone VARCHAR, email VARCHAR)");
  }catch(SQLiteException e) {
   Log.i("ruby", "newVersion < 2 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS profile");  //新增失敗,刪除table1 table
   downSuccess = false;  //設定之後DB準備降回舊版,onDowngrade後android會自動setVersion(newVersion)
  }
 }
 if(newVersion < 3) {
  try {
   Log.i("ruby", "newVersion < 3");
   db.execSQL("DROP TABLE IF EXISTS table1");  //刪除table1 table
   db.execSQL("Create TABLE table1 (First_Name char(50), Last_Name char(50), Address char(50), City char(50), Country char(25), Birth_Date datetime)");
  }catch(SQLiteException e) {
   Log.i("ruby", "newVersion < 3 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS table1");  //新增失敗,刪除table1 table
   downSuccess = false;  //設定之後DB準備降回舊版,onDowngrade後android會自動setVersion(newVersion)
  }
 }
 if(newVersion < 4) {
  try {
   Log.i("ruby", "newVersion < 4");
   db.execSQL("DROP TABLE IF EXISTS table2");  //刪除table2 table
   db.execSQL("Create TABLE table2 (Company char(50), Phone char(50), co_addr char(50))");
  }catch(SQLiteException e) {
   Log.i("ruby", "newVersion < 4 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS table2");  //新增失敗,刪除table2 table
   downSuccess = false;  //設定之後DB準備降回舊版,onDowngrade後android會自動setVersion(newVersion)
  }
 }
 if(newVersion < 5) {
  try {
   Log.i("ruby", "newVersion < 5");
   db.execSQL("DROP TABLE IF EXISTS table3");  //刪除table3 table
   db.execSQL("Create TABLE table3 (Company char(50), Phone char(50), co_addr char(50))");
  }catch(SQLiteException e) {
   Log.i("ruby", "newVersion < 5 Upgrade fail... maybe a crappy rom...\n", e);
   db.execSQL("DROP TABLE IF EXISTS table3");  //新增失敗,刪除table3 table
   downSuccess = false;  //設定之後DB準備降回舊版,onDowngrade後android會自動setVersion(newVersion)
  }
 }
}
(為方便測試每個 version 更新前會先刪除 table,避免出現Exception)

注意事項

  • 呼叫 getReadableDatabase()getWritableDatabase () 後必須關閉 DB 連線,可使用 sqlitedb.close();
  • 執行 onUpgrade()onDowngrade() 時,若 DB 更新失敗必須將 DB version 設為舊的 version,避免有某個版本沒有更新到,可用 sqlitedb.setVersion(oldVersion);
  • DB 必須用 db.execSQL() 等語法建立,不可用 assets/XXX.sqlite 的 SQL 複製出

SQLiteOpenHelper.getWritableDatabase() 源碼片斷


int version = db.getVersion();
if (version != mNewVersion) {
    db.beginTransaction();
    try {
        if (version == 0) {
            onCreate(db);
        } else {
            if (version > mNewVersion) {
                onDowngrade(db, version, mNewVersion);
            } else {
                onUpgrade(db, version, mNewVersion);
            }
        }
        db.setVersion(mNewVersion);
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
}
onOpen(db);
由上可知,在呼叫 onUpgrade()onDowngrade() 後會將 DB version 設為新傳入的 mNewVersion,所以若在 onUpgrade()onDowngrade() 中更新 DB 失敗,建議重設 DB Version 避免有版本未更新到

測試結果

copy assets/test.sqlite 的 SQL

/*************************************************************************************************************************************/
/* copy assets/test.sqlite 的 SQL(順號升版)                                                                                          */
/*************************************************************************************************************************************/
01-06 17:01:10.722: I/ruby(7190): onCreate------------------------------------------------------>version:1
01-06 17:01:10.753: I/ruby(7190): onOpen-------------------------------------------------------->version:1
01-06 17:01:58.768: I/ruby(7254): onCreate------------------------------------------------------>version:2
01-06 17:01:58.776: I/ruby(7254): onOpen-------------------------------------------------------->version:2
01-06 17:01:58.780: I/ruby(7254): createDatabase Version=2-------------------------------------->version:2
01-06 17:02:13.042: I/ruby(7316): ====================================================================================================
01-06 17:02:13.042: I/ruby(7316): onUpgrade oldVersion=2 newVersion=3 DATABASE_VERSION=3-------->version:3
01-06 17:02:13.042: I/ruby(7316): oldVersion < 3------------------------------------------------>version:3
01-06 17:02:13.046: I/ruby(7316): oldVersion < 4------------------------------------------------>version:3
01-06 17:02:13.046: I/ruby(7316): oldVersion < 5------------------------------------------------>version:3
01-06 17:02:13.050: I/ruby(7316): newExecSQL---------------------------------------------------->version:3
01-06 17:02:13.089: I/ruby(7316): onOpen-------------------------------------------------------->version:3
01-06 17:02:13.089: I/ruby(7316): createDatabase Version=3-------------------------------------->version:3
01-06 17:02:52.229: I/ruby(7377): ====================================================================================================
01-06 17:02:52.229: I/ruby(7377): onUpgrade oldVersion=3 newVersion=4 DATABASE_VERSION=4-------->version:4
01-06 17:02:52.229: I/ruby(7377): oldVersion < 4------------------------------------------------>version:4
01-06 17:02:52.229: I/ruby(7377): oldVersion < 5------------------------------------------------>version:4
01-06 17:02:52.233: I/ruby(7377): newExecSQL---------------------------------------------------->version:4
01-06 17:02:52.265: I/ruby(7377): onOpen-------------------------------------------------------->version:4
01-06 17:02:52.265: I/ruby(7377): createDatabase Version=4-------------------------------------->version:4
execSQL 語法建立的 SQL

/*************************************************************************************************************************************/
/* execSQL 語法建立的 SQL(順號升版)                                                                                                  */
/*************************************************************************************************************************************/
01-06 17:11:56.530: I/ruby(7588): onCreate------------------------------------------------------>version:1
01-06 17:11:56.542: I/ruby(7588): onOpen-------------------------------------------------------->version:1
01-06 17:12:09.565: I/ruby(7649): ====================================================================================================
01-06 17:12:09.565: I/ruby(7649): onUpgrade oldVersion=1 newVersion=2 DATABASE_VERSION=2-------->version:2
01-06 17:12:09.565: I/ruby(7649): oldVersion < 2------------------------------------------------>version:2
01-06 17:12:09.569: I/ruby(7649): oldVersion < 3------------------------------------------------>version:2
01-06 17:12:09.569: I/ruby(7649): oldVersion < 4------------------------------------------------>version:2
01-06 17:12:09.573: I/ruby(7649): oldVersion < 5------------------------------------------------>version:2
01-06 17:12:09.577: I/ruby(7649): newExecSQL---------------------------------------------------->version:2
01-06 17:12:09.612: I/ruby(7649): onOpen-------------------------------------------------------->version:2
01-06 17:12:09.612: I/ruby(7649): createDatabase Version=2-------------------------------------->version:2
01-06 17:12:21.765: I/ruby(7710): ====================================================================================================
01-06 17:12:21.765: I/ruby(7710): onUpgrade oldVersion=2 newVersion=3 DATABASE_VERSION=3-------->version:3
01-06 17:12:21.768: I/ruby(7710): oldVersion < 3------------------------------------------------>version:3
01-06 17:12:21.768: I/ruby(7710): oldVersion < 4------------------------------------------------>version:3
01-06 17:12:21.772: I/ruby(7710): oldVersion < 5------------------------------------------------>version:3
01-06 17:12:21.776: I/ruby(7710): newExecSQL---------------------------------------------------->version:3
01-06 17:12:21.811: I/ruby(7710): onOpen-------------------------------------------------------->version:3
01-06 17:12:21.815: I/ruby(7710): createDatabase Version=3-------------------------------------->version:3
01-06 17:12:37.155: I/ruby(7772): ====================================================================================================
01-06 17:12:37.155: I/ruby(7772): onUpgrade oldVersion=3 newVersion=4 DATABASE_VERSION=4-------->version:4
01-06 17:12:37.159: I/ruby(7772): oldVersion < 4------------------------------------------------>version:4
01-06 17:12:37.159: I/ruby(7772): oldVersion < 5------------------------------------------------>version:4
01-06 17:12:37.163: I/ruby(7772): newExecSQL---------------------------------------------------->version:4
01-06 17:12:37.210: I/ruby(7772): onOpen-------------------------------------------------------->version:4
01-06 17:12:37.210: I/ruby(7772): createDatabase Version=4-------------------------------------->version:4
由上測試結果可看出,若 SQLite 是用 copy assets/xxx.sqlite 產生的 DB,在第一次版本更新時不會執行 onUpgrade()onDowngrade()
version 跳號升版

/*************************************************************************************************************************************/
/* version往上跳號測試(跳號升版)                                                                                                     */
/*************************************************************************************************************************************/
01-06 17:15:54.608: I/ruby(7973): onCreate------------------------------------------------------>version:1
01-06 17:15:54.624: I/ruby(7973): onOpen-------------------------------------------------------->version:1
01-06 17:16:10.718: I/ruby(8037): ====================================================================================================
01-06 17:16:10.718: I/ruby(8037): onUpgrade oldVersion=1 newVersion=3 DATABASE_VERSION=3-------->version:3
01-06 17:16:10.722: I/ruby(8037): oldVersion < 2------------------------------------------------>version:3
01-06 17:16:10.726: I/ruby(8037): oldVersion < 3------------------------------------------------>version:3
01-06 17:16:10.726: I/ruby(8037): oldVersion < 4------------------------------------------------>version:3
01-06 17:16:10.726: I/ruby(8037): oldVersion < 5------------------------------------------------>version:3
01-06 17:16:10.729: I/ruby(8037): newExecSQL---------------------------------------------------->version:3
01-06 17:16:10.796: I/ruby(8037): onOpen-------------------------------------------------------->version:3
01-06 17:16:10.800: I/ruby(8037): createDatabase Version=3-------------------------------------->version:3
01-06 17:16:28.800: I/ruby(8100): ====================================================================================================
01-06 17:16:28.800: I/ruby(8100): onUpgrade oldVersion=3 newVersion=7 DATABASE_VERSION=7-------->version:7
01-06 17:16:28.804: I/ruby(8100): oldVersion < 4------------------------------------------------>version:7
01-06 17:16:28.808: I/ruby(8100): oldVersion < 5------------------------------------------------>version:7
01-06 17:16:28.808: I/ruby(8100): newExecSQL---------------------------------------------------->version:7
01-06 17:16:28.874: I/ruby(8100): onOpen-------------------------------------------------------->version:7
01-06 17:16:28.878: I/ruby(8100): createDatabase Version=7-------------------------------------->version:7
version 順號降版

/*************************************************************************************************************************************/
/* version往下順號測試(順號降版)                                                                                                     */
/*************************************************************************************************************************************/
01-06 17:32:11.253: I/ruby(8454): onCreate------------------------------------------------------>version:1
01-06 17:32:11.284: I/ruby(8454): onOpen-------------------------------------------------------->version:1
01-06 17:32:35.495: I/ruby(8517): ====================================================================================================
01-06 17:32:35.495: I/ruby(8517): onDowngrade oldVersion=5 newVersion=4 DATABASE_VERSION=4------>version:4
01-06 17:32:35.499: I/ruby(8517): newVersion < 5------------------------------------------------>version:4
01-06 17:32:35.526: I/ruby(8517): onOpen-------------------------------------------------------->version:4
01-06 17:32:35.530: I/ruby(8517): createDatabase Version=4-------------------------------------->version:4
01-06 17:32:53.999: I/ruby(8578): ====================================================================================================
01-06 17:32:53.999: I/ruby(8578): onDowngrade oldVersion=4 newVersion=3 DATABASE_VERSION=3------>version:3
01-06 17:32:53.999: I/ruby(8578): newVersion < 4------------------------------------------------>version:3
01-06 17:32:54.003: I/ruby(8578): newVersion < 5------------------------------------------------>version:3
01-06 17:32:54.050: I/ruby(8578): onOpen-------------------------------------------------------->version:3
01-06 17:32:54.054: I/ruby(8578): createDatabase Version=3-------------------------------------->version:3
01-06 17:33:07.343: I/ruby(8640): ====================================================================================================
01-06 17:33:07.343: I/ruby(8640): onDowngrade oldVersion=3 newVersion=2 DATABASE_VERSION=2------>version:2
01-06 17:33:07.343: I/ruby(8640): newVersion < 3------------------------------------------------>version:2
01-06 17:33:07.347: I/ruby(8640): newVersion < 4------------------------------------------------>version:2
01-06 17:33:07.347: I/ruby(8640): newVersion < 5------------------------------------------------>version:2
01-06 17:33:07.417: I/ruby(8640): onOpen-------------------------------------------------------->version:2
01-06 17:33:07.417: I/ruby(8640): createDatabase Version=2-------------------------------------->version:2
version 跳號降版

/*************************************************************************************************************************************/
/* version往下跳號測試(跳號降版)                                                                                                     */
/*************************************************************************************************************************************/
01-06 17:36:54.026: I/ruby(8707): onCreate------------------------------------------------------>version:5
01-06 17:36:54.034: I/ruby(8707): onOpen-------------------------------------------------------->version:5
01-06 17:37:20.085: I/ruby(8771): ====================================================================================================
01-06 17:37:20.089: I/ruby(8771): onDowngrade oldVersion=5 newVersion=3 DATABASE_VERSION=3------>version:3
01-06 17:37:20.089: I/ruby(8771): newVersion < 4------------------------------------------------>version:3
01-06 17:37:20.089: I/ruby(8771): newVersion < 5------------------------------------------------>version:3
01-06 17:37:20.132: I/ruby(8771): onOpen-------------------------------------------------------->version:3
01-06 17:37:20.136: I/ruby(8771): createDatabase Version=3-------------------------------------->version:3
01-06 17:37:33.558: I/ruby(8833): ====================================================================================================
01-06 17:37:33.561: I/ruby(8833): onDowngrade oldVersion=3 newVersion=1 DATABASE_VERSION=1------>version:1
01-06 17:37:33.561: I/ruby(8833): newVersion < 2------------------------------------------------>version:1
01-06 17:37:33.565: I/ruby(8833): newVersion < 3------------------------------------------------>version:1
01-06 17:37:33.569: I/ruby(8833): newVersion < 4------------------------------------------------>version:1
01-06 17:37:33.569: I/ruby(8833): newVersion < 5------------------------------------------------>version:1
01-06 17:37:33.644: I/ruby(8833): onOpen-------------------------------------------------------->version:1
01-06 17:37:33.647: I/ruby(8833): createDatabase Version=1-------------------------------------->version:1
由上述測試結果可看出,execSQL 語法建立的 SQL,version 不管順號還是跳號都可以正常更新 DB

References

範例下載

2014/01/06

Singleton的雙重檢查鎖與volatile

前言:

幾年前從踏進java開始,熟悉了物件導向之後,偶然從朋友那邊聽到了一個詞,Design Pattern,因此買了本歐萊禮的Headfirst Design Pattern來看,其中對於Singleton這Pattern一直有特別印象,因為這Pattern在系統設計時,放置系統設定檔案資料時很常用,也是一個好入門的Pattern,只是對於書上的雙重檢查鎖範例,一直沒有去理解它,趁最近比較有空時,花點時間去搞懂它。

Singleton雙重檢查鎖技巧:

先看一下書上的範例:

public class Singleton {
    private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) {
            synchronized(Singleton.class) {
                if(uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

這毫無疑問的是個Singleton的Pattern,建構子設為private,因此不能用new Singleton()的方式來建立instance,而其他物件要來取此物件的instance,也只能透過它對外開放的getInstance()來取得;為了講求效能,因此只有在uniqueInstance尚未初始化,也就是為null時,才會用synchronized來避免多執行緒造成new過多物件的問題發生,而如果它已經初始化過了,則直接回傳物件回去。

看起來是個很簡單很好懂的範例,但這範例裡面有個讓我困惑很久的地方就是,為什麼要在uniqueInstance這個field上加個volatile的關鍵字?

關鍵字volatile:

一開始我對valotitle的理解大概是這樣。

如果我今天宣告了一個宣告了一個long變數為volatile,如下

public volatile long value = 0l;

則其語意會相等於(注意是語意,不是底層真正的實作方式):

public long value = 0l;
public synchronized void set(long v) {
    value = v;
}

public synchronized long get() {
    return value;
}

也就是說,把一個有可能會被中斷的操作,變為不可中斷的操作,看起來似乎不加關鍵字volatile是對Singleton沒啥影響,那為何還要加呢?於是我花了一些時間尋找了幾篇相關討論的文章。

問題1,物件可能尚未初始化完成:

在這個網站The "Double-Checked Locking is Broken" Declaration提到,在不同系統下的compiles,可能會對於指令的處理有所不同,以下是網站裡提到的測試程式片段:

singletons[i].reference = new Singleton();

網站裡提到,Paul Jakubik發現把這程式碼片段程式在Symantec JIT執行過後,它會先對物件做memory allocate,然後才去執行建構子

這會造成什麼問題呢?讓我們在回過頭來看Singleton的程式碼:

public static Singleton getInstance() {
    if(uniqueInstance == null) {
        synchronized(Singleton.class) {
            if(uniqueInstance == null) {
                uniqueInstance = new Singleton();
            }
        }
    }
    return uniqueInstance;
}

上面問題在於,一開始我們判斷物件是不是為null,null代表物件尚未初始化過,接著我們而在Symantec JIT的情況下,會先做memory allocate,在去執行建構子,因此可能有某個Thread正在對物件做初始化,做到一半被中斷了,而第二個物件呼叫了getInstance(),然後發現該物件不為null,因此馬上就回傳回去,因此它就得到了一個尚未初始化完成的物件,這有可能會造成系統錯誤。

問題2,編譯器優化:

接著另一本書提到的Effective Java (2nd Edition)裡面的 Item66 Synchronize access to shared mutable data 提到的範例,編譯器可能會對你的程式碼進行優化,如以下的程式碼:

public class HoistingTestClass {
    private static boolean stopRequested;

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(!stopRequested)
                    i++;
            }
        });

        t.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            stopRequested = true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

程式裡面,宣告一個Thread,並且先讓它執行,Thread裡面只做一件事,就是判斷stopRequested這個flag,如果為false,就一直執行下去,而主thread會在停止1秒之後,把stopRequested這個flag設為true。預期中,這程式應該是會跑一秒之後就停止了,不過實際卻是無窮迴圈。原因在於jvm對它做了優化處理。所以原本code是:

while(!stopRequested)
    i++;

在jvm執行時會被處理成:

if(!stopRequested)
    while(true)
        i++;

因此會和預期中的不一樣。而這問題要怎麼處理,只要將變數加上volatile則一切就正常了。

private static volatile boolean stopRequested;

為何用volatile可以解決呢?因為用了volatile,編譯器會已確保這個值的正確做為主要策略,而不會已效能為主來優化程式,因此它可以讓被設為volatile的field在任何Thread去讀取時,都會看到最新的狀態。

問題3,指令的重新排序:

在這篇文章 JSR 133 (Java Memory Model) FAQ 提到,執行程式時為了提高效率,因此compiler和processor可以對指令做重新排序的動作,比如說這段code:

class VolatileExample {
    int x = 0;
    boolean v = false;

    public void writer() {
        x = 42;
        v = true;
      }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

其中compile會認為在writer()內的這段code

public void writer() {
    x = 42;
    v = true;
}

和以下這段code,就語意上來說執行起來是一樣的,而為了效能考量,它有可能會對這段code進行重新排序然後執行,就會變成以下這段code:

public void writer() {
    v = true;
    x = 42;
}

而這如果發生在multithread的情況下,當有一條thread執行writer(),而另一條thread執行reader(),就會發生問題了,問題在於reader()內的程式:

public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
}

這邊會先判斷v是否為true,是true的話,根據原始碼的流程來看,如果這邊的v為true了,則代表writer()已經將x設為42了,但是由於編譯器對writer()做了重新排序,因此當reader()判斷v== ture之後繼續往下執行時,x的值有可能還是為0,這會造成問題。

所以這邊的code只要boolean v設為volatile,就能避免編譯器對writer ()這段code重新排序,如下:

volatile boolean v = false;

結論:

所以回到一開始的疑問,為何要在這Pattern的instance加上volatile關鍵字。

  • 為了讓Singleton instance初始化正常,不會初始化到一半就被別的thread取走了。

  • 確保編譯器不會幫你對Singleton的code進行優化,讓一切判斷如你預期般的執行。

  • 確保指令在執行時不會被重新排序,造成Singleton的值有異常。

值得注意的是,在JAVA 5以上才能用這種雙重鎖的技巧,原因volatile在JAVA 5之後變得比較完善(JSR-133實現)。。

參考:

  1. 深入理解Java内存模型(一)——基础
  2. 深入理解Java内存模型(四)——volatile
  3. The "Double-Checked Locking is Broken" Declaration
  4. Double-checked locking
  5. JSR 133 (Java Memory Model) FAQ

2014/01/05

DSL in Action - written by Debasish Ghosh

看過 Martin Fowler 在 InfoQ 的演講影片 Introduction to Domain Specific Languages之後,接下來,選擇看一本 DSL in Action 的書,這本書的內容涵蓋 JVM 所能支援的 DSL,並從各種角度,去分析實現 DSL 方法的優劣。這本書分兩個部份,第一個部份是 1~3 章,再加上 Appendix A,第二個部份則是 DSL 的實作範例,接下來先看第一個部份。

DSL

DSL 的功能是將 problem domain 對應到 solution domain,首先要找出兩個 domain 之間的共通語彙(vacabulary),透過這個共通的專有名詞,來建立 domain expert 跟 IT system 之間的溝通模型。換句話說,就是要從 domain expert 所描述的業務邏輯中,找到該 domain 的專有名詞、專用術語,再以這個專用術語為基礎,建立溝通互動的語言,形成雙方都能使用的 DSL。

Programmer 設計 DSL 時,要注意幾個地方:

  1. 要一直把使用者放在心上,因為 DSL 的好壞,不好用的話,會直接影響到使用的意願,那就失去了設計 DSL 的原意,沒有人用的 DSL 就是個失敗的設計。

  2. DSL 只需要針對該業務範圍進行抽象化的設計,沒有多餘的東西,太複雜冗長的語言,只會講低使用者的使用意願。

DSL 分為 Inernal、External、非文本DSL 三種。

DSL 的優缺點

DSL 的優點很容易理解,因為適度的抽象化,可幫助使用者更容易處理複雜的問題,試想,如果沒有 SQL ,那麼我們應該怎麼操作 Relational DB,要直接用 API 下 select() 嗎?

除了優點之外,我們更要注意 DSL 的缺點

  1. 設計DSL是很困難的工作
  2. DSL 需要大量前期設計,投入的人力成本
  3. DSL 增加的中間層,可能會有性能憂慮
  4. DSL 有時缺少足夠的編輯工具
  5. 可能會造成「學不完的DSL」現象
  6. DSL 可能導致語言之間的摩擦:開發APP可能需要同時使用多個 DSL,也因此可能造成整合上的問題

良好的抽象應具有的特質

DSL 牽涉到簡化業務邏輯的設計,這是一種抽象化的過程,我們要知道,要從哪些指標判斷抽象化的優劣

  1. 極簡:只開放使用者需要使用的功能,沒有多餘、外露的內部實作內。例如 API 要回傳 Map 而不是 TreeMap,可使用繼承的方式,隱藏內部的實作設計。
  2. 精煉:抽象化的內容不包含任何非本質的細節,移除不必要的細節,可利用 DI(Dependency Injection)隱藏實現的細節
  3. 擴展性:抽象設計可在不影響現有使用者的情況下,持續改進升級。利用 mixin、functional programming 的 closure、open class 的方式達到擴展性
  4. 組合性:可與其他抽象設計組合成更高階的抽象設計。使用 Command、Decorator Pattern。但可能會在multithread環境下造成問題。

實作 DSL 的方法

第二章一開始,以一個證券交易的實例設計 DSL,第一個版本是用 Java語法,但遇到交易員不熟悉 Java 語法的問題,而且 Java 語言裡有過多跟證券交易無關的東西。第二個版本是 XML,XML適合描述文件結構,不適合拿來作為 DSL,而且XML有太多無關的標記。因此又有了第三個版本,是使用 Groovy,這個版本才勉強有了個適當的 DSL。

Internal DSL 的分類

  1. 生成式:編譯後,轉換生成實作語言的的code

    1.1 編譯時meta programming:Lisp, Template Haskell

    1.2 執行時meta programming:Ruby, Groovy

  2. 內嵌式:領域專用的類別內嵌於宿主語言的類別系統

    2.1 Smart API:Java, Ruby

    2.2 AST:Java, Ruby, Groovy

    2.3 內嵌類別:Haskell, Scala

    2.4 反射式meta programming:Ruby, Groovy

External DSL 的分類

  1. 上下文驅動的字串操作

  2. xml 轉換成可使用的資源

  3. DSL 工作台

  4. DSL 中內嵌異質代碼

  5. 基於解析器組合子的 DSL 設計

實作 DSL 的方法有很多,我們應該如何選擇一個最適當的方法呢?要考慮的因素如下:

  1. 重用現有的機制:利用強大的宿主語言提供的功能,例如 Scala或 Haskell
  2. 充分利用現有的知識:要根據開發團隊現有的知識水準來選擇實作的方法。
  3. 外部DSL的學習曲線:外部DSL可能會很複雜,必須要把學習曲線納入開發成本。
  4. 適當的表現力:Internal DSL有重用宿主語言的優勢,但相對也約束了描述業務領域的表現力
  5. 組合性:DSL跟宿主語言之間的是不是能簡單地整合起來

DSL Driven Application Development

在開發 JVM 環境的 DSL 時,最重要的就是要看怎麼跟 JVM 整合在一起,開發的時候,要注意三個問題:1. 整合問題 2. 異常與錯誤的處理 3. 性能的表現。

如果要在一個系統裡面,同時使用多個 DSL,對於 JVM 來說,整合是不成問題的,而且我們可以選擇使用 Java、Groovy、Spring、JRuby、Scala 這些語言來實作 DSL,直接分別將這些實作包裝成 jar,就可以讓主程式引用了。

Internal DSL: Groovy

如果選擇使用了 Groovy,有兩種方式可以將 Groovy Script 整合起來:

  1. 使用 javax.script 的 Script Engine,Script Engine 是一種 sandbox,Groovy DSL 跟 Java Class 之間無法互通,另外在出現 Exception 的時候,stack trace 顯示的行號沒辦法直接對應到 DSL 中的行號,不容易除錯。所以在整合 Groovy DSL時,要優先考慮使用第二種方法。

  2. 使用 groovy.lang.GroovyClassLoader,以 Groovy 實做的 Order 類別,可直接讓 JVM 使用,在運作 dsl script 之後,也可以直接回傳 Order 的 List,Groovy 跟 Java 之間的互動比 Script Engine 的方法整合地更緊密。

Internal DSL: Spring

Spring 2.0 版之後就支援使用 Ruby、Groovy 實作的 Bean,還能直接運用 Spring 的 DI 功能,動態注入 script code,下面是一個定義 bean 的範例,透過 refresh-check-delay 的設定,spring 將會在每5000毫秒檢查script是否有被更新,而自動 refresh 載入的 bean,系統就可以在不關機的條件下,直接更新業務邏輯。

<lang:jruby
    id="accIntCalcRule"
    refresh-check-delay="5000"
    script-interface="org.spingframework.scripting.AccruedInterestCalculationRule"
    script-source="classpath:RubyAccruedInterestCalculationRule.rb">
</lang:jruby>

External DSL: XML

使用 XML Parser 進行文件的解析與處理。

External DSL: ANTLR、JAVACC

ANTLR(Another Tool for Language Recognition)裡面包括了 詞法分析器(Lexer)、語法分析器(Parser)、樹分析器 (tree parser) 的功能,編寫文法(詞法規則與語法規則)描述文件之後,交給ANTLR,就能生成以 Java 語言實作的 parser 程式碼,ANTLR 3.X支援生成 Java,C#,JavaScript,C 這幾種語言的程式碼。

感想

會去看這本 DSL in Action 根本是個意外,原本是要了解 Gradle,然後知道了 Groovy,漸漸地 dig in,最後到了 DSL。讀了幾篇文章後,才了解到,我們在設計系統時,考慮的系統設定模組、外部 API 模組,其實都屬於一種 DSL,我們也可以在設計系統設定時,採用不同於 XML 的方法,設計 API,也可以考慮使用 Groovy實作。而且看了之後,才知道還有太多專有名詞還不了解,現在頂多只能懂得一些皮毛。

以沒寫過 Groovy, Ruby,更別說會寫 Haskell, Scala的狀況,要能深刻了解這本書的內容,還真的有些吃力。接下來,應該把注意力先放回到Groovy。