并发编程 · 基础篇(上) · android线程那些事(2)

简介: 并发编程 · 基础篇(上) · android线程那些事

四、线程安全

说完线程基础,我们聊一聊线程安全,线程安全首先有六个问题需要大家一起思考

image.png

4.1 带着问题出发

4.1.1 什么是线程安全?

第一个问题是什么是线程安全?

《Java Concurrency In Practice》的作者Brian Goetz说过,当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的

通俗一点来说: 线程安全是指多线程访问同一个资源时,不会因为线程交叉执行而导致资源混乱,从而保证程序的正确性。

4.1.2 你知道有哪些线程不安全的情况?

第二个问题是你知道有哪些线程不安全的情况?

小木箱从android、容器和线程池三个方面举例说明线程不安全的情况。

首先,在android多线程编程中,如果多个线程同时访问同一个Activity里的资源,也可能导致线程不安全的情况,因为Activity是一个单例,它的资源可能被多个线程同时访问。

image.png

上面的代码中,MyActivity类中定义了一个counter变量,并且在onCreate方法中启动了两个线程,两个线程都会更新counter变量,但是由于没有进行同步操作,这两个线程可能会同时访问counter变量,从而导致线程不安全的问题,从而可能引发Java异常。

为了解决这个问题,可以在更新counter变量时使用同步操作,例如使用synchronized关键字:

image.png

然后,在android UI操作并不是线程安全的,并且这些操作必须在UI线程执行,子线程是无法更新UI的,具体实现思路如下:

image.png

但这并非绝对的,子线程其实也是可以更新UI的

Toast本质是通过window显示和绘制的,而子线程不能更新UI在于ViewRootImpl的checkThread方法无法在Activity维护View树的行为。

然后,HashMap的putVal方法添加元素不是线程安全的,因此可通过Collections类的静态方法synchronizedMap获得线程安全的HashMap

image.png

image.png

image.png

最后,在ThreadPoolExecutor中,addWorker为什么需要持有mainLock,本质原因是workers是HashSet类型的,不能保证线程安全。

image.png

4.1.3 怎么避免线程安全问题?

第三个问题怎么避免线程安全问题?当存在多个线程协作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,保证线程安全的方式有三种。

第一种是使用线程安全的类,如AtomicInteger类;

第二种是加锁排队执行,如synchronized和ReentrantLock等使用;

第三种是使用线程本地变量,如ThreadLocal来处理。

后文会详细讲解实现过程和原理。

4.1.4 多线程会带来哪些线程安全问题?

多线程会带来哪些线程安全问题?多线程会带来四个线程安全问题。第一个问题是原子性问题,第二个问题是竞争条件,第三个问题是死锁,第四个问题是丢失更新。

首先我们来说说第一个问题原子性问题,某些操作不能被中断,比如赋值操作,如果多线程同时对同一变量进行赋值操作,会导致变量的值不正确。比如多线程同时访问 i++ 的场景:

image.png

如下图异常代码所示,输出结果可能不是按顺序输出打印的,因为多线程同时对count变量进行赋值操作,可能会出现线程安全问题。为了解决这个问题,可以使用同步代码块,保证变量的操作是原子性的

image.png

输出结果:

小木箱:1

小木箱:3

CrazyCodingBoy:2

小木箱:4

CrazyCodingBoy:5

小木箱:6

CrazyCodingBoy:7

CrazyCodingBoy:8

那么正确的编码是怎样的呢,我们应该使用Atomic原子类,可以保证count变量的操作是原子性的

import java.util.concurrent.atomic.AtomicInteger;
//正常代码
public class AtomicDataThread implements Runnable{
    private static AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run(){
        for(int i=0; i<4; i++){
            count.incrementAndGet();
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }
    public static void main(String[] args){
        AtomicDataThread atomicDataThread = new AtomicDataThread();
        Thread thread1 =  new Thread(atomicDataThread,"小木箱");
        Thread thread2 =  new Thread(atomicDataThread,"CrazyCodingBoy");
        thread1.start();
        thread2.start();
    }
}

输出结果:

20

第二个问题是竞争条件,当多个线程同时访问某个变量时,某个线程修改了变量的值,但是其他线程没有读取到修改后的值,导致程序出现错误,如下面的代码所示:

image.png

输出结果:

Thread-1 : 2

Thread-3 : 4

Thread-0 : 1

Thread-4 : 5

Thread-2 : 3

上述代码中,5个线程同时访问count变量,并且在run方法中将count变量加1,但是由于多线程的存在,有可能某个线程修改了count变量的值,而其他线程还没有读取到修改后的值,这就可能导致程序出现错误。

为了解决这个问题,可以使用synchronized关键字来对count变量进行同步操作:

image.png

输出结果:

Thread-0:1

Thread-4:2

Thread-3:3

Thread-2:4

Thread-1:5

第三个问题是死锁,多个线程互相等待对方释放某个资源,导致程序无法继续执行。

假设有两个小木箱线程P1和小木箱线程P2,它们分别需要资源A和资源B。

但是它们同时只能获得一个资源,当小木箱线程P1获得资源A时,P2获得资源B,然后小木箱线程P1等待资源B,小木箱P2等待资源A。

但是由于资源A和资源B只有一个,所以小木箱线程P1和小木箱线程P2都无法获得自己想要的资源,这就是死锁。

死锁的原因是由于小木箱线程P1和小木箱线程P2同时请求资源A和资源B,而资源A和资源B只有一个。

所以小木箱线程P1和小木箱线程P2都无法获得自己想要的资源,从而导致死锁的发生。

image.png

输出结果:

Thread[#23,小木箱线程P2,5,main]get ResB

Thread[#22,小木箱线程P1,5,main]get ResA

Thread[#23,小木箱线程P2,5,main]waiting get ResA

Thread[#22,小木箱线程P1,5,main]waiting get ResB

最后一个问题是丢失更新。

丢失更新是指当多个线程同时访问某个变量时,某个线程修改了变量的值。

但是其他线程没有读取到修改后的值,导致程序出现错误。

主要原因是多个线程同时访问某个变量,而该变量没有被同步,导致其他线程读取到的值不是最新的值。

image.png

输出结果:

count=10000

解决方案有三种,第一种是使用synchronized关键字,使用synchronized关键字可以解决多线程访问变量时出现的线程安全问题。第二种是使用Atomic类,Atomic类提供了一种原子操作,可以解决多线程访问变量时出现的线程安全问题。第三种是使用volatile关键字,volatile关键字可以保证多线程访问变量时,可以读取到最新的值。

4.1.5 线程安全问题体现是怎样的?

线程编程会带来性能问题呢?主要有两个方面,第一个方面是线程调度,第二个方面是线程协作。

首先,我们说说第一个方面,线程调度具体体现是缓存失效带来性能问题。下面代码存在线程不安全的问题,因为多个线程同时对SUM变量进行操作,可能会出现脏读、脏写等问题。

image.png

输出结果:

199269

可以使用synchronized关键字加锁来解决。

image.png

输出结果:

200000

由于程序有很大概率会再次访问刚才访问过的数据,所以为了加速整个程序的运行,会使用缓存,这样我们在使用相同数据时就可以很快地获取数据。

可一旦进行了线程调度,切换到其他线程,CPU就会去执行不同的代码,因为CPU读写缓存速度低于内存读写缓存速度,原有缓存很可能失效,需要重新读写缓存新数据,造成一定的开销。

因此,线程调度器为了避免频繁地发生上下文切换,有四种解决方式,分别是减少线程的数量、 调整线程优先级、 整线程优先级、调整时间片大小和使用预取策略。

减少线程的数量方面,我们可以减少线程的数量,可以减少上下文切换的发生次数,这样可以提高效率。

调整线程优先级方面,我们在线程调度器中,可以通过调整线程的优先级来控制上下文切换次数,使计算机系统能够更高效地运行。

调整时间片大小方面,我们在线程调度器可以通过调整时间片大小来控制上下文切换的发生次数,以提高系统效率。

使用预取策略方面,我们在线程调度器可以使用预取策略来减少上下文切换次数,以提高系统效率。

首先,我们说说第二个方面线程协作导致线程不安全的情况。

因为主线程在设置running变量为false之前,另一个线程可能已经读取了running变量的值并将它设置为true。这样,循环就会一直运行,导致线程不安全的情况。

image.png

为了解决这个问题,可以使用synchronized关键字来确保变量running的原子操作:

image.png

因为线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中。

这些问题在单线程中并不存在,但在多线程中为了确保数据的正确性,就不得不采取上述方法,因为线程安全的优先级要比性能优先级更高,间接降低了性能。

4.1.6 怎样获取子线程的结果?

第五个问题为怎样获取子线程的结果?获取子线程的结果有两种方式,第一种方式是FutureCallable,第二种方式是Callable接口。

因为Runnable没有具体返回值,也不能抛出checked Exception。

image.png

如果我们想要拿到线程执行结果,那么建议使用Future和Callable方式。

Future是一个存储器,Future存储了call0这个任务的结果,而这个任务的执行时间是无法提前确定的。

因为这完全取决于call方法执行的情况,通过Future.get来获Callable取接口返回的执行结果。

image.png

输出结果:

image.png

如果我们想取消任务的执行,我们可以调用cancel方法。

  1. 如果这个任务还没有开始执行,那么这种情况最简单,任务会被正常的取消,未来也不会被执行,方法返回true。
  2. 如果任务已完成,或者已取消,那么cancel方法会执行失败,方法返回false。
  3. 如果这个任务已经开始执行了,那么这个取消方法将不会直接取消该任务,而是会根据我们填的参数。 mayInterruptIfRunning。

使用Future的注意点有两个。

第一个是当for循环批future量获取的结果时,容易发生一部分线程很慢的情况,get方法调用timeout时应使用限制。

第二个是生命周期只能前进,不能后退。就和线程池的生命周期一样,一旦完全完成了任务,Future就永久停在了“已完成”的 状态,不能重头再来。


Callable比较简单了,类似于Runnable,被其它线程执行的任务,实现call方法,有返回值

image.png

输出结果:

小木箱说: 我是call的返回值

4.1.7 什么是多线程的上下文切换?

第六个问题什么是多线程的上下文切换?在实际开发中,线程数往往是大于 CPU 核心数的,比如 CPU 核心数可能是 8 核、16 核等等,但线程数可能达到成百上千个。

这种情况下,操作系统就会按照一定的调度算法,给每个线程分配时间片,让每个线程都有机会得到运行。

而在进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。

但上下文切换带来的开销是比较大的,假设我们的任务内容非常短,比如只进行简单的计算,那么就有可能发生我们上下文切换带来的性能开销比执行线程本身内容带来的开销还要大的情况。

那么什么情况会导致密集的上下文切换呢?如果程序频繁地竞争锁,或者由于 IO 读写等原因导致频繁阻塞,那么程序就可能需要更多的上下文切换,上下文切换导致了更大的开销,我们应该尽量避免这种情况的发生。

4.2 线程安全特性

带着问题出发小木箱说完了,接下来我们聊一下线程安全特性,无论是缓存失效、还是上下文切换带来的时序性问题和线程调度引发的数据准确性问题。

在深入理解Java虚拟机那本书统一归纳总结为线程安全的三大特性: 原子性、有序性和可见性。

image.png

4.2.1 可见性

当线程 A在CPU1上执行,线程 B在CPU2上执行,共享变量param=0,线程 A给共享变量param赋值时,会把param的初始值加载到CPU1的高速cache中,然后赋值2,线程 B给共享变量param赋值时,会把param的初始值加载到CPU2的高速cache中。此时param的值在CPU2中是0而不是2。线程 A在CPU1中修改了param但是CPU2中的线程 B却没有拿到。

保证线程可见性的方式有三种,第一种是提供volatile关键字保证可见性。

当一个变量被volatile修饰时,保证修改的值会被立刻更新到主内存中,当其他线程读取时,会去主内存中读取新值。

第二种是synchronized。第三种是Lock。

lock和synchronized因为同一时间只有一个线程执行,锁释放之前会把变量的修改刷新到主内存中.

image.png

使用volatile关键字可以确保变量可见性。当一个线程对volatile变量进行写操作时,会导致其他线程对该变量的读操作立即可见

image.png

使用synchronized关键字可以确保变量可见性。当一个线程访问一个对象的synchronized方法或者synchronized块时,其他线程对该对象的其他synchronized方法或者synchronized块的访问将被阻塞,直到访问线程离开synchronized方法或者synchronized块时,其他线程才可以访问该对象的其他synchronized方法或者synchronized块。

image.png

这样一来,synchronized关键字可以保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而也就保证了操作的可见性,即一个线程修改了某个共享变量的值,这新值对其他线程来说是可见的。

ReentrantLock通过使用内部的可重入锁来保证可见性。当线程获得锁时,它会同步内存,确保所有线程都能看到最新的状态。当线程释放锁时,ReentrantLock会将最新的状态写回主内存,确保其他线程能够看到最新的状态。

image.png

那么保证线程可见性的本质是什么呢? Happens-Before原则 ,什么是Happens-Before原则呢? JMM向程序员提供了足够强内存可见性保证,不影响程序执行结果情况,可见性保证并一定存在,比如下面的程序,A Happens-Before B 并不保证,因为其不影响程序执行结果;

image.png

JMM为了满足编译器和处理器的约束尽可能少

image.png


Happens-Before遵循的规则是:只要不改变程序的执行结果,编译器和处理器想怎么优化就怎么优化。

Happens-Before核心思想是: 前一个操作的结果对后续操作时可见的

Happens-Before目的是: 为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

4.2.2 有序性

JMM (Java Memory Model) 有序性问题是指在多线程编程中,由于编译器和处理器优化,导致程序执行顺序与代码顺序不一致的问题。JMM 为程序员提供了一种可靠的机制来确保程序的正确性,从而避免出现不可预料的结果。

image.png

因为JMM定义了一种内存模型,该模型定义了线程之间的内存访问顺序,但是由于处理器的指令重排序,导致线程之间的内存访问顺序可能会发生改变。

image.png

这就导致了线程之间存在有序性问题,从而出现了程序的不一致性。

image.png

JMM有序性问题可以通过使用使用synchronized关键字、使用Lock锁和使用Atomic类来解决: 使用synchronized关键字可以确保每次对变量的读写操作都是从主内存中读取最新的值,从而保证线程间变量的一致性。

image.png

当一个线程访问一个对象的synchronized代码块时,其他线程便不能访问该对象的其他synchronized代码块。

Lock锁可以确保每次对变量的读写操作都是从主内存中读取最新的值,从而保证线程间变量的一致性。当多个线程访问同一个ReentrantLock对象时,多个线程会按照获取锁的顺序依次获取锁,而不会发生竞争,从而保证线程安全。

image.png

ReentrantLock使用一个可重入的锁来确保线程按照顺序访问共享资源。ReentrantLock使用FIFO(先进先出)的锁队列来管理等待的线程,从而保证线程按照它们发出请求的顺序来访问共享资源。

Atomic类提供了一种可以实现原子操作的方法,从而保证线程间变量的一致性。Atomic还可以通过使用内存屏障来保证操作的有序性,内存屏障可以确保操作在特定时间点完成,从而保证操作的有序性。

image.png

在实际开发过程中,Android中存在多个进程,多个进程间通过IPC进行数据交互,如果没有同步机制,会出现JMM有序性问题。

4.2.3 原子性

JVM原子性是指Java虚拟机中的指令是原子的,也就是说,它们不能被其他线程中断或改变。这意味着,当一个线程正在执行某个指令时,其他线程就不能改变它。这确保了程序的正确性,避免了线程之间的竞争条件。

image.png

JVM通过使用synchronized关键字、使用Atomic类、使用Lock锁和使用CAS算法来确保原子性。

使用Synchronized关键字可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块,Synchronized可以保证操作的原子性,即操作过程不可被中断,Synchronized使用的是互斥锁机制,能够保证同一时刻只有一个线程可以访问某个资源,从而保证了操作的原子性。

image.png

使用Atomic类的方法可以保证操作的原子性,即每次操作都是不可中断的,也就是说在一个线程进行操作的过程中,其他线程不能中断或插入,只有当前线程完成操作后,其他线程才能进行操作。

image.png

使用ReentrantLock可以保证原子性,因为ReentrantLock使用了一个可重入的锁,ReentrantLock可以保证操作的原子性,即一个操作必须在另一个操作完成之前完成。ReentrantLock还可以通过使用比较和交换(CAS)技术来确保原子性,从而保证操作的一致性和正确性。

image.png

使用CAS算法可以通过比较并交换操作来确保原子性。当多个线程同时尝试修改某个变量时,CAS算法会检查变量当前值是否与预期值相等,如果相等,就修改变量的值,否则就不做任何修改。这种方式可以确保多个线程同时修改变量时,只有一个线程的修改能够成功,从而保证了原子性。

image.png

简单总结一下:

如果多线程访问同一块资源时候,你想要保证资源的可见性,那么小木箱建议你使用volatile、synchronized、ReentrantLock和Atomic

如果多线程访问同一块资源时候,你想要保证资源的有序性,那么小木箱建议你使用synchronized和ReentrantLock

如果多线程访问同一块资源时候,你想要保证资源的原子性,那么小木箱建议你使用synchronized、Atomic、ReentrantLock和CAS算法

4.3 线程安全强度

线程安全特性小木箱说完了,接下来我们聊一下线程安全强度,线程安全强度有三大特征: 第一,线程不可变。第二,绝对线程安全。第三,相对线程安全。第四,线程隔离。

线程不可变

线程不可变是指一个线程实例的状态在它被创建之后不能被改变的概念。也就是说,一旦一个线程被创建,它的属性例如它的优先级,名字等都不能被改变。

不可变的object一定是线程安全的,因为线程的状态是由操作系统内核控制的,操作系统内核不允许线程的状态发生变化。

使用final基础数据类型、string、枚举类型、Number部分子类和集合类型Collections.unmodifiableXXX()获取不可变集合都可以保证线程不可变

绝对线程安全

绝对线程安全是指在多线程环境下,任何时刻都不会出现数据不一致的情况,也就是说不管多少个线程同时对同一个数据进行操作,最终结果都是一致的,一个线程对数据的改变,其他线程都能看到,也就是说绝对线程安全是指多个线程同时对数据进行操作时,数据的一致性是绝对保证的。

绝对线程安全的实现,可以降低系统出现线程安全问题的可能性,提高系统的稳定性和可靠性。

不管运行环境如何,调用者都不需要任何额外的sync操作,比如:ATM取钱,怎么去的和取完之后怎么拿走不影响取钱这个业务安全

相对线程安全

相对线程安全是指在多线程环境下,有一定的约束条件下,不会出现数据不一致的情况,也就是说在满足一定的条件下,多个线程同时对数据进行操作时,数据的一致性是相对保证的。

保证对这个object单独的操作是thread安全的;但是对一些特定顺序的连续调用,需要额外的手段来保证,java中的大部分thread安全类都属于这种类型,比如:Vector、HashTable、Collections、synchronizedCollection()方法包装的集合

线程隔离

线程隔离指对象本身不是thread安全的,但是可以在调用端采用正确的同步手段来保证对象在并发环境中安全的使用。

为了提高系统的并发性能,减少线程之间的竞争。 为了提高系统的安全性,防止一个线程对其他线程造成损害。为了提高系统的稳定性,防止一个线程中断其他线程的执行。为了提高系统的可维护性,防止一个线程影响其他线程的运行。JVM对各个线程进行独立隔离

线程对立

无法在多线程环境中并发使用的代码,java中几乎没有,因为会导致一种不可控制的状态,从而使系统处于不稳定的状态。

线程对立的目的是为了保护多个线程之间的共享资源,避免不同线程之间的数据竞争,从而防止程序出现数据不一致的问题。

4.4 线程安全方案

线程安全强度小木箱说完了,接下来我们聊一下线程安全方案,小木箱将上述的安全策略归纳总结为四大类,互斥阻塞同步、非阻塞同步、无同步方案和控制并发流程

4.4.1 互斥阻塞同步

阻塞线程执行达到同步的目的

 synchronized

按照锁的使用位置,synchronized锁的类型有四种: 第一种是对象锁,第二种是方法锁,第三种是类锁,第四种是静态方法锁

第一种是对象锁,synchronized(this),同步一个代码块,只作用于一个对象,如果调用两个对象的同步代码块方法,不会同步,可以用来保护一个对象的实例方法,使得多个线程可以同时访问,但是同一时刻只能有一个线程可以执行该方法,以避免多线程环境中数据出现错误。

第二种是类锁,synchronized(Student.class),同步一个类,对这个类的所有对象同步,可以用来保护一个类的静态方法,使得多个线程可以同时访问,但是同一时刻只能有一个线程可以执行该方法,以避免多线程环境中数据出现错误。

第三种是方法锁,synchronized method,同步一个方法,作用同上,只作用于一个对象,当多个线程访问同一个对象的实例方法时,它们会被同步,以保证线程安全。

第四种是静态方法锁,synchronized static method,同步静态方法,作用同步一个类,当多个线程访问同一个类的静态方法时,它们会被同步,以保证线程安全。

image.png

 新版本jvm对synchronized进行了很多优化,例如自旋锁等,synchronized的性能略微比ReentrantLock差一点

 独占锁Reentrantlock、共享锁CountDownLatch、CyclicBarrier、Phaser、Semaphore等
4.4.2 非阻塞同步

非阻塞同步主要解决线程等待、切换带来的性能问题。基于冲突检测的乐观并发策略:先操作,如果没有其他线程竞争就直接成功;否则采取补偿措施不断重试,直到成功;底层需要硬件指令集支持。

CAS(compare and swap) 和原子类AtomicInteger的方法compareAndsSet()、getAndIncrement()都是非阻塞同步表现形式

4.4.3 无同步方案

所谓的无同步方案就是控制并发流程

 控制并发流程定义

控制并发流程是指保证线程安全,不一定要同步,如果一个method不涉及共享数据,就无须同步。

控制并发流程特征
  • 特征一: 栈封闭

多个thread访问同一个method的局部变量时,不会出现thread安全问题,因为局部变量存储在虚拟机栈中,属于thread私有的

  • 特征二: 线程本地存储(Thread Local Storage)

如果一段代码中的数据必须与其他线程共享,那就保证共享数据代码在同一个thread里面执行,无须同步也能保证thread不会出现数据争用的问题

可以使用ThreadLocal类实现thread本地存储功能

  • 特征三: 可重入代码

可以在代码执行的任何时刻中断,转而执行另外一段代码,原来的程序不会出现任何错误

怎样控制并发流程

Java控制并发可以使用Cyclicbarrier和RxJava来控制并发流程。下面着重来讲解Cyclicbarrier,Cyclicbarrier是一种同步工具类,Cyclicbarrier允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。

image.png

当所有线程都到达屏障点时,屏障点才会打开,所有线程才能继续执行。 Cyclicbarrier可以用来实现多线程之间的协作,比如说,玩家在游戏中到达某个关卡时,所有玩家都要到达某个位置,然后才能继续游戏,这时候就可以使用Cyclicbarrier了。 Cyclicbarrier底层使用AQS(AbstractQueuedSynchronizer)实现,AQS是一种基于FIFO队列实现的锁机制,AQS通过一个int变量来表示同步状态,当状态为0时,表示无锁状态,当状态大于0时,表示有锁状态。

Cyclicbarrier在内部维护了一个计数器,每当一个线程到达屏障点时,计数器的值就会减1,当计数器的值减为0时,表示所有线程都已经到达屏障点,此时就会打开屏障,所有线程继续执行。

image.png

Cyclicbarrier优点有两个,第一个是Cyclicbarrier可以实现让一组线程等待至某个状态之后再全部同时执行。 第二个是Cyclicbarrier可以复用,当线程到达屏障点后,计数器会重置为初始值,这样就可以多次使用Cyclicbarrier了。

Cyclicbarrier缺点有两个,第一个是当某个线程超时或者被中断时,整个系统都会受到影响,因为其他线程都会被阻塞。第二个是如果线程太多,可能会导致计数器溢出。

简单总结一下就是:

如果你想保证线程不可变,那么小木箱建议你使用String、Integer 、volatile和 ConcurrentHashMap

如果你想保证线程相对安全,那么小木箱建议你使用mutex、semaphore、lock、局部变量、可重入函数、非阻塞算法和Vector等

如果你想保证线程处于隔离状态,那么小木箱建议你在Linux使用Namespace机制、使用锁和ThreadLocal

如果你想确保绝对的线程对立,那么小木箱建议你使用原子操作、线程池访问权限管控、同步锁、volatile关键字和信号量

如果你想确保绝对的线程安全,那么可以使用原子操作、同步锁、volatile关键字和ConcurrentHashMap

4.5 UncaughtException兜底

最后,我们尝试回答一个问题: 线程的未捕获异常UncaughtException应该如何处理?

image.png

当线程抛UncaughtException,我们可以利用UncaughtExceptionHandler处理,因为主线程可以轻松发现异常,子线程却不行。

在子线程抛出了异常会被主线程覆盖,子线程异常无法用传统方法捕获子线程抛出异常。

主线程try catch没用,只能捕获当前线程的异常,不能直接捕获所有异常,因此UncaughtExceptionHandler提高了代码健壮性。

image.png

输出结果: 小木箱成长营捕获了Thread-0线程的异常

这样,我们可以全局为不健康的线程进行兜底管控。

五、结语

并发编程 · 基础篇(上) · android线程那些事课程就此结束了,对于每一个Android开发者来说,线程知识重要性不言而喻,国内为什么老八股喜欢考线程知识,因为如果你不具备这方面扎实的线程安全和线程基础知识,那么应对高性能下载组件实现还是处理启动和卡顿优化等工作都非常棘手。

下一节,小木箱将带大家学习并发编程 · 基础篇(中) · 三大分析法分析Handler。

我是小木箱,如果大家对我的文章感兴趣,那么欢迎关注小木箱的公众号小木箱成长营。小木箱成长营,一个专注移动端分享的互联网成长社区。

参考资料

相关文章
|
23天前
|
Java Android开发 UED
🧠Android多线程与异步编程实战!告别卡顿,让应用响应如丝般顺滑!🧵
【7月更文挑战第28天】在Android开发中,确保UI流畅性至关重要。多线程与异步编程技术可将耗时操作移至后台,避免阻塞主线程。我们通常采用`Thread`类、`Handler`与`Looper`、`AsyncTask`及`ExecutorService`等进行多线程编程。
35 2
|
16天前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
36 0
|
8天前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
28 4
|
8天前
|
缓存 Java 数据处理
Java中的并发编程:解锁多线程的力量
在Java的世界里,并发编程是提升应用性能和响应能力的关键。本文将深入探讨Java的多线程机制,从基础概念到高级特性,逐步揭示如何有效利用并发来处理复杂任务。我们将一起探索线程的创建、同步、通信以及Java并发库中的工具类,带你领略并发编程的魅力。
|
16天前
|
Java API 开发者
Java中的并发编程:解锁多线程的潜力
在数字化时代的浪潮中,并发编程已成为软件开发的核心技能之一。本文将深入探讨Java中的并发编程概念,通过实例分析与原理解释,揭示如何利用多线程提升应用性能和响应性。我们将从基础的线程创建开始,逐步过渡到高级的同步机制,并探讨如何避免常见的并发陷阱。读者将获得构建高效、稳定并发应用所需的知识,同时激发对Java并发更深层次探索的兴趣。
27 2
|
21天前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
12天前
|
Java 数据库
Java中的并发编程:深入理解线程池
在Java的并发编程领域,线程池是提升性能和资源管理的关键工具。本文将通过具体实例和数据,探讨线程池的内部机制、优势以及如何在实际应用中有效利用线程池,同时提出一个开放性问题,引发读者对于未来线程池优化方向的思考。
30 0
|
16天前
|
数据采集 并行计算 程序员
Python中的并发编程:理解多线程与多进程
在Python编程中,理解并发编程是提升程序性能和效率的关键。本文将深入探讨Python中的多线程和多进程编程模型,比较它们的优劣势,并提供实际应用中的最佳实践与案例分析。
|
19天前
|
安全 Java 程序员
大家都说Java有三种创建线程的方式!并发编程中的惊天骗局!
今天来聊一个比较有意思的话题,这是一道Java八股文中的八股文,简称八股文Plus!
|
19天前
|
安全 Java API
Java中的并发编程:深入理解线程同步与协作机制
在Java的并发编程领域中,线程间的同步与协作是实现高效、稳定多线程应用的关键。本文将深入探讨Java中用于线程同步的各种锁机制,包括内置锁和显式锁,以及线程间协作的等待/通知机制。同时,我们将通过实例分析这些机制的应用,并指出常见的并发问题及解决方案,旨在为读者提供一套完整的Java并发编程指南。