Java多线程&并发知识细节(一)

        众所周知,多线程是Java中一个很重要,很基础的一个概念,无论是Android应用也好,还是Java应用也好,会有很多地方需要考虑多线程。在实际开发中,之所以要开启多线程,是为了最大化cpu的多核处理能力。如果应用只用一个线程来处理所有的逻辑,那么就会造成CPU资源的浪费。开启了多线程之后,就让我们的应用具备了并发处理逻辑的能力。

一:线程和任务

       多线程里面最重要的就是线程,线程可以理解为系统可以切分的最小单元。线程存在的目的是为了处理任务,而任务的执行又必须依赖某个线程来执行。在Java中,代表线程的是Thread,而代表任务的是Runable,Callable,首先来看一下Thread。

      Thread是大家很熟悉的一个知识点,所以一些简单的细节就略过。Thread会通过start的方式来启动,然后虚拟机会调用它的run函数来执行相应的任务,等run函数执行完毕,线程也就结束了。所以一般如果我们想让一个线程长时间存活,那么就别让它从run函数中返回,一般就是循环。

       一个线程不可以被重复start,否则会抛出IllegalThreadStateException。针对于线程的状态,被封装在了内部的枚举类State中,总共包括NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING和TERMINATED五种状态。大部分都很好理解,需要注意的是WAITING和TIMED_WAITING这两个状态,当调用LockSupport的park函数的时候,也会让线程处于WAITING状态,LockSupport是个很重要的类,在后面学习锁或者其他同步装置的时候,会频繁看到它。

       线程还有一个ThreadGroup的概念,类似于View和ViewGroup的关系。每个Thread属于某个ThreadGroup,而ThreadGroup里面还可以有其他的ThreadGroup,这样就形成了一个Thread Tree。Android UI线程的ThreadGroup名字是main,在这个Thread Tree,最根本的是system的ThreadGroup,它不属于任何一个ThreadGroup:

ThreadGroup

        学习ThreadGroup的另一个目的就是UncaughtExceptionHandler:

UncaughtExceptionHandler

        这个接口可以理解为未捕获异常处理器,当一个线程抛出了一个未捕获的异常,系统的处理顺序是这样的。首先,虚拟机会调用当前线程的getUncaughtExceptionHandler函数,如果当前线程没有指定uncaughtExceptionHandler字段,那么所属的ThreadGroup就会承担这个角色:

getUncaughtExceptionHandler

       而ThreadGroup实现了UncaughtExceptionHandler接口:

ThreadGroup

那么来看一下它的uncaughtException函数:

ThreadGroup&uncaughtException

       uncaughtException函数的处理逻辑是这样,如果当前ThreadGroup属于某个ThreadGroup的话,就调用parent的uncaughtException函数。前面我们说到,system的ThreadGroup不属于任何一个ThreadGroup。所以当调用到system的ThreadGroup的时候,就会调用getDefaultUncaughtExceptionHandler来获得一个defaultUncaughtExceptionHandler字段,而这个字段是Thread的静态字段。

        笔者通过debug调试,这个字段默认是有值的,是定义在RuntimeInit里面的一个内部类。同时,这也是我们平时开发中,写CrashHandler这样的崩溃处理器的时候,调用setDefaultUncaughtExceptionHandler来设置我们自定义的UncaughtExceptionHandler的原因。接下来在看Thread几个常用的函数:

           yield:这个函数的意思是当前线程放弃CPU的执行权,从而让其他的线程执行,也有可                        能是当前的线程再度抢到了CPU。但这个函数使用的机会很少,更多的是为了                            debug或者测试,在FutureTask中用到了这个函数,后面会介绍到。

           sleep:让当前的线程睡眠一段时间,它和定义在Object里面的wait函数很类似,区别                             在于wait会把持有的对象锁释放掉,而sleep则不会,并且wait需要通过notify来                           唤醒。

           interrupt():这是一组函数,类似的还有interrupted(),isInterrupted()和                                                           isInterrupted(boolean ClearInterrupted)。interrupt()函数会中断当前的线                                     程,然后设置当前线程的中断状态。即便如此,它并不一定会导致run函                                     数里正在执行的操作会结束。如果当前线程因为sleep,wait或者join函数                                    处于等待状态的时候,那么就会响应这个中断操作,会抛出                                                       InterruptedException,同时会把中断状态重置。还有像IO的一些操作也                                     会响应中断,抛出异常。其他的时候,只是设置了一个中断状态。所以我                                   们写代码的时候,就需要我们检查线程的中断状态。因为通常interrupt操                                   作没办法影响我们的业务逻辑,更多的时候是相关逻辑执行完毕的时候,                                   对返回的数据不处理。详细的可以参考这篇文章。

           join():这个函数的意思是等待调用这个函数的线程结束,如果没有结束,就等待,                                类似于wait的效果。这个函数主要用于线程间的协同,比如两个线程间有依                                赖,一个线程必须等另一个线程结束了之后才可以继续执行。但实际使用的                                并不多,相似的效果完全可以通过wait,Condition等来实现,所以只做了                                  解。他的实现原理也很简单,依赖的就是线程自己的wait和notify函数,然后                              通过不断循环判断线程是否存活,当线程结束的时候,会调用自己的notify函                              数,join函数也就可以返回了。

Thread$join

        除此之外,Thread还定义了isAlive(),holdsLock()和getState()等函数,比较简单,大家看源码即可理解。

二:任务

         前面我们提到,线程是为了执行某项任务,而任务必须依附于某个线程来执行。在Java中,代表任务的是Runnable和Callable。

        首先,这里先提一个很简单的问题,就是创建线程的方式。一般来说有三种方式:创建Thread的子类,重写run函数,另外两种方式就是分别借助于Runnable和Callable。对比而言,后两种方式,一方面方便了任务的复用,可以让一个任务拆解成多个线程并发处理,类似于任务的拆解;另一方面就是将任务的声明和任务的执行分开了,降低了耦合性。平时开发中,我们创建某个类型的子类,是为了对它的功能进行增强或更改,而我们创建Thread的子类,只是重写了run函数,没有其他的改动,所以实现Runnable,应该是更好的选择。

        回过头来,继续分析任务。Runnable和Callable基本类似,只不过Callable有返回值,而Runnable没有。Executors提供了工具函数,可以把Runnable转化为Callable:

Executors$callable

       而它的转化逻辑也很简单,使用了实现了Callable接口的RunnableAdapter,然后在call()函数里执行Runnable的run()函数,返回指定的结果。这是一个很巧妙的设计,很容易让人想到适配器模式,我们可以学习一下。

       但Callable本质上也只是一个任务,如果我们想获得这个任务的结果怎么办呢,就需要用到另外一个接口Future。Future一方面可以理解为某个异步任务的执行结果,并提供了获取结果的函数get()。如果任务还没有执行完毕,那么调用线程就会被阻塞,同时还可以设置超时时间。另一方面可以把Future理解为异步任务本身,可以对异步任务进行监听,比如判断是否结束,取消任务,任务是否被取消等等。这是一个很重要的思路,因为如果让笔者自己来设置这个框架的话,会想当然的认为Future代表返回结果,然后还会有另外一个类代表正在执行的任务,而Future是这个类的一个字段。其实完全没有必要,一个Future就可以完成所有的操作。简单来说,Future提供了两种函数:一种获得异步任务的执行结果,另一种取消任务。

        Future是一个接口,它的实现类是FutureTask,接下来分析一下FutureTask的实现原理。由于FutureTask代表的是异步任务的执行结果,所以它内部肯定会包装一个任务。由于Runnable和Callable可以相互转化,所以FutureTask提供了两个构造函数:

FutureTask构造函数

       FutureTask有一个int类型的state,来代表对应的异步任务的状态。在JDK中,很多类都会声明state,像Thread,FutureTask,ThreadPoolExecutor等等。这个地方给我们带来的启示就是,以后需要一些标识位的时候,不一定非得声明一系列的boolean变量,声明state可能是更好的选择,并且一般这种int类型的state都是可以相互比较大小的。FutureTask刚开始是NEW状态,其他的状态,后续我们会一一分析到。

       FutureTask实现了RunnableFuture接口,这个接口就是Runnable和Future的结合体,也就是任务和执行结果的结合体:

RunnableFuture

        一般使用FutureTask的场景有两个:构造Thread对象或者提交到ThreadPoolExecutor里执行,这时候,FutureTask就会作为任务,然后在相应线程里执行他的run函数:

FutureTask

       前面提到,FutureTask是对Runnable或者Callable代表的任务的封装,所以它的run函数里肯定会调用这些任务的逻辑。需要注意的是,FutureTask代表的是一个在任意一个线程执行一次的任务。也就是说,FutureTask的任务不需要重复执行,如果任务已经在某个线程里开始执行了或者任务已经执行完了,那么其他的线程直接通过get等待或者直接获取结果就好,根本不需要重复执行。

        明白了上面的结论,接下来看具体的实现。run函数中首先会对state进行判断,如果state不等于NEW,那么就意味着异步任务已经执行完毕了,无论是正常的执行也好,还是遇到了异常也好,总之不需要重复执行了。但这样还不够,因为有可能这个任务正在被某个线程执行中,还没有执行完毕,state还是NEW,那这个时候就通过后面的CAS运算来保证。这个运算通过UNSAFE来实现,在静态代码块中完成初始化:

UNSAFE

      FutureTask有一个Thread类型的runner字段,来代表执行对应任务的线程。通过UNSAFE的objectFieldOffset函数来得到runner字段在内存里的偏移量runnerOffset,然后通过compareAndSwapObject来修改。以这个地方的调用为例,他的意思是在runner==null的前提下,将它设置为当前的线程,否则的话就失败。UNSAFE是一个可以直接操作内存的类,compareAndSwapObject也可以保证原子性,不会出现线程安全问题。所以一旦某个线程已经开始执行了对应的任务,那么后续的线程在调用的时候,compareAndSwapObject就会返回false,就会进入到if里面的代码,直接return返回,避免了任务的重复执行。

        这里对于UNSAFE只是简单的介绍,真实开发中,这个类我们没有权限使用,使用的是相应的原子类。相应的线程安全问题的细节后面也会讲到,这个时候我们可以发现,FutureTask并没有使用到锁,使用的就是CAS和volatile的方式,来确保线程安全。

       确保了任务不会重复执行之后,接下来就调用call函数来执行任务的逻辑了,任务运行可能成功,也可能抛出异常,还有可能被取消。首先来看异常的情况,那么就会调用setException函数:

setException

        先是短暂的将状态设置为COMPLETING,这是一个瞬时状态。然后讲outcome设置为抛出的异常,outcome字段代表当前异步任务的执行结果,可能是个正常的结果,可能是个异常,然后将state设置为最终的EXCEPTIONAL。最后会调用finishCompletion()函数,这个函数后面在统一讲。

         如果执行顺利,就会调用set函数:

set函数

        和setException类似,设置outcome为计算的结果,最终状态设置为NORMAL,调用finishCompletion。最后再来看一下取消的情况,先来看一下cancel函数:

cancel

         首先还是对state的判断,这个判断逻辑的意思就是只有在state==new的情况下才可以取消,也就是任务还没开始执行或者还没执行完毕的时候才可以取消。换句话说,如果这个任务已经执行完毕,无论正常结束还是异常结束,或者已经被取消了,那么就不能在被cancel了。其中如果参数mayInterruptIfRunning==true的话,就会通过interrupt来中断正在执行任务的线程。并且也会根据mayInterruptIfRunning来把state配置为INTERRUPTING或者CANCELLED。最后,也是会调用finishCompletion()函数。

       我们再来详细分析一下任务取消可能出现的各种情况,如果当前的任务已经执行完毕,也就是set()或者setException()函数被调用了,或者任务已经被取消过了,那么cancel()函数的调用也就没了作用,简单的返回了false。排除了以上的情况,我们先来看mayInterruptIfRunning==false的情况。如果是在call()函数执行的过程中,调用了cancel,那么state会被设置为CANCELLED,然后cancel()返回true。但对于call()函数的执行没有影响,call()还是会执行,只不过在调用set的时候,由于此时的state已经被设置为了CANCELLED,那么set里面的逻辑就没办法执行了。这种情况下的取消就相当于没办法阻止任务的执行,但会对任务的结果不处理。再来看一下mayInterruptIfRunning==true的情况,如果在call()函数执行的时候,调用了cancel(),并且会interrupt正在执行任务的线程。这个时候就需要看对应的任务逻辑能不能响应interrupt了。如果不能响应interrupt,那么call()函数还是会执行完毕,但set()函数不起作用,最终的state是INTERRUPTED。如果能响应interrupt,那么就会调用setException函数,这个函数也不起作用,最终的state也是INTERRUPTED。如果是在call()执行完毕之后,set()调用之前调用的cancel()。那么就是set()函数不起作用,最终的state还是会被设置为INTERRUPTED。也就是说,FutureTask的state最终会是NORMAL,EXCEPTIONAL,CANCELLED和INTERRUPTED中的一个。

        分析完了取消任务的各种情况了之后,同时受影响的还有get函数:

get()

       首先还是判断state,如果<=COMPLETING,那意味着任务还没有安全执行完毕,所有的state里,只有初始状态NEW,是

report

       report需要考虑各种情况,如果任务正常执行完毕了,也就是NORMAL的时候,那么直接返回结果。如果任务被取消了,也就是CANCELLED,INTERRUPTING或者INTERRUPTED的时候,就会抛出CancellationException。其他的情况,就是任务执行的时候抛出异常了,就会抛出ExecutionException,并且cause是任务抛出的异常。也就是说,FutureTask的任务执行的时候即便发生了异常也不会被抛出,只有调用get()函数的时候才能知道具体的异常信息,这一点在使用线程池的时候需要注意。当任务还没有执行完毕的时候,那么调用get()的线程就会被阻塞,这个通过awaitDone()来实现:

awaitDone

       awaitDone是等待任务的执行完毕,并且会受到interrupt和time-out的影响,在此之前,会阻塞当前的线程。函数里首先是一层for循环,这个for循环个人感觉是对if语句的替代,因为里面的逻辑归根结底是对各种情况的处理,只不过用传统的if-else来做,会造成很多重复的代码,通过for循环来优化if-else的代码,这种思路我们也可以借鉴一下,并且在JDK中,这种用法也很普遍。在实际开发中,针对某个任务的结果感兴趣的可能会有多个线程,也就是说get()函数可能会被多个线程调用,然后阻塞这多个线程。FutureTask内部通过单链表的形式来记录这些线程,里面的节点是WaitNode:

WaitNode

        每个线程都会被封装在WaitNode里,for循环中首先检查这个线程是否被中断了,如果是的话,会抛出InterruptedException,并通过removeWaiter()函数将对应的节点删除,具体的链表节点删除操作这里就先略过了。接下来继续判断state,当s>COMPLETING的时候,就意味着任务已经执行完毕了,这个时候返回就好,不需要调用removeWaiter来删除节点,因为可以走到这个if里面来,就说明该线程已经从阻塞状态里出来了,FutureTask已经通过调用set(),setException或者cancel(),进而调用了finishCompletion函数。一方面通过LockSupport的unPark()函数来唤醒了线程,另一方面也已经把节点删除了:

finishCompletion()

        如果state==COMPLETING,意味着任务正在结束中,也就是正在执行set()或者setException()函数,这个时候就调用了yield函数,让出CPU执行权,同时会在以后的循环的时候会满足state>COMPLETING,函数也会正常退出了。如果q==null的时候,意味着线程还没有创建节点,先创建节点。如果queued==false,就意味着线程节点还没有加入链表,就把它加入到链表中,并设置为头节点。接下来就是执行阻塞的逻辑,根据有没有设置超时,来分别调用LockSupport的park和parkNanos函数。LockSupport是用来实现线程block的一个较为底层的类,后面分析锁的时候会进一步分析,这里先了解一下就好。

          在回到get()带有超时时间版本的函数中:

get(long timeout, TimeUnit unit)

        如果超时时间到了,任务还没有执行,就会抛出TimeoutException。TimeoutException的本意就是某项操作还没有执行完毕,但我可以设置一个等待的时间,如果设置了等待的时间,操作还是没有完成,那么就可以抛出这个异常。不过一般这种情形并一定要必须抛出异常,也可以通过返回特殊值,比如null,false之类的来标记失败就可以。

        至此,FutureTask的get()函数就分析完了。重点在两方面,一方面是FutureTask依赖于LockSupport实现了线程的阻塞和唤醒;另一方面就是for循环的经典使用。那么对于FutureTask而言,我们已经分析完了它的run(),get()和cancel()函数,了解了这些就了解了FutureTask工作的骨架。分析完了FutureTask的工作原理之后,我们来看一下它和Thread的使用。我们可以通过Callable来封装任务,然后用Callable来构造一个FutureTask。由于FutureTask又实现了RunnableFuture,所以可以作为一个Runnable来构造一个Thread,就类似于通过Runnable来实现线程的方式。这种方式的好处就是有返回值,同时也可以很好的解决某个线程依赖另一个线程的计算结果的问题。

三:线程安全问题

        前面我们分析了多线程最基础的两个概念线程和任务,算是给多线程打下了一个基础。但伴随着多线程的使用,就会产生线程安全问题。线程安全问题大家也很熟悉,当多个线程访问同一个资源的时候,如果某个线程对资源的更改不是原子操作,在执行完毕之前,就有可能因为被其他的线程抢到了CPU的执行权,从而让当前线程访问到了脏数据,然后就产生了业务的逻辑问题。

        对于线程安全问题,这里就不举例说明了,一些细节大家可以看这篇文章https://github.com/LRH1993/android_interview/blob/master/java/concurrence.md,这里只说总结一些结论。要想解决线程安全问题,就要从可见性,有序性和原子性这三个方面,只有这三个方面的问题都解决了,线程安全问题才可以得到解决。

        首先来看一下可见性,当多个线程访问同一个变量的时候,这个变量是存储在主内存的。每个线程都有自己的工作内存,那么当线程对这个变量进行修改的时候,就会先把变量的值从主内存加载到自己工作内存的缓存中,在工作内存完成修改,最后在刷新到主内存里。但有可能这三个步骤执行的过程中,被其他的线程抢先了,其他的线程也要读取这个变量,这个时候虽然线程更改了变量,但由于还没来得及刷新到主内存,所以其他线程从主内存拿到的变量值还是旧数据,是脏数据,那么逻辑就会出现问题,这就是可见性的问题。解决可见性的方案之一是volatile关键字。当变量被volatile修饰之后,线程对它的修改会被马上刷新到主内存,这样其他线程需要读取的时候,从主内存中拿到的就是新值。同时,一个线程修改了被volatile修饰的变量之后,会导致其他线程工作内存里的缓存失效,那么就会强迫他们再次向主内存请求最新的数据。也就是说,修改了被volatile修饰的变量之后,其他的线程无论是直接从主内存里读取还是从自己的工作内存缓存里读取,都能确保拿到的是新值。但这个需要发生在线程读取之前,如果是读取之后才修改的,那么就不会有影响了。

        下一个就是有序性,之所以要考虑有序性,是因为代码运行的时候,会发生指令重排序。处理器为了提供效率,可能会对代码进行重排序,但它会保证程序的运行结果和代码顺序执行的结果一致。如果其中前后代码有依赖的话,也就不会重排序了。虽然保证了和代码顺序执行的结果一致,但是它是从单线程的角度来保证的,如果发生在多线程的情况下,就会有问题。这里直接采用参考文章的例子了:

重排序例子

       这个例子中,从线程1的角度来讲,它的两行代码的执行顺序是无所谓的,没影响的,但如果线程1,2并行就会有影响。如果线程1的语句2先执行,带context并没有完成初始化,这个时候执行了线程2,那么由于context还是null,那么逻辑就会出现问题。针对有序性,Java已经做了一部分的保证,具体的就不列了,大家可以看这里。但这还不够,所以还需要再次用到volatile。volatile会禁止指令重排序。当变量被volatile修饰之后,它前面的语句肯定在变量前面执行,它后面的语句肯定会在变量后面执行。这个变量就相当于一个屏障,但是它并不保证变量前后语句内部自己的相对顺序。同时,当执行到变量的读写操作时,它前面的操作肯定已经执行完毕,而且执行结果对后面可见。针对刚才那个例子,如果inited被volatile修饰了之后,那么context的初始化肯定会发生在inited赋值之前,这样也就不会出现刚才的问题了。

        到此,针对线程安全问题的三个特性,volatiel已经解决了两个,但是原子性还是没有解决,而且volatile也解决不了。原子性指的是某个操作在业务上应该是原子的,也就是要么执行成功,要么失败,也就是相当于没执行,类似于Java中的事务,但是代码在程序里不是原子的。可能在执行过程中,被其他的线程切走了,从而违背了原子性,导致出现了逻辑错误。在java中,原生的保证了基本类型的读取是原子的。只能是简单的赋值,读取,而且还要是直接赋值,变量间的赋值也不是原子的。由于volatile无法保证原子性,所以Java引入了一些原子类,这里以AtomicInteger为例来分析。

        AtomicInteger可以理解为针对int的辅助类,对int的更改操作,像自增,自减,增加一个数或减少一个数提供了原子操作,也就是不可中断的操作,本来这些操作都是非原子的。AtomicInteger里面的函数大体可以分为两类,一类是返回修改之前的值,另一类是返回修改之后的值,主要包括:

         返回旧值:getAndSet(),getAndIncrement(),getAndDecrement()和getAndAdd()

         返回新值:incrementAndGet(),decrementAndGet()和addAndGet()

        这些函数的作用从名字上就能看出来,很好理解。通过这些函数就可以实现对int变量更改的原子操作,而它的原理则是根据for循环和CAS操作配合使用,这里以getAndSet()举例:

getAndSet()

CAS,也就是compare and set,通过UnSafe来实现:

compareAndSet(int expect, int update)

       这个函数的意思是针对某个int变量,如果它的值==expect,那么就把它设置为update,成功的话返回true,失败的话返回false,并且整个操作是原子的。而它内部使用的就是UnSafe,这个类从名字上看是不安全的,因为这是一个底层类,可以直接操作内存,所以效率高,由于可以直接操作内存,使用也要慎重。在前面学习FutureTask的时候,它内部也用到了这个类。

      在实际开发中,一般都是使用volatile修饰一个基本类型的变量,将这个变量用做状态标记,主要是为了线程的协同使用,比如一个线程的逻辑受到另一个线程逻辑的影响,类似于一个控制器或者开关的作用。变量被修饰了之后,就保证了可见性和有序性。但对这个变量的修改,不可以依赖变量当前的值,如果要依赖当前的值,那么就需要使用CAS操作。由于UNSAFE我们没权限使用,所以就使用对应的原子类就好。如果对变量的修改依赖变量当前的值,这背后往往代表着某种条件,只有条件和预期的一致,才可以继续进行,这样就可以避免其他的线程对变量的修改,当前线程没有看到。CAS是一个直接返回的函数,要么成功,要么失败。失败了往往条件已经被其他的线程改变了,我们可以直接放弃,也可以在外套层for循环不断的尝试,这也是乐观锁的概念,在循环的过程中,相当于锁的自旋。for循环适用于业务中必须要执行的操作,这种操作可以执行下去的时机往往是并行的其他线程已经都处理完了,或者在和其他线程争夺的过程中当前线程抢先了,然后我们才可以继续执行下去,并且在循环中,期望值会不断的切换为变量当前的值。就像AtomicInteger里面的getAndSet(),不断的把期望值设置为get()返回的值。所以针对像AtomicInteger这样的原子类,它里面的原子操作使用的不多,更多的是直接使用compareAndSet(),然后根据需要加不加循环。不加循环就相当于是一个开关或者触发条件,加了开关代表的是必须要执行的操作,往往执行的是资源释放,唤醒线程这样收尾的逻辑。FutureTask就是一个很好的例子,只不过它内部直接使用了UnSafe。其中的state,runner等变量都被volatile修饰,并且每次的修改都要依赖当前值,满足条件了,逻辑才可以继续,所以使用到了CAS。同时,finishCompletion()使用到了CAS外和循环,进行链表元素清除和唤醒等待线程的逻辑。

       现在我们明白了CAS,也明白了CAS加上for循环的原因,getAndSet()函数也就很好理解了。伴随着原子类的加入,线程安全的三个问题都被解决了,volatile解决了可见性和有序性,原子类解决了原子性,也就是volatile和原子类一起,就可以解决线程安全问题。

        这种方式主要就是用volatile来修饰一个变量,作为状态标记,然后控制线程的业务流程,类似于开关,其实也可以理解为锁。相比较锁有性能的优势,他们没有锁相关的性能开销,但是并不能完全代替锁的使用,在一些复杂的场景还是要使用锁来解决。

        除了状态标记,volatile还有一个场景就是单例模式。通过double check的方式实现单例的代码中,要用volatile来修饰。代码如下:

SingleTon

        上述代码里,instance之所以需要被volatile修饰,是因为instance=new SingleTon(),这行代码会发生指令重排序,这个代码会被拆分成3个指令,这里直接拷贝了:

指令重排序

        最终执行的时候,可能3会在2前面执行,那么出现的结果就是虽然instance!=null,但是初始化工作还没有完成,这个时候如果其他的线程访问,就会直接拿走instance,然后就报错了。刚开始学的时候,笔者犯了一个很想当然的错误,认为外面加锁了,即便发生指令重排序,由于其他的线程拿不到锁,所以应该没问题。但其实这个问题的关键是,如果发生了重排序,也就是instance!=null了,那么其他的线程就会直接跳过if里面的代码,直接return instance,也就是说,其他的线程根本就不需要锁,而不是锁失效了。

参考文章:https://github.com/LRH1993/android_interview/blob/master/java/concurrence.md

你可能感兴趣的:(Java多线程&并发知识细节(一))