1. 怎样创建Looper?

Looper的构造方法为private,所以不能直接使用其构造方法创建。

private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

要想在当前线程创建Looper。需使用Looper的prepare方法,Looper.prepare()。

假设如今要我们来实现Looper.prepare()这种方法,我们该怎么做?我们知道,Android中一个线程最多仅仅能有一个Looper,若在已有Looper的线程中调用Looper.prepare()会抛出RuntimeException(“Only one Looper may be created per thread”)。

面对这种需求,我们可能会考虑使用一个HashMap,当中Key为线程ID,Value为与线程关联的Looper,再加上一些同步机制,实现Looper.prepare()这种方法,代码例如以下:

public class Looper {

    static final HashMap<Long, Looper> looperRegistry = new HashMap<Long, Looper>();

    private static void prepare() {
synchronized(Looper.class) {
long currentThreadId = Thread.currentThread().getId();
Looper l = looperRegistry.get(currentThreadId);
if (l != null)
throw new RuntimeException("Only one Looper may be created per thread");
looperRegistry.put(currentThreadId, new Looper(true));
}
}
...
}

上述方法对Looper.class对象进行了加锁。这些加锁开销有可能造成性能瓶颈。

有没有更好的方法实现Looper.prepare()方法?看一看Android的中Looper的源代码。

public class Looper {

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    public static void prepare() {
prepare(true);
} private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
...
}

prepare()方法中调用了ThreadLocal的get和set方法。然而整个过程没有加入同步锁,Looper是怎样实现线程安全的?

2. ThreadLocal

ThreadLocal位于java.lang包中,下面是JDK文档中对该类的描写叙述

Implements a thread-local storage, that is, a variable for which each thread has its own value. All threads share the same ThreadLocal object, but each sees a different value when accessing it, and changes made by one thread do not affect the other threads. The implementation supports null values.

大致意思是,ThreadLocal实现了线程本地存储。

全部线程共享同一个ThreadLocal对象,但不同线程仅能訪问与其线程相关联的值。一个线程改动ThreadLocal对象对其它线程没有影响。

ThreadLocal为编写多线程并发程序提供了一个新的思路。例如以下图所看到的,我们能够将ThreadLocal理解为一块存储区,将这一大块存储区切割为多块小的存储区。每一个线程拥有一块属于自己的存储区,那么对自己的存储区操作就不会影响其它线程。对于ThreadLocal<Looper>,则每一小块存储区中就保存了与特定线程关联的Looper。

3. ThreadLocal的内部实现原理

3.1 Thread、ThreadLocal和Values的关系

Thread的成员变量localValues代表了线程特定变量,类型为ThreadLocal.Values。由于线程特定变量可能会有多个,而且类型不确定,所以ThreadLocal.Values有一个table成员变量,类型为Object数组。这个localValues能够理解为二维存储区中与特定线程相关的一列。

ThreadLocal类则相当于一个代理。真正操作线程特定存储区table的是其内部类Values。



3.2 set方法

public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
} Values values(Thread current) {
return current.localValues;
}

既然与特定线程相关,所以先获取当前线程,然后获取当前线程特定存储,即Thread中的localValues,若localValues为空。则创建一个,最后将value存入values中。

void put(ThreadLocal<?> key, Object value) {
cleanUp(); // Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1; for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index]; if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
} if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
} // Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
} // Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}

从put方法中,ThreadLocal的reference和值都会存进table,索引分别为index和index+1。

对于Looper这个样例,

table[index] = sThreadLocal.reference;(指向自己的一个弱引用)

table[index + 1] = 与当前线程关联的Looper。

3.3 get方法

public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
} return (T) values.getAfterMiss(this);
}

首先取出与线程相关的Values,然后在table中寻找ThreadLocal的reference对象在table中的位置。然后返回下一个位置所存储的对象。即ThreadLocal的值,在Looper这个样例中就是与当前线程关联的Looper对象。

从set和get方法能够看出,其所操作的都是当前线程的localValues中的table数组。所以不同线程调用同一个ThreadLocal对象的set和get方法互不影响,这就是ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。

4. ThreadLocal背后的设计思想Thread-Specific Storage模式

Thread-Specific Storage让多个线程能够使用同样的”逻辑全局“訪问点来获取线程本地的对象。避免了每次訪问对象的锁定开销。

4.1 Thread-Specific Storage模式的起源

errno机制被广泛用于一些操作系统平台。

errno 是记录系统的最后一次错误代码。对于单线程程序。在全局作用域内实现errno的效果不错,但在多线程操作系统中,多线程并发可能导致一个线程设置的errno值被其它线程错误解读。

当时非常多遗留库和应用程序都是基于单线程编写,为了在不改动既有接口和遗留代码的情况下。解决多线程訪问errno的问题,Thread-Specific Storage模式诞生。

4.2 Thread-Specific Storage模式的整体结构

线程特定对象,相当于Looper。

线程特定对象集包括一组与特定线程相关联的线程特定对象。

每一个线程都有自己的线程特定对象集。

相当于ThreadLocal.Values。

线程特定对象集能够存储在线程内部或外部。Win32、Pthread和Java都对线程特定数据有支持,这种情况下线程特定对象集能够存储在线程内部。

线程特定对象代理,让client能够像訪问常规对象一样訪问线程特定对象。假设没有代理,client必须直接訪问线程特定对象集并显示地使用键。

相当于ThreadLocal<Looper>。

从概念上讲。可将Thread-Specific Storage的结构视为一个二维矩阵,每一个键相应一行。每一个线程相应一列。第k行、第t列的矩阵元素为指向相应线程特定对象的指针。线程特定对象代理和线程特定对象集协作,向应用程序线程提供一种訪问第k行、第t列对象的安全机制。

注意。这个模型仅仅是类比。实际上Thread-Specific Storage模式的实现并非使用二维矩阵,由于键不一定是相邻整数。

參考资料

  1. Thread-local storage
  2. 面向模式的软件架构·卷2:并发和联网对象模式

Android怎样保证一个线程最多仅仅能有一个Looper?的更多相关文章

  1. 一个线程加一运算,一个线程做减一运算,多个线程同时交替运行--synchronized

    使用synchronized package com.pb.thread.demo5; /**使用synchronized * 一个线程加一运算,一个线程做减法运算,多个线程同时交替运行 * * @a ...

  2. Oracle数据库软件标准版的一个限制:仅仅能用一个rman channel

    Oracle数据库软件标准版的一个限制:仅仅能用一个rman channel Restrictions in "Standard Edition" Rman channel all ...

  3. 写两个线程,一个线程打印1-52,另一个线程打印A-Z,打印顺序为12A34B56C......5152Z

    题目: 写两个线程,一个线程打印1-52,另一个线程打印A-Z,打印顺序为12A34B56C......5152Z.要求用线程间的通信. /** * 写两个线程,第一个线程打印1-52,第二个线程打印 ...

  4. 当一个线程进入某个对象的一个 synchronized 的实例方 法后,其它线程是否可进入此对象的其它方法?

    如果其他方法没有 synchronized 的话,其他线程是可以进入的. 所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的.

  5. 2种方式(线程间通信/互斥锁)实现两个线程,一个线程打印1-52,另一个线程打印字母A-Z,打印顺序为12A34B56C......5152Z

    //2019/06/13 本周HT面试遇到的问题,答得不是很好,自己重新做一下.面试只需要写出线程间通信的方式,//我当时大致知道思路,因为之前看过马士兵老师的多线程视频,但是代码写出来估计编译都是报 ...

  6. 写2个线程,其中一个线程打印1~52,另一个线程打印A~z,打印顺序应该是12A34B45C……5152Z

    我写的 class LN { private int flag = 0; public static char ch = 'A'; public static int n = 1; public sy ...

  7. java中最简单的方式新起一个线程

    启动一个线程在一个方法中启动一个线程,有两种方法第一种是让类实现Runable接口,这样的话编译器就会提示你实现里面的未实现的方法(就是run方法)第二种是,现在方法中new一个线程,然后直接调用他的 ...

  8. linux程序设计——取消一个线程(第十二章)

    12.7    取消一个线程 有时,想让一个线程能够要求还有一个线程终止,就像给它发送一个信号一样. 线程有方法能够做到这一点,与与信号处理一样.线程能够被要求终止时改变其行为. pthread_ca ...

  9. Linux内核驱动将多个C文件编译成一个ko文件的方法——每一个C文件中都有module_init与module_exit

    以两个C文件为例: 将本该被分别编译成adc_device.ko和adc_driver.ko的adc_device.c.adc_driver.c编译成一个ko文件! 採用方法: 第一步.改动C文件 1 ...

随机推荐

  1. MERGE INTO USING用法

    MERGE INTO [your table-name] [rename your table here] USING ( [write your query here] )[rename your ...

  2. 并发编程学习笔记(4)----jdk5中提供的原子类及Lock使用及原理

    (1)jdk中原子类的使用: jdk5中提供了很多原子类,它会使变量的操作变成原子性的. 原子性:原子性指的是一个操作是不可中断的,即使是在多个线程一起操作的情况下,一个操作一旦开始,就不会被其他线程 ...

  3. Vue指令的概念

    指令(Directives) 是带有v- 前缀的特殊属性,指令属性是单一的js表达式. 指令的职责就是表达式的值发生变化时,在DOM中做出相应的回应. 如下例子: 实例 <div id=&quo ...

  4. VMware vCenter 6.5 安装及群集配置介绍

    一.介绍 VMware vCenter Server 提供了一个可伸缩.可扩展的平台,为虚拟化管理奠定了基础.可集中管理VMware vSphere环境,与其他管理平台相比,极大地提高了 IT 管理员 ...

  5. CAD绘制一个单行文字(com接口VB语言)

    主要用到函数说明: _DMxDrawX::DrawText 绘制一个单行文字.详细说明如下: 参数 说明 DOUBLE dPosX >文字的位置的X坐标 DOUBLE dPosY 文字的位置的Y ...

  6. gitlab恢复、迁移

    文件说明 安装包:gitlab-ce_8.11.5-ce.0_amd64.deb 备份的数据:533751277_gitlab_backup.tar 系统:Ubuntu 16.04.4 LTS \n ...

  7. iOS的影片播放 MediaPlayer 和 AVPlayer

    在iOS開發上,如果遇到需要播放影片,如開機動畫…,我們很習慣地會使用MediaPlayer來播放影片,因為很方便使用,所以就一直使用下去.但是隨著客戶的要求越來越嚴苛,尤其是過場動畫或互動效果上的表 ...

  8. javaHashcode与equals

    转载自:http://blog.csdn.net/jiangwei0910410003/article/details/22739953 Java中的equals方法和hashCode方法是Objec ...

  9. Python学习-字符串的基本知识

    字符串的基本知识 根据所展示形式的不同,字符串也可以分为两类 原始字符串: 使用单引号包括:‘liuwen’ 使用双引号包括:"liuwen" 使用3个单引号包括 :'''liuw ...

  10. MySQL Docker方式安装

    以5.7版本为例 1 配置mysql配置文件编辑/etc/my.cnf,添加以下内容: [mysqld] skip-host-cache skip-name-resolve datadir=/var/ ...