• 售前

  • 售后

热门帖子
入门百科

《Android开发艺术探索》第10章-Android 的消息机制读书笔记

[复制链接]
心醉的斜阳呈 显示全部楼层 发表于 2022-1-12 18:22:19 |阅读模式 打印 上一主题 下一主题
目录



1. Android 的消息机制概述

1.1 Android 的消息机制是什么?

Android 的消息机制是通过 Handler 的运行机制来实现将一个任务切换到 Handler 所在的线程中去执行。
但是,完成把一个任务切换到 Handler 所在的线程中去执行这个事情,单靠 Handler 类是不行的;实际上,Handler 的运行需要 MessageQueue 和 Looper 的支撑,Handler 是作为 Android 消息机制的上层接口而已。
换句话说,Android 定义了Handler 直接面向了开发者,屏蔽了 MessageQueue 和 Looper(没有完全屏蔽 Looper),开发者只需要和 Handler 打交道就可以运用 Android 的消息机制了。
1.2 Handler 就是专门用来更新 UI 的,这种说法对吗?为什么?

不对。
在开发过程中,我们在子线程中执行一些耗时的操作,比如读取文件,读取数据库,访问网络等,拿到我们需要的数据,然后把这些数据显示在 UI 上。这时,直接在子线程中操作 UI 控件来显示数据,Android 是不允许的,会抛出异常给我们的;正确的做法是,在 UI 线程创建一个 Handler 对象,在子线程中使用这个 Handler 对象将要显示的数据切换到 Handler 所在的 UI 线程,再操作 UI 控件来显示数据。这就是 Handler 用来更新 UI 的场景了。
来看看实际的代码吧:
  1. // 在主线程创建 Handler 对象
  2. private Handler mainThreadHandler = new Handler(){
  3.     @Override
  4.     public void handleMessage(Message msg) {
  5.         super.handleMessage(msg);
  6.         if (msg.what == 3) {
  7.             Log.d(TAG, "handleMessage: msg.what=" + msg.what + ",msg.obj=" +
  8.                     msg.obj + ",threadName=" + Thread.currentThread().getName());
  9.             // 这里是主线程,可以放心更新 UI 了。
  10.         }
  11.     }
  12. };
  13. // 点击按钮从子线程发送消息到主线程
  14. public void sendMessage2UIThread(View view) {
  15.         // 开启一个子线程
  16.     new Thread(new Runnable() {
  17.         @Override
  18.         public void run() {
  19.             int what = 3;
  20.             String obj = "hello, ui thread!";
  21.             Log.d(TAG, "sendMessage2UIThread: what="+ what +",obj=" +
  22.                     obj + ",threadName=" + Thread.currentThread().getName());
  23.             mainThreadHandler.obtainMessage(what, obj).sendToTarget();
  24.         }
  25.     }, "work-thread").start();
  26. }
复制代码
打印日志如下:
  1. D/MainActivity: sendMessage2UIThread: what=3,obj=hello, ui thread!,threadName=work-thread
  2. D/MainActivity: handleMessage: msg.what=3,msg.obj=hello, ui thread!,threadName=main
复制代码
但是,我们还可以把数据从主线程切换到子线程中去执行。这里使用实际的例子来进行说明:
  1. private Handler workThreadHandler;
  2. private void startWorkThread() {
  3.         // 开启一个子线程
  4.     new Thread(new Runnable() {
  5.         @Override
  6.         public void run() {
  7.             Looper.prepare();
  8.             // 在子线程中创建 Handler 对象
  9.             workThreadHandler = new Handler() {
  10.                 @Override
  11.                 public void handleMessage(Message msg) {
  12.                     super.handleMessage(msg);
  13.                     if (msg.what == 2) {
  14.                         Log.d(TAG, "handleMessage: msg.what=" + msg.what + ",msg.obj=" +
  15.                                 msg.obj + ",threadName=" + Thread.currentThread().getName());
  16.                     }
  17.                 }
  18.             };
  19.             Looper.loop();
  20.         }
  21.     }, "work-thread").start();
  22. }
  23. // 点击按钮从主线程发送消息到子线程
  24. public void sendMessage2WorkThread(View view) {
  25.     int what = 2;
  26.     String obj = "hello, work thread!";
  27.     Log.d(TAG, "sendMessage2WorkThread: what="+ what +",obj=" +
  28.             obj + ",threadName=" + Thread.currentThread().getName());
  29.     workThreadHandler.sendMessage(workThreadHandler.obtainMessage(what, obj));
  30. }
复制代码
点击按钮,打印日志如下:
  1. D/MainActivity: sendMessage2WorkThread: what=2,obj=hello, work thread!,threadName=main
  2. D/MainActivity: handleMessage: msg.what=2,msg.obj=hello, work thread!,threadName=work-thread
复制代码
可以看到,这里确实实现了把数据从主线程切换到子线程中了。
因此,我们说,Handler 并非是专门用来更新 UI 的,只是常被开发者用来更新 UI 而已。
1.3 在子线程真的不能更新 UI 吗?

我们知道,Android 规定访问 UI 要在主线程中进行,如果在子线程中更新 UI,程序就会抛出异常。这是因为在 ViewRootImpl 类中会对 UI 做验证,具体来说是由 checkThread 方法来完成的。
  1. void checkThread() {
  2.     if (mThread != Thread.currentThread()) {
  3.         throw new CalledFromWrongThreadException(
  4.                 "Only the original thread that created a view hierarchy can touch its views.");
  5.     }
  6. }
复制代码
现在我们在子线程里去给 TextView 控件设置文本:
  1. private TextView tv;
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4.     super.onCreate(savedInstanceState);
  5.     setContentView(R.layout.activity_check_thread_not_working);
  6.     tv = (TextView) findViewById(R.id.tv);
  7.     new Thread(() -> {
  8.         SystemClock.sleep(1000L);
  9.         tv.setText("I am text set in " + Thread.currentThread().getName());
  10.     },"work-thread").start();
  11. }
复制代码
运行程序,会报错:
  1. 2022-01-08 05:47:15.391 9225-9252/com.wzc.chapter_10 E/AndroidRuntime: FATAL EXCEPTION: work-thread
  2.     Process: com.wzc.chapter_10, PID: 9225
  3.     android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
  4.         at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
  5.         at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)
  6.         at android.view.View.requestLayout(View.java:17476)
  7.         at android.view.View.requestLayout(View.java:17476)
  8.         at android.view.View.requestLayout(View.java:17476)
  9.         at android.view.View.requestLayout(View.java:17476)
  10.         at android.view.View.requestLayout(View.java:17476)
  11.         at android.widget.TextView.checkForRelayout(TextView.java:6871)
  12.         at android.widget.TextView.setText(TextView.java:4057)
  13.         at android.widget.TextView.setText(TextView.java:3915)
  14.         at android.widget.TextView.setText(TextView.java:3890)
  15.         at com.wzc.chapter_10.CheckThreadNotWorkingActivity.lambda$onCreate$0$CheckThreadNotWorkingActivity(CheckThreadNotWorkingActivity.java:19)
  16.         at com.wzc.chapter_10.-$$Lambda$CheckThreadNotWorkingActivity$Thy_KGiEr_duYPMycxt-0lYIEGo.run(lambda)
  17.         at java.lang.Thread.run(Thread.java:818)
复制代码
可以看到正是在 ViewRootImpl 类的 checkThread 方法里面抛出了异常:
  1. Only the original thread that created a view hierarchy can touch its views.
复制代码
checkThread 方法里的 mThread 就是 UI 线程,现在我们在子线程里面调用了 checkThread 方法,则 Thread.currentThread() 就是子线程,这样 mThread != Thread.currentThread() 判断就为 true,会进入 if 分支,抛出 CalledFromWrongThreadException 异常。
但是,如果我把 SystemClock.sleep(1000L); 这行代码注释掉会怎么样呢?
运行程序,效果如下:

是的,这不是幻觉,在子线程更新 UI 成功了。
那么,问题又来了,为什么有休眠时在子线程更新 UI 报错,而不休眠时在子线程更新 UI 成功呢?
这是因为有休眠时,在执行更新 UI 操作时,ViewRootImpl 对象已经创建成功了,就会执行到 checkThread 方法了;没有休眠时,在执行更新 UI 操作时, ViewRootImpl 对象还未创建,就没有执行到 checkThread 方法了。
实际上,我们这里不加休眠的情况下,只是在子线程设置文本时没有走 checkThread 方法而已,等到真正把文本绘制到屏幕上,仍然是在 UI 线程进行的。
再看一下这个方法,
  1. void checkThread() {
  2.     if (mThread != Thread.currentThread()) {
  3.         throw new CalledFromWrongThreadException(
  4.                 "Only the original thread that created a view hierarchy can touch its views.");
  5.     }
  6. }
复制代码
只要 mThread 与 Thread.currentThread() 相同就不会报异常,并且异常的中文含义:只有原来创建了视图体系的线程才可以操作它的 View。这根本没有说不让子线程更新 UI。这里真正想说明的意思是:哪个线程创建了视图体系,就要由那个线程来操作它的 View;换句话说,如果某个线程去操作另外一个线程创建的 View,那是不允许的。
那么,如果我们就在子线程中去完成视图的添加,这会有问题吗?
我们在子线程里面去添加一个 Window,代码如下:
  1. public void createUIInWorkThread(View view) {
  2.     new Thread(() -> {
  3.         // 这里要由 Looper 对象,因为在 ViewRootImpl 里面会创建 ViewRootHandler 对象。
  4.         Looper.prepare();
  5.         TextView tv = new TextView(MainActivity.this);
  6.         tv.setBackgroundColor(Color.GRAY);
  7.         tv.setTextColor(Color.RED);
  8.         tv.setTextSize(40);
  9.         tv.setText("i am text created in " + Thread.currentThread().getName());
  10.         WindowManager windowManager = MainActivity.this.getWindowManager();
  11.         WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
  12.                 WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
  13.                 0, 0, PixelFormat.TRANSPARENT);
  14.         layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
  15.         layoutParams.gravity = Gravity.CENTER;
  16.         layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  17.                 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
  18.         windowManager.addView(tv, layoutParams);
  19.         Looper.loop();
  20.     }, "work-thread").start();
  21. }
复制代码
点击按钮,查看效果:

可以看到,在子线程里里面操作 UI 是可以的。
这里我们总结一下:


  • 在线程 A 里面一般不能操作线程 B 的 UI;但是如果线程 B 的 ViewRootImpl 还没有创建,这时就不会走 checkThread 方法,也不会抛出异常,最终仍是由线程 B 完成了 UI 的操作。
  • 在一个线程里操作由这个线程自己创建的视图体系是可以的,也可以说,一个线程只可以操作它自己的 UI。
1.4 Android 系统为什么使用单线程模型来访问 UI?

Android 的 UI 控件不是线程安全的,如果在多线程中并发访问可能会导致 UI 控件处于不可预期的状态;而如果对 UI 控件的访问加上锁机制,会让 UI 访问的逻辑变得复杂,也会降低 UI 访问的效率。
所以,Android 采用单线程模型才处理 UI 操作。
1.5 为什么说 Handler 类是 Android 消息机制的上层接口?

从构造方法来看
分为两大类:可以传递 Looper 对象的和不可以传递 Looper 对象的。

我们重点看不传递 Looper 对象的 Handler(Callback callback, boolean async) 方法,因为这个方法非常具有代表性。
  1. public Handler(Callback callback, boolean async) {
  2.     mLooper = Looper.myLooper();
  3.     if (mLooper == null) {
  4.         throw new RuntimeException(
  5.             "Can't create handler inside thread that has not called Looper.prepare()");
  6.     }
  7.     mQueue = mLooper.mQueue;
  8.     mCallback = callback;
  9.     mAsynchronous = async;
  10. }
复制代码
这个方法的主要作用:


  • 通过 Looper.myLooper() 方法来获取并通过 mLooper持有 Looper 对象。Looper.myLooper() 方法会从线程本地变量 ThreadLocal里面取出与当前线程对应的 Looper 对象。
  • 如果 mLooper 对象仍为 null,就会抛出异常:“Can't create handler inside thread that has not called Looper.prepare()”,这是告诉当前线程没有调用 Looper.prepare(),所以不能创建 Handler。
  • 通过 Looper 对象获取 MessageQueue 对象并赋值给 mQueue 成员变量。
在构造 Handler 对象时,就一定要持有 Looper 对象和 MessageQueue 对象,也就是说,Handler 类组合了 Looper 对象和 MessageQueue 对象。
从发送消息方法来看
发送消息的方法分为两大类:postXXX 方法和 sendXXX 方法。
postXXX 方法用于发送一个 Runnable 对象,它会包装成一个 Message 对象,再发送到消息队列中;
sendXXX 方法用于发送一个 Message 对象到消息队列中。

从调用图可以看到,不管是 postXXX 方法和 sendXXX 方法,最终调用的都是 enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) 方法:
  1. private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
  2.     msg.target = this;
  3.     if (mAsynchronous) {
  4.         msg.setAsynchronous(true);
  5.     }
  6.     return queue.enqueueMessage(msg, uptimeMillis);
  7. }
复制代码
这个方法的主要作用:


  • 把 Handler 对象赋值给 Message 对象的 target 字段;
  • 调用 MessageQueue 对象的 enqueueMessage 方法把消息加入到消息队列。
从处理消息方法来看
当消息从消息队列中取出时,会调用 Handler 对象的 dispatchMessage 方法来分发消息
  1. public void dispatchMessage(Message msg) {
  2.     if (msg.callback != null) {
  3.         handleCallback(msg);
  4.     } else {
  5.         if (mCallback != null) {
  6.             if (mCallback.handleMessage(msg)) {
  7.                 return;
  8.             }
  9.         }
  10.         handleMessage(msg);
  11.     }
  12. }
  13. public interface Callback {
  14.     public boolean handleMessage(Message msg);
  15. }
  16. private static void handleCallback(Message message) {
  17.     message.callback.run();
  18. }
  19. public void handleMessage(Message msg) {
  20. }
复制代码
该方法的主要作用
对于 Message 来说,就是回调接口的作用:Message 对象持有Handler对象,通过调用这个 Handler 对象的 dispatchMessage方法,把 Message 对象回调给了 Handler。
对于 Handler 来说,就是分发消息的作用:


  • 如果 Message 对象的 callback 字段不为空,那么这个消息内部持有了一个 Runnable 对象,就调用 handleCallback 方法来运行那个 Runnable 对象封装的代码;
  • 如果 Message 对象的 callback 字段为空,而 mCallback 对象不为 null,就使用 Callback 类的 handleMessage 方法来处理消息;这个 mCallback 是在 Handler 的构造方法里面完成赋值的,使用这个回调的好处是不必要为了重写 Handler 类的 handleMessage 方法而去子类化 Handler;
  • 如果 Message 对象的 callback 字段为空,且mCallback 对象为 null,就使用 Handler 类的 handleMessage 方法来处理消息了。
从获取消息来看
Handler 封装了一系列的 obtainMessage 工具方法,方便我们拿到 Message 对象。
从移除消息来看
Handler 封装了 removeXXX 方法,内部委托给 MessageQueue 对象去做真正的工作。
  1. public final void removeMessages(int what) {
  2.     mQueue.removeMessages(this, what, null);
  3. }
复制代码
总结一下:使用 Handler 可以组合 MessageQueue 对象和 Looper 对象,可以发送消息,可以处理消息,可以获取消息对象,可以移除消息,所以说Handler 是 Android 消息机制的上层接口。
1.6 Android 消息机制的整体流程是什么?


图解:


  • 在主线程创建 Handler 对象 handler,默认使用的是主线程的 Looper 对象以及对应的 MessageQueue 对象;
  • 在工作线程通过 Handler 对象 handler 的发送消息方法发送消息,最终通过 MessageQueue 对象的 enqueueMessage 方法把消息加入到消息队列中;
  • Looper.loop() 方法运行在创建 Handler 里的线程,在这里就是运行在主线程, Loop.loop() 方法不断从消息队列中获取符合条件的 Message 对象;
  • 获取到符合条件的 Message 对象后,通过 Message 对象持有的 target 字段(实际就是发送该消息的那个 Handler 对象)的 dispatchMessage 方法把消息回调给发送消息的那个 Handler,这样消息就在主线程接收到了。
2. Android 的消息机制分析

2.1 ThreadLocal 的使用场景有哪些?

场景一:当某些数据是以线程为作用域并且不同线程具有不同的线程副本的时候,考虑使用 ThreadLocal。
对于 Handler 来说,它需要获取当前线程的 Looper(Looper 的作用域就是线程并且不同线程具有不同的 Looper),这时候使用 ThreadLocal 就可以轻松实现 Looper 在线程中的存取。
对于 SimpleDateFormat 来说,它不是线程安全的,也就是说在多线程并发操作时,会抛出异常。看演示代码如下:
  1. public class SimpleDateFormatDemo1 {
  2.     private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3.     public static void main(String[] args) {
  4.         ExecutorService threadPool = new ThreadPoolExecutor(
  5.                 5, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));
  6.         List<String> data = Arrays.asList(
  7.                 "2021-03-01 00:00:00",
  8.                 "2020-01-01 12:11:40",
  9.                 "2019-07-02 23:11:23",
  10.                 "2010-12-03 08:22:33",
  11.                 "2013-11-29 10:10:10",
  12.                 "2017-09-01 14:14:14",
  13.                 "2021-04-01 15:15:15"
  14.         );
  15.         for (String date : data) {
  16.             threadPool.execute(new Runnable() {
  17.                 @Override
  18.                 public void run() {
  19.                     try {
  20.                         System.out.println(sdf.parse(date));
  21.                     } catch (Exception e) {
  22.                         e.printStackTrace();
  23.                     }
  24.                 }
  25.             });
  26.         }
  27.         threadPool.shutdown();
  28.     }
  29. }
复制代码
运行这段程序,会出现这样的异常:
  1. java.lang.NumberFormatException: For input string: ".103E2103E2"
  2.         at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
  3.         at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
  4.         at java.lang.Double.parseDouble(Double.java:538)
  5.         at java.text.DigitList.getDouble(DigitList.java:169)
  6.         at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
  7.         at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
  8.         at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
  9.         at java.text.DateFormat.parse(DateFormat.java:364)
  10.         at com.java.advanced.features.concurrent.threadlocal.SimpleDateFormatDemo1$1.run(SimpleDateFormatDemo1.java:45)
  11.         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  12.         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  13.         at java.lang.Thread.run(Thread.java:748)
复制代码
为了解决 SimpleDateFormat 线程不安全的问题,我们可以使用一个 synchronized修饰的方法封装其 parse 方法,但是这样多线程下会竞争锁,效率不高。使用 ThreadLocal 来为每个线程创建一个专属的 SimpleDateFormat 对象副本,当一个线程下需要获取 SimpleDateFormat 对象进行操作时,它获取的是它自己的那个副本,对其他线程的 SimpleDateFormat对象副本没有影响,这样就不会发生线程不安全的问题了。
  1. public class SimpleDateFormatDemo2 {
  2.     private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(new Supplier<SimpleDateFormat>() {
  3.         @Override
  4.         public SimpleDateFormat get() {
  5.             return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  6.         }
  7.     });
  8.     public static void main(String[] args) {
  9.         ExecutorService threadPool = new ThreadPoolExecutor(
  10.                 5, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));
  11.         List<String> data = Arrays.asList(
  12.                 "2021-03-01 00:00:00",
  13.                 "2020-01-01 12:11:40",
  14.                 "2019-07-02 23:11:23",
  15.                 "2010-12-03 08:22:33",
  16.                 "2013-11-29 10:10:10",
  17.                 "2017-09-01 14:14:14",
  18.                 "2021-04-01 15:15:15"
  19.         );
  20.         for (String date : data) {
  21.             threadPool.execute(new Runnable() {
  22.                 @Override
  23.                 public void run() {
  24.                     try {
  25.                         System.out.println(threadLocal.get().parse(date));
  26.                     } catch (Exception e) {
  27.                         e.printStackTrace();
  28.                     }
  29.                 }
  30.             });
  31.         }
  32.         threadPool.shutdown();
  33.     }
  34. }
复制代码
多次运行程序,都可以正常解析了。
实际上,JDK1.8 提供了线程安全的 DateTimeFormatter 来替代线程不安全的 SimpleDateFormat。这里就不详细说明了。
场景二:使用 ThreadLocal来进行复杂逻辑下的对象传递。
比如一个线程中的任务,它的函数调用栈比较深,或者说调用链有不能修改的第三方库,这时我们想要传递一个监听器参数进去,该怎么办呢?
如果都是自己的代码,可以修改给调用链上的每一个函数增加监听器参数,但是这样改动的地方很多,容易出错,也很麻烦;
如果调用链有不可以改动的第三方库,可以将监听器作为静态变量供线程访问,但是在多线程下每个线程都要有自己的监听器对象,我们就需要用一个集合(以线程名为键,以监听器为值)来管理这些监听器了。这样多线程下在获取指定的监听器的时候,还是会存在就集合的竞争。所以不好。
使用 ThreadLocal 就可以解决这个问题。代码如下:
  1. private static ThreadLocal<Runnable> runnableThreadLocal = new ThreadLocal<>();
  2. public void threadLocalargs(View view) {
  3.    new Thread("thread1") {
  4.        @Override
  5.        public void run() {
  6.            task();
  7.        }
  8.    }.start();
  9.    new Thread("thread2") {
  10.        @Override
  11.        public void run() {
  12.            task();
  13.        }
  14.    }.start();
  15. }
  16. private void task() {
  17.     Runnable runnable = () -> Log.d(TAG, "run: " + Thread.currentThread().getName());
  18.     runnableThreadLocal.set(runnable);
  19.     method1();
  20. }
  21. private void method1() {
  22.     method2();
  23. }
  24. private void method2() {
  25.     method3();
  26. }
  27. private void method3() {
  28.     runnableThreadLocal.get().run();
  29. }
复制代码
打印日志如下:
  1. D/MainActivity: run: thread1
  2. D/MainActivity: run: thread2
复制代码
2.2 为什么 ThreadLocal 可以在多个线程中互不干扰地存储和修改数据?

先来看下 ThreadLocal 的基本使用:
  1. private static ThreadLocal<Boolean> sBooleanThreadLocal = new ThreadLocal<>();
  2. private static ThreadLocal<String> sStringThreadLocal = new ThreadLocal<>();
  3. public void threadLocal_basic(View view) {
  4.     sBooleanThreadLocal.set(true);
  5.     log();
  6.     new Thread("Thread#1") {
  7.         @Override
  8.         public void run() {
  9.             super.run();
  10.             sBooleanThreadLocal.set(false);
  11.             sStringThreadLocal.set(Thread.currentThread().getName());
  12.             log();
  13.         }
  14.     }.start();
  15.     new Thread("Thread#2"){
  16.         @Override
  17.         public void run() {
  18.             super.run();
  19.             sStringThreadLocal.set(Thread.currentThread().getName());
  20.             log();
  21.         }
  22.     }.start();
  23. }
  24. private void log() {
  25.     Log.d(TAG, "["+ Thread.currentThread().getName() +"]"+ "sBooleanThreadLocal.get()=" + sBooleanThreadLocal.get());
  26.     Log.d(TAG, "["+ Thread.currentThread().getName() +"]"+ "sStringThreadLocal.get()=" + sStringThreadLocal.get());
  27. }
复制代码
打印日志如下:
  1. D/MainActivity: [main]sBooleanThreadLocal.get()=true
  2. D/MainActivity: [main]sStringThreadLocal.get()=null
  3. D/MainActivity: [Thread#1]sBooleanThreadLocal.get()=false
  4. D/MainActivity: [Thread#1]sStringThreadLocal.get()=Thread#1
  5. D/MainActivity: [Thread#2]sBooleanThreadLocal.get()=null
  6. D/MainActivity: [Thread#2]sStringThreadLocal.get()=Thread#2
复制代码
可以看到,虽然我们在不同的线程中访问的都是同样的 ThreadLocal 对象,但是它们从 ThreadLocal 对象中获取的值,正好就是设置的值或者是默认值。
这里我们看的是 JDK1.8 的源码。
我们先来看一下相关的类关系吧:


  • 每个Thread对象都持有一个ThreadLocal.ThreadLocalMap 对象threadLocals;
  • ThreadLocalMap 是 ThreadLocal 的静态内部类,ThreadLocalMap里面维护了一个Entry数组table`;
  • Entry 是 ThreadLocal 的静态内部类,封装了一个键值对,key 是 ThreadLoal对象,value 是 ThreadLocal 的泛型对应的值。
绘制类关系图如下:

核心代码如下:
[code]public class ThreadLocal {    static class ThreadLocalMap {        static class Entry extends WeakReference

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作