前言众所周知,Android会在UI线程中调用ViewRootImpl中的checkThread方法检测UI是否更新//ViewRootImpl.javafinalThreadmThread;publicViewRootImpl(Contextcontext,Displaydisplay){mThread=Thread.currentThread();}voidcheckThread(){if(mThread!=Thread.currentThread()){thrownewCalledFromWrongThreadException("仅创建视图层次结构的原始线程可以触摸它的视图。");}}为什么Android只能在UI线程UI更新,不能在子线程更新UI1?为什么Android不使用多线程来更新UI?为什么GUI框架是单线程的2早期的GUI应用程序是单线程的,GUI事件在“主事件循环”中处理。目前的GUI框架使用了一种稍微不同的模型:在这个模型中,创建了一个专门的事件派发线程(EventDispatchThread,EDT)来处理GUI事件。单线程的GUI框架不限于Java,在Qt、NexiStep、MacOSCocoa、XWindows等GUI框架都是单线程的。很多人都尝试过写一个多线程的GUI框架,但是由于静态条件和死锁带来的稳定性问题,最后都回到了单线程的事件队列模型:使用专用线程从队列中提取事件,并将它们转发给应用程序定义的事件处理程序。死锁问题更容易发生在多线程GUI框架中,部分原因是在输入事件的处理过程中与GUI组件的面向对象模型的交互不正确。用户启动的操作以一种“气泡上升”的方式从操作系统传递到应用程序:操作系统首先检测到鼠标单击,然后由工具包将其转换为“鼠标单击”事件,最终转换为转发给应用程序侦听器的更高级别的事件(例如“按下鼠标左键”事件)。另一方面,由应用程序启动的操作从应用程序“冒泡”回到操作系统。例如,如果在应用程序中触发修改某个组件的背景颜色的请求,该请求将被转发到特定的组件,最后转发给操作系统进行绘制。因此,一方面,这组操作将以完全相反的顺序访问相同的GUI对象;另一方面,确保每个对象都是线程安全的会导致不一致的锁定顺序并导致死锁。多线程GUI框架中死锁的另一个原因是模型-视图-控制(MVC)设计模式的广泛使用。通过将用户交互分解为模型、视图和控件等模块,可以大大简化GUI应用程序的实现,但这也进一步增加了锁定顺序不一致的风险。单线程GUI框架通过线程关闭机制实现线程安全。所有GUI对象,包括可视化组件和数据模型,都只能在事件线程上访问。当然,这只是将确保线程安全的部分工作留给了应用程序开发人员,他们必须确保这些对象被正确地封装在事件线程中。从以上文档不难发现,早期的前辈们尝试过多线程的GUI框架,但都以“失败”告终,又回到了单线程的事件队列模型。存在线程安全问题线程安全的三大害处什么是线程安全3线程安全的确切定义是非常复杂的。定义越正式,越复杂,不仅难以提供实践指导,也难以直观理解。因此,下面给出了一些非正式的描述,这可能看起来令人困惑。网上可以找到很多“定义”,比如:可以在多线程中调用,线程间交互不会出错。可以被多个线程同时调用,调用者不需要执行额外的动作。看看这些定义,难怪我们会对线程安全感到困惑。它们听起来很像“如果可以从多个线程安全地使用一个类,它就是线程安全的”。这种说法虽然没有太大的争议,但也不会带来太大的帮助。我们如何区分线程安全类和非线程安全类?此外,“安全”是什么意思?在线程安全的定义中,核心概念是正确性。如果说线程安全的定义是模糊的,那是因为没有明确的正确性定义。正确性意味着一个类的行为完全符合其规范所指定的。在一个好的规范中,通常会定义各种不变条件(Invariant)来约束对象的状态,定义各种后置条件(Postcondition)来描述对象操作的结果。由于我们通常不编写详细的规范,我们如何知道这些类是否正确?我们不知道,但这并不妨碍我们在确定“类的代码有效”后使用它们。这种“代码可信”与我们对正确性的理解非常接近,因此我们可以将单线程正确性大致定义为“看到就知道”。在对“正确性”给出更明确的定义之后,我们就可以定义线程安全了:当多个线程访问某个类时,这个类总能表现出正确的行为,那么这个类就称为线程安全的。由于单线程程序也可以看作是多线程程序,如果某个类在单线程环境下不正确,那么它一定不是线程安全的。BigEvil-Visibility可见性是一个复杂的属性,因为可见性的错误总是违背我们的直觉。4上个世纪的单核时代,所有线程都在唯一的CPU上执行,CPU缓存和内存就像一对,你的就是我的,我的就是你的,不分彼此因为只有一个CPU而且只有一个CPU缓存,所以一个线程花费了oceans,另外一个thread必须能够看到还剩多少个ocean。比如下图中,内存中一共有100个大洋。如果线程A花费了20个大洋,线程B想要花费更多,它只能花费80个大洋。一个线程可以修改共享变量,另一个线程可以立即看到它。它被称为可见性。21世纪的多核时代,每个CPU都有自己的缓存。这时候CPU缓存和内存的关系就不好讨论了,就像古代皇帝(内存)后宫佳丽三千一样。(CPU),皇上跟众美女说我们的国库够用:100两白银,美女们都知道国库有100两银子,皇后要花80两买口罩(操作CPU-1缓存),贵妃要花70两买BB霜(运行CPU-2缓存),过了一会皇上一看(CPU缓存同步到内存),国库还有30两银子。比如下图中,内存有100两银子,线程A花了80两,然后同步到内存,皇上看到金库还有20两,线程B花了70两,然后同步到记忆中,金库变成了30两个?啧啧,国库花的银子越来越多了。这里要说明一下,线程A对共享变量的操作对于线程B是不可见的,接下来我们用一段代码来看一下可见性问题。下面的代码4显示了当多个线程共享数据而不同步时出错。在代码中,主线程和读取线程都会访问共享变量ready和number。主线程启动读取器线程,然后将数字设置为45并准备好为真。读线程一直循环,直到发现ready的值变为true,然后输出number的值。尽管程序看起来输出45,但实际上可能输出0,或者根本无法终止。这是因为代码中没有使用足够的同步机制,所以不能保证主线程写入的ready和number值对读线程可见。publicclassVisibilityTest{privatestaticbooleanready;私有静态整数;privatestaticclassReaderThreadextendsThread{@Overridepublicvoidrun(){while(!ready){Thread.yield();}System.out.println("数字="+数字);}}publicstaticvoidmain(String[]args){newReaderThread().start();准备好=真;数=45;}}二恶英-原子性类似于可见性,原子性也是一个复杂的属性,因为原子性的错误也违背了我们的直觉。原子性:指一条或多条指令(操作)在CPU执行过程中不被中断、不可分割的特性。这里要强调的是,在CPU指令(操作)的执行过程中,是指CPU指令(操作)是原子性的。级别而不是语言级别下面介绍两种常见的原子错误形式read-modify-write接下来我们用一段代码来看一下“read-modify-write”问题。下面的代码是用Kotlin写的,在reduceCount方法中循环执行count--操作10万次,两个线程分别执行reduceCount方法。可以先想想程序运行后的输出计数。funmain(){valthread1=thread{reduceCount()}valthread2=thread{reduceCount()}thread1.join()thread2.join()println("count=$count")}//200,000varcount:Long=200000LprivatefunreduceCount(){//循环100,000次for(iin1..100000){count--}}直观上应该是count为0,但是程序输出的count是0到100000之间的随机数,为什么是这样?尽管减量操作count--是一种紧凑的语法,使其看起来只是一个操作,但该操作不是原子操作,因此不会作为不可分割的操作执行。其实包括三个独立的操作:读count的值,给value加1,然后把第二步的结果写入count。以上三个操作,count的初始值都是200000,那么在某种情况下,两个线程都读到一个200000的值,然后进行自减操作,都将count的值设置为199999。这显然不是我们所期望的。这种由于执行时机不当导致的不正确结果是一种非常重要的情况,称为:竞态条件(RaceCondition)先检查再执行是最常见的竞态条件类型,即通过一个可能的条件来决定下一步的动作无效的观察结果。先检查再执行的问题中常见的情况是下面这种形式的代码://checkif(condition){//action}我们来看看通过惰性初始化来“先检查再执行”的问题。惰性初始化的目的是将对象Defer到实际使用时才进行初始化,并保证只初始化一次3。公共类LazyInitRace{私有LazyInitRace实例=null;publicLazyInitRacegetInstance(){//首先检查实例是否已经初始化,如果已经初始化,则返回现有实例if(instance==null){//否则,将创建一个新实例instance=newLazyInitRace();}返回实例;}privateLazyInitRace(){}}LazyInitRace中存在竞争条件,可能会破坏该类的正确性。假设线程A和线程B同时执行getInstance。线程A看到该实例为空并创建一个新的LazyInitRace实例。线程B还需要判断实例是否为空。此时实例是否为空取决于不可预知的时机,包括线程是如何调度的,以及线程A需要多长时间来初始化LazyInitRace并设置实例。如果在线程B检查时instance为null,那么调用getInstance两次可能会得到不同的结果,即使通常认为getInstance返回的是同一个实例。3比如下图中,线程A在执行完“check”阶段后进行线程调度(切换),线程A和线程B在图中依次执行,最后发现两个线程都创建了一个新的LazyInitRace实例,但这不是想要的结果。三害——有序性在可见性部分的示例代码中有一种情况:number很可能输出0,因为读线程可能看到的是写入ready的值,但看不到后面写入number的值,就是这种现象称为“重新排序(Reordering)”。在没有同步的情况下,编译器、处理器和运行时可能会对操作的执行顺序做出一些意想不到的调整。在缺乏足够同步的多线程程序中,如果要判断内存操作的执行顺序,几乎不可能得出正确的结论。publicclassVisibilityTest{privatestaticbooleanready;私有静态整数;privatestaticclassReaderThreadextendsThread{@Overridepublicvoidrun(){while(!ready){Thread.yield();}System.out.println("数字="+数字);}}publicstaticvoidmain(String[]args){newReaderThread().start();准备好=真;数=45;}}顺序是指程序按照代码的顺序执行下面的代码示例5来说明顺序问题。在程序PossibleReordering中,说明了5。在没有适当同步的情况下,即使是最简单的并发程序也很难推断出其行为。很容易想象PossibleReordering如何输出(1,0)or(0,1)or(1,1):线程A可以在线程B启动之前完成执行,并且线程B可以在线程A启动之前完成执行,或者操作两者交替进行。但奇怪的是,PossibleReordering也可以输出(0,0)。由于每个线程中的操作之间没有数据流依赖性,因此可以乱序执行操作。(虽然这些操作是顺序执行的,但有可能在缓存刷新到主存的不同时刻,线程A中的赋值操作从线程B的角度来看可能是倒序执行的。)publicclassPossibleReordering{private静态整数x=0;私人静态y=0;私有静态整数a=0;私人静态intb=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadthread1=newThread(()->{a=1;x=b;});线程thread2=newThread(()->{b=1;y=a;});thread1.start();thread2.start();线程1.join();thread2.join();System.out.println("("+x+","+y+")");}}下图显示了可能由重新排序Executionmode引起的交替,本例中会输出(0,0)。总结本文从多线程并发编程中线程安全的角度解释了为什么AndroidUI框架是单线程的。估计AndroidUI框架的设计者也是借鉴了前人多线程中的线程安全问题,所以采用了单线程的封闭机制来实现线程安全。说明与参考UI也可以在子线程中更新,这里就不赘述了。
