יום שבת, 29 בנובמבר 2014

ההארה של ה-MainThread חלק 3 ואחרון

מאמר זו נסמך ומסכם את שלושת המאמרים הקודמים ומומלץ לקרוא אותם תחילה 1, 2, 3.
במאמר הראשון בסדרה עצרנו בנקודה בה הפעלנו ב-ActivityThread את הפונקציה הראשונה אשר תרוץ בהפעלת אפליקצייה חדשה, היא public static void main, הצגנו חידה והתפנינו להבין איך Lopper עובד.

לאחר שעברנו על Looper ו-Handler נחזור כעת לקוד המקור ונבחן את public static void  main. לאחר ניקוי מסיבי (מדובר במחלקה כבדה), סידור והזזות קטנות, נראה שהמצב דומה מאוד ל-

public class ActivityThread {
 
    static Handler sMainThreadHandler;  // set once in main()
    final H mH = new H();

    public static void main(String... args) {
        // Save the main looper as a static variable of the Looper class
        Looper.prepareMainLooper();
        // Get a reference to the main handler
        ActivityThread thread = new ActivityThread();
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        // Posting some messages
        Looper.loop();
    }
 
    private class H extends Handler {
        ...
        ...
    }
}
ברגע שה-Thread אשר שועתק (Fork) מהזיגוטה הפך פעיל, מתבצעות הפעולות הבאות בפונקציית ה-main (מקור שמם של ה-Looper וה-Thread הנוכחיים): 
  1. נקראת הפונקצייה Looper.prepareMainLooper, היא זהה ל-Looper.prepare עם תוספת של שמירת ה-Lopper הנוכחי, ה-MainLooper, כמשתנה סטטי של מחלקת Looper - הוא יהיה נגיש מכל מקום בקריאת Looper.getMainLooper.
  2. ה-Handler של ה-Thread הנוכחי, ה-MainThread, ישמר כמשתנה סטטי של מחלקת ActivityThread.
  3. ישלחו פקודות ראשונות ל-MainThreadHandler.
  4. Looper.loop ידאג להמשך חיי ה-Thread וליכולתו לקבל פקודות.
ל-MainThread חשיבות עצומה בכל הנוגע לאפליקצייה שלנו וכאן אצטט את הדוקומנטיצה של אנדרואיד - הוא אחראי לתפיסה ושליחה של events כמו לחיצה, מגע, וציור לממשק המשתמש, הוא ה-Thread היחידי בו האפליקציה תתקשר עם רכיבים מה-Android UI toolkit - ולכן לפעמים יכונה בנוסף ל-Main Thread - ה-UI Thread והוא ה-Thread אשר יהיה אחראי לכל הקריאות של פונקציות ה-LifeCycle במערכת (Recievers, Services, Activities, Providers).

לדוגמא, בלחיצת המשתמש על כפתור, ה-UI Thread ישלח הודעה (Handler.post) אשר תודיע לכפתור כי עליו לצייר עצמו מחדש במצב לחוץ. 

כפי שכולנו למדנו על בשרנו, במידה והאפליקצייה תבצע פעולות אינטנסיביות על ה-UI Thread, הביצועים שלו יפגמו והמשתמש ירגיש שה״אפליקציה תקועה״, חמור מכך - אם ה-UI Thread יהיה חסום למשך 5 שניות יוצג למשתמש הדיאלוג המושמץ - "application not responding״ (ANR dialog) והמשתמש יקבל את האופציה להרוג את האפליקציה. 
על כן, פעולות אשר לוקחות זמן רב כמו חישובים מסובכים, network events, אינטרקציה עם קבצים או עם ה-database וכיוצא בזאת, יופנו ל-Thread אחר. 
הסיבה לכך שכל הפעולות הקשורות לממשק המשתמש מתבצעות ב-Thread יחיד היא מכיוון שה-Android UI toolkit אינו Thread-safe. לסיכום שני חוקים מוכרים וחשובים לגבי ה-Main Thread:
  1. אל תחסום אותו
  2. אל תנסה לגשת לקומפוננטות של ה-UI toolkit מחוץ ל-Main Thread
ובמילים אחרות - Get off my main thread - כותרת אשר תלווה את סדרת המאמרים הבאה, בהם יבחנו (ברמת הקוד) השיטות השונות אשר אנדרואיד מספק לנו על מנת לבצע פעולות ב- Worker Thread בהם נמצא בין היתר את המחלקות Thread, AsyncTask, Loaders, ThreadPoolExecutors. 
ספוילר - כנראה שאת חלקם עד רובם כל מפתח אנדרואיד מכיר ועם זאת שמסתכלים ברמת הקוד דברים יכולים להפתיע ולחדש.

ועכשיו לחובות שלי - שתי חידות הצגתי, במאמר הראשון ובמאמר הקודם.
החידה מהמאמר הראשון -
שאלתי מה יהיה הפלט של הקוד הבא -

public class MainActivity extends Activity {

    private static final String TAG = "my_tag";
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (savedInstanceState == null) {
            Handler handler = new Handler();    
            handler.post(new Runnable() {    post1 נקרא לזה
                public void run() {
                    Log.d(TAG, "Posted before requesting orientation change");
                }
            });
            Log.d(TAG, "Requesting orientation change");
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            handler.post(new Runnable() {    post2 נקרא לזה
                public void run() {
                    Log.d(TAG, "Posted after requesting orientation change");
                }
            });
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}

“Stress and nervous tension are now serious social problems in all parts of the galaxy and it is in order that this situation should not be in any way exacerbated that the following facts will now be revealed in advanced”
על פי המדריך לטרמפיסט לגלקסיה ומנסיון אישי, מסתבר שעודף סקרנות עשוי להוביל למשבר חברתי עמוק ולכן אתחיל בתשובה -
  1. Requesting orientation change
  2. onStart
  3. Posted before requesting orientation change
  4. onStop
  5. onDestroy
  6. onStart
  7. Posted after requesting orientation change
אלה מכם שידעו את התשובה יכולים לעבור לפתרון החידה הבאה או לכבות כעת את המחשב, המסוקרנים מהסיבה לתשובה ימשיכו איתי הלאה.
כדי לפתור את התשובה, נברר מה ב-MessageQueue של ה-MainLooper, וזאת נעשה כך-
public class MyApplication extends Application {
 
    private static final String TAG = "main_looper"; 
    private Printer printer;

    @Override
    public void onCreate() {
        super.onCreate();
        printer = new Printer() {

            @Override
            public void println(String x) {
                Log.d(TAG, x);    
            }
        };

        Looper.getMainLooper().setMessageLogging(printer);
    }
}
לפני שה-Activity הראשון שלי נוצר, אוסיף לוג ל-MainLooper שלי וכך אקבל את כל המשימות אשר הוא ישלוף. והתוצאה (אחרי לא מעט סינונים) -

Dispatching to Handler (android.app.ActivityThread$H): 100
Dispatching to Handler (android.app.ActivityThread$H): 118
Dispatching to Handler (android.app.ActivityThread$H): 126

מחלקת ActivityThread$H לא חדשה לנו - היא מתוארת בתחילת הכתבה הזו, החלקים בקוד של H המעניינים אותנו -
private class H extends Handler {
    public static final int LAUNCH_ACTIVITY                    = 100;
    public static final int CONFIGURATION_CHANGED   = 118;
    public static final int RELAUNCH_ACTIVITY               = 126;

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case LAUNCH_ACTIVITY:
                ActivityClientRecord r = (ActivityClientRecord)msg.obj;
                handleLaunchActivity(r, null);
                break;
            case RELAUNCH_ACTIVITY:
                ActivityClientRecord r = (ActivityClientRecord)msg.obj;
                handleRelaunchActivity(r);
                break;
            case CONFIGURATION_CHANGED:
                mCurDefaultDisplayDpi = ((Configuration)msg.obj).densityDpi;
                handleConfigurationChanged((Configuration)msg.obj, null);
                break;
        }        
    }
}
100 - ידאג לאתחל את ה-Actvity, לעניינינו הוא יקרא ל-
  1. onCreate
  2. onStart
118 - ידאג לשינוי האוריינטציה, מהקוד המוצג כאן ניתן לנחש שכל נושא בחירת ה-Resources יהיה באחראיותו
126 - ידאג להריץ מחדש את ה-Activity לאחר שהאוריינטציה התחלפה - לעניינינו הוא יקרא ב-Activity שלנו ל-
  1. onStop
  2. onDestroy
ייצור Activity חדש ויקרא אצלו ל-
  1. onCreate
  2. onStart
מלבד זאת הוא גם ידאג לשלוף את המידע של ה-Activity הישן, לפני שזה יהרס ולשלוח אותו ל-Activity החדש - הוא ה-savedInstanceState. סדר הפעולות המפורט מסביר מדוע אם נבדוק את האוריינטציה של המכשיר ב-onDestroy נקבל landscape על אף שה-Activity שנהרס פעל ב-Portrait.

נסכם -
תחילה לסדר הפעולות יכנס onCreate → onStart, ב-onCreate שלנו נוסיף למשימות את post1, לאחריו 118, 126 ולסיום post2, כך שסדר המשימות שלנו הוא
onCreate → onStart → post1 → onStop → onDestroy → onCreate → onStart → post2
אם נקרא את הלוגים המפוזרים במשימות לעיל נקבל בדיוק את התשובה.

הערה חשובה:
post2 נוצר ב-Activity הראשון אך נקרא רק כאשר ה-Activity השני מוצג והראשון אחרי onDestroy, במידה והיינו עושים בו משהו עם שדה של ה-Activity הראשון היינו יוצרים באג. בכל post שאנו שולחים יש לקחת בחשבון את המצב בו אנו עשויים להיות ברגע שהמשימה תחזור ל-handler.

לגבי החידה אשר הוצגה במאמר הקודם, נשאלה השאלה מה הבעיה בקטע קוד הבא -

public class MainActivity extends Activity {

    protected static final String TAG = "my_tag";
    private Handler handler = new Handler();
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler.postDelayed(new Runnable() {
   
            @Override
            public void run() {
                Log.d(TAG, "3 minutes passed");
            }
        }, TimeUnit.MINUTES.toMillis(3));
    }
}
הבעיה הגדולה הטמונה בקטע הקוד הזה נובעת מהעובדה שבג׳אווה, non-static inner and anonymous class שומרת מצביע ל-outer class שלה, כך שאוכל להגיע ל-MainActivity מתוך ה-Runnable שלי בעזרת MainActivity.this. 
ה-Runnable שלי נשמר ב-Message בתוך ה-MainLooper וכעת אם אסובב את המסך מספר פעמים, ה-Activities שלי לא ישתחררו עד ששלושת הדקות יעברו וה-Message יתמחזר. במצב זה הזכרון עשוי להתנפח והאפליקציה עלולה להיאט ואף לקרוס. בנוסף קיימת אותה הבעיה שדיברנו עליה בהיערה החשובה מהפסקה הקודמת.
ישנם מספר פתרונות לבעיה, החל מ-Handler\Runnable סטטים ועד לשחרור כל המשימות של ה-Handler ב-onDestory. הדרך האופטימלית לפתרון הבעיה היא ביד המפתח - כל עוד הוא יודע את שקורה מאחורי הקוד שלו ומבין את הבעיות העשויות לצוץ.

 לקריאה נוספת -
http://developer.android.com/guide/components/processes-and-threads.html 
http://corner.squareup.com/2013/12/android-main-thread-2.html  
https://techblog.badoo.com/blog/2014/08/28/android-handler-memory-leaks/
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html 
 
 

יום שבת, 22 בנובמבר 2014

ההארה של ה-MainThread חלק 2


נמשיך בעקבות המאמר הקודם ונצלול לנבכי ה-Thread באנדרואיד, להבין טוב יותר את פעילותו.
מאמר זה חשוב מאוד והבנת חלקים אחרים באנדרואיד נסמכת על הבנת הנאמר כאן.
Loopers
Looper היא מחלקה אשר עוזרת לנו לשמור Thread בחיים ולהעביר אליו הוראות בטור. 
במידה ונתחיל את ה-Thread הבא -

public class MyThread extends Thread {

    @Override
    public void run() {
        // do stuff here
        // and another stuff here
    }
}
ה-Thread שלנו יעבור ב-run, יעשה דבר ועוד דבר ויצא. על מנת להשאיר Thread פעיל ושיעבוד עבורנו על פי דרישה, נחפש משהו יותר דומה ל-

MessageQueue queue;
 
@Override
public void run() {
    while (true) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return; 
        }
        msg.target.dispatchMessage(msg); 
        msg.recycle();  
    }

במבנה הזה ישנה לולאה אשר שולפת משימות מתור, שולחת כל משימה בתורה לאובייקט target אשר ידאג לבצע אותה וממחזרת את המשימה, זה בהפשטה מה שקורה ב-Thread עם Looper.
 
איך Looper עובד?
המחלקה עצמה דיי פשוטה ומומלץ לעבור על קוד המקור לקבל הבנה עמוקה יותר, הסרתי והזזתי חלק מהקוד לצורך ההסבר -


public class Looper {

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    final MessageQueue mQueue;
 
    public static void prepare() {
        sThreadLocal.set(new Looper());
    }
    
    public static void loop() {
        final MessageQueue queue = myLooper().mQueue; 
        while (true) {
            Message msg = queue.next(); // might block
            msg.target.dispatchMessage(msg);
            msg.recycleUnchecked();
        }
    }

    public static Looper myLooper() {
        return sThreadLocal.get();
    }
 
    public void quit() {
        mQueue.quit();
    } 
}
 
מי שעקב באדיקות אחרי המאמר הקודם יבחין שה-Looper נשמר ב-ThreadLocal, דבר המבטיח מקסימום של Looper אחד ל-Thread וזמינות של כל Looper ל-Thread שלו בלבד.

public static void prepare()
ידאג ליצור את ה-Looper ולשמור אותו ב-ThreadLocal של ה-Thread הנוכחי.
public static void loop()
ידאג להריץ את כל המשימות ברשימה שלנו.
public static Looper myLooper()
יחזיר לנו את ה-Looper של ה-Thread אשר אנו נמצאים בו
public void quit()
על מנת לאפשר ל-Thread להשתחרר בסיום העבודה יש לקרוא לסיום ה-Looper ובכך 
להודיע ל-Thread שמשימותיו תמו.

Handler הוא השותף של ה-Looper, הוא ה-target ב-msg.target ב-void loop של ה-Looper,
לו שתי יכולות עיקריות:
  • שליחת משימות ל-Looper (יכול להיעשות מכל Thread)
  • ביצוע המשימות אשר ה-Looper שולף (יבוצע ב-Thread אשר מקושר ל-Looper)
המתווך בין ה-Handler ל-Looper יהיה ה-Message, מחלקה גנרית ויעילה מאוד, מרבית הפעילות של המשתמש עם האפליקציה יתורגם ל-Message ל-Looper.
כל Handler יהיה מקושר ל-Looper אחד בלבד אשר יהיה מקושר ל-Thread אחד בלבד, מספר ה-Handler אשר יהיו מקושרים לאותו ה-Looper אינו מוגבל, סדר הפעילויות בפשטות יראה כך -
 
הדיאגרמה לקוחה מתוך הבלוג של rxwen - just do IT

האיור לעיל מציג את שקורה ב-Thread אחד בלבד, לו Looper אחד ושני Handler. במקרה הזה Handler1 שלח את Message1 וכלשיגיע תורו ה-Looper יחזיר אותו ל-Handler להמשך טיפול, כמו כן Handler2 אחראי לשליחת משימות 2 ו-3.

הגנריות של Message באה לידי ביטוי בכך שהשימוש ב-Message נתון כמעט באופן בלעדי לידי 
המפתח -

/**
 * User-defined message code so that the recipient can identify
 */ 
public int what;
long when;

/**
 * if you only need to store a few integer values.
 */
public int arg1; 
public int arg2;
/**
 * An arbitrary object to send to the recipient.  
 */
public Object obj;

המפתח יחליט מה להעביר ב-what על מנת לזהות בהמשך את ה-Message כאשר זה יחזור מה-Looper ל-Handler, המפתח יחליט מתי ה-Message צריך להשלח ב-when, המפתח יחליט אם ומה להכיל ב-arg1 ו-arg2 והאם להוסיף אובייקט ל-obj. 

היעילות של Message -

Handler target;
Runnable callback;
Message next;
private static final Object sPoolSync = new Object();
private static Message sPool;
 
/**
 * Return a new Message instance from the global pool. Allows us to
 * avoid allocating new objects in many cases.
 */
public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            return m;
        }
    }
    return new Message();
}

/**
 * Recycles a Message 
 * Used internally by the MessageQueue and Looper when disposing of queued Messages.
 */
void recycle() {
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    when = 0;
    target = null;
    callback = null;

    synchronized (sPoolSync) {
        next = sPool;
        sPool = this;
    }
}
 
כשבחנו את ה-Looper ראינו שהוא מכיל MessageQueue, הוא ידאג לשלוף עבורו כל Message בתורו ובתנאי שה-when שלו הגיע, בסיום ה-Looper ימחזר את ה-Message. 

היצירה של ה-Message היא אחת מ-2 המשימות שנותרו ל-Handler.
דוגמא אחת לשימוש ב-Handler -
 // Each handler is associated to one looper.
Handler handler = new Handler(myLooper) {
    public void handleMessage(Message message) {
        switch (message.what) {
            case SOMETHING:
                // do something on the thread of the looper
                break;
            case SOMETHING_ELSE:
                // do something else on the thread of the looper
                break;
        }
    }
};

// Create a new message associated to that handler.
Message message = handler.obtainMessage();
message.what = SOMETHING_ELSE;

// Add the message to the looper queue, can be called from any thread.
handler.sendMessage(message);
 
בדוגמא הנוכחית יצרנו Handler, קישרנו אותו ל-Looper והגדרנו לו פעולות לבצע כתלות במשימה, לאחר מכן יצרנו משימה אשר מקושרת ל-Handler שלנו ושלחנו אותה להמתין בתור עד שה-Looper יחזיר לנו אותה. הטיפול במשימה יתבצע ב-Thread המקושר ל-Looper, אך שליחת המשימה תוכל להתבצע מכל
Thread.

דרך נוספת ונפוצה לשלוח משימות בעזרת Handler תהיה

handler.post(new Runnable() {
    public void run() {
        // do something on the thread of the looper
    }
});
 
כאן יצרנו Handler עם wait של 0 מילישניות (במידה והיינו משתמשים ב-postDelayed זה הערך אשר היה
משתנה), וה-Runnable שלנו ישמר ב-callback של ה-Message ובבוא העת ירוץ ב-Thread של 
ה-Looper.

Handler אשר יאותחל כך -
Handler handler = new Handler();
יקושר ל-Thread בו הוא נוצר, במהלך האיתחול שלו הוא יבצע
mLooper = Looper.myLooper();
ככלל ביצירת Handler חדש כדאי למפתח לדעת לאיזה Looper ו-Thread ה-Handler הנוצר מקושר 
ולכן לא מומלץ לאתחל Handler מבלי לספק לו Looper.

HandlerThread
 
HandlerThread - מחלקה אשר יורשת מ-Thread ומחברת בין על האמור לעיל
 
public class HandlerThread extends Thread {

    Looper mLooper;

    @Override
    public void run() {
        Looper.prepare();
        mLooper = Looper.myLooper();
        Looper.loop();
    }

    public Looper getLooper() {
        return mLooper;
    }

    public void quit() {
        mLooper.quit();
    }
}
 השימוש בה יתבצע כך -
HandlerThread thread = new HandlerThread("MyCoolThreadName");
thread.start(); // starts the thread.
Handler myThreadHandler = new Handler(thread.getLooper());
// do a lot of stuff with my handler
// finally let the thread quit
thread.quit(); 

במאמר הבא נסיים להבין את ה-MainThread ונענה על החידה מהמאמר הראשון, לבינתיים אציג חידה נוספת אשר הוצגה ב-(DroidCon Tel-Aviv (2014 - מה הבעיה בקטע קוד הבא?

public class MainActivity extends Activity {

    protected static final String TAG = "my_tag";
    private Handler handler = new Handler();
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        handler.postDelayed(new Runnable() {
   
            @Override
            public void run() {
                Log.d(TAG, "3 minutes passed");
            }
        }, TimeUnit.MINUTES.toMillis(3));
    }
}

   
לקריאה נוספת: 
http://rxwen.blogspot.co.il/2010/08/looper-and-handler-in-android.html
http://corner.squareup.com/2013/10/android-main-thread-1.html

יום שבת, 15 בנובמבר 2014

ההארה של ה-MainThread חלק 1

 “Understanding more about the architecture and what’s actually going on in the platform will help you write better applications” (Chet Haase and Romain Guy - Tech lead of the Android UI toolkit)

על מנת לענות על החידה מהכתבה הראשונה, נחתור לביאור ה-Main Thread ולהבנת הפעולות אשר יביאו אותנו ליצירת ה-Main Thread.
נניח והחלטנו לבנות אפליקציית לוח-שנה, ומכיוון שאין אנו רוצים להטמע בין שאר אפליקציות לוח-השנה הקיימות החלטנו להוסיף פיצ׳ר חדש וייחודי לאפליקציה שלנו - התאריך אצלנו יוצג בצורה אופטימלית לכל שפה. כך למשל נבקש להראות למשתמש מארה״ב תאריך בצורה M/DD/YY ולמשתמש מצרפת DD/MM/YY, עוד על אפשרויות לוקליזציה של תאריכים ניתן למצוא כאן.
לשם השמשת הפיצ׳ר, כתבנו את הקוד הבא -


/**
 * Gets a date instance and return its date formatted to the user's locale.
 * @param date - The date instance to format
 * @return the date in a short and localized format
 */ 
public String formatDate(Date date) {
    DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT);
    return formatter.format(date);  
}
פונקציה אשר מקבלת אובייקט של תאריך ומחזירה את התאריך הנוכחי מותאם לשפה של המשתמש.
לאחר שסיימנו לכתוב את האפליקציה והתפננו לשיפורי מהירות, גילינו שהפונקציה הנ״ל רצה המון פעמים, יוצרת הרבה אובייקטים וגורמת לקריאות רבות ל-GC.

השלב הראשון בדרך לפתרון - הכר את הקוד שלך. אחת השאלות הראשונות אשר צריכות לעלות לקורא בקריאת הקוד היא - האם בקריאה שנייה לפונקציה יווצר אובייקט נוסף של DateFormat או האם מדובר באובייקט יחיד?

בהצצה חטופה בקוד המקור של אנדרואיד ניתן למצוא שאכן בכל קריאה נקבל אובייקט חדש -
public static final DateFormat getDateInstance(int style) { 
    checkDateStyle(style);
    return getDateInstance(style, Locale.getDefault());
}

public static final DateFormat getDateInstance(int style, Locale locale) {
    checkDateStyle(style);
    if (locale == null) 
        throw new NullPointerException("locale == null");
    }
    return new SimpleDateFormat(LocaleData.get(locale).getDateFormat(style), locale);
}

מכאן הדרך לפתרון מהירה, נאתחל את המשתנה פעם אחת בלבד -

private DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT); 
 
public String formatDate(Date date) {  
    return formatter.format(date);  
}

סגרנו את הפתרון, בדקנו, קימפלנו כמעט העלינו ואז גילינו שאנחנו מקבלים ערכים מוזרים בקריאות ממספר Threads לפונקציה שלנו.
  
גם כאן הפתרון מהיר ופשוט, נדאג לסינכרון -
private DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT); 
 
public synchronized String formatDate(Date date) {  
    return formatter.format(date);  
}
אך לאחר הפתרון הנ״ל גילינו שהפונקציה שלנו מהווה צוואר בקבוק ואנו לא מעוניינים לחסום אותה. מכאן נבדוק בדוקמנטציה של אנדרואיד ונגלה את המשפט הבא: 
״SimpleDateFormat is not thread-safe. Users should create a separate instance for each thread.״

וכאן נכנס ThreadLocal לעבודה, ThreadLocal מאפשר לנו ליצור אובייקטים אשר קשורים ל-Thread בו הם נוצרו, נוכל מכל מקום בקוד לבקש את האובייקט ונקבל את האובייקט הקשור ל-Thread בו אנו נמצאים, כך שאותו הקוד, יספק לנו אובייקטים שונים כתלות ב-Thread בו הקוד נקרא. במילים אחרות - כל Thread יצור בשבילו את האובייקט אשר יהיה נגיש רק לו.

איך זה עובד?
המחלקה עצמה עברה מספר דורות והיא עמוסה באופטימיזציות, להבנה מעמיקה ניתן לקרוא את הבלוג של בוב לי, האיש אשר חתום על הקוד של ThreadLocal באנדרואיד. 
בהפשטה רבה של הקוד: 
נניח שבכל Thread יהיה לנו את ה-HashMap הבא
public HashMap<Object, Object> values;
ו-ThreadLocal בנוי כך

public class ThreadLocal<T extends Object> extends Object {
 
    public T get() {
        return Thread.currentThread().values.get(this);
    }

    public void set(T value) {
        Thread.currentThread().values.put(this, value);
    }
}
אזי לפתירת הבעיה שלנו אנחנו יכולים לגשת כך

public static ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>();
 
public String formatDate(Date date) {  
    if (df.get() == null)
        df.set(DateFormat.getDateInstance(DateFormat.SHORT));
     return df.get().format(date);  
}

בסופו של דבר, קיבלנו קוד נקי מאוד אשר יצור אובייקט בודד של DateFormat לכל Thread המבקש לקרוא לפונקציה שלנו. מלבד פתירת בעיות Thread-safe ניתן להשתמש בו לפתרון בעיות נוספות, למשל ב-Worker Threads אשר כל worker עובד על פעולה בעלת id ייחודי, נוכל לשמור את ה-id ב-ThreadLocal וכך להנגיש אותו לכל מקום בקוד שלנו.

הערה חשובה:
ג׳אווה מעודדת מאוד מחזור Threads ושימוש ב-Thread pools ועל כן יש להזהר בשימוש ב-ThreadLocal, במצב הזה למשל -

final static ThreadLocal<Integer> counter = new ThreadLocal<Integer>();
 
public void fireThreads() { 
    ExecutorService ex = Executors.newFixedThreadPool(2);
    final AtomicBoolean inFirstThread = new AtomicBoolean(true);
    for (int i = 0; i < 4; i++) {
        ex.execute(new Runnable() {
 
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {}
                if (inFirstThread.getAndSet(false))
                    counter.set(5);
                Log.d(TAG, Thread.currentThread().getName() + ", " + counter.get());
            }
        });
    }
    ex.shutdown();
}
ניסינו לתת ל-Thread הראשון בלבד את הערך 5 ב-counter, אך מפני שאנחנו ממחזרים את ה-Threads 
נקבל בלוג שלנו -
pool-1-thread-1, 5
pool-1-thread-2, null
pool-1-thread-1, 5
pool-1-thread-2, null 
אמנם המשתנים ב-ThreadLocal ישמרו כ-WeakReference, אך עם זאת אין צורך להוסיף על הבעיות אשר עשויות לצוץ כאשר ערכים לא רצויים נדחפים לקוד שלנו.
במקרה הנוכחי, על מנת לפתור את הבעיה היינו משנים את הקוד שיראה כך

if (inFirstThread.getAndSet(false))
    counter.set(5);
else
    counter.remove();
ובכך נאפס את הערך ב-counter שלנו.