Android开发-多线程之换个视角理解_乱序执行 android(1)

本文详细阐述了Java内存模型(JMM),介绍了主存和本地内存的概念,讨论了多核CPU下的同步问题,包括重排序和happens-before规则,以及synchronize、volatile和CAS的使用和原理。文章还涉及了AQS抽象队列同步器和ReentrantLock与synchronized的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

3 Java内存模型的本质(重点)

JMM java内存管理模型,提出了主存和线程本身的工作内存概念,如图:

说明: 主存即内存。本地内存即CPU缓存(包括三级缓存、寄存器、WCbuffer等)。

一个单核CPU在一个线程上执行指令,如果需要切换线程它会把当前线程的执行现场保存到内存中去,方便后续恢复,然后清空PC计数器,加载新线程的指令地址。因此,单核CPU不存在同步的问题。

当多核CPU分别在执行自己线程指令时,如果存在共享同一个变量,那么就有可能存在竞争关系,因为每个CPU的核都有自己的本地缓存,而二者通信是通过内存中共享变量的方式来实现的。这就存在这个变量同步不及时的情况,所以需要同步。

其实本质上本地内存是一个抽象的概念。实际上指的是CPU中的L1、L2和寄存器等相关的缓存。 现在的设备都是多个CPU多核同时工作。如:CPU执行PC(程序计数器)中的某条指令需要一个变量,那么CPU不是直接去内存中操作该变量。而是先把变量读取到L3->L2->L1(三级缓存),最后到寄存器缓存起来,然后CPU在去寄存器中读取该变量。当然CPU不会一次只读取一个变量,而是一次读取一个缓存行Cache Line,一个缓存行的大小是64个Byte。如果在需要下一个变量则会先从寄存器和缓存中查找,这样就比去操作内存块很多。最后,把计算结果刷新到寄存器,同步到主存(也就是内存)中。

4 同步相关问题(重点)

4.1 重排序和happens-before
4.1.1 重排序

不管什么用到语言,我们写的代码最终都会转成汇编指令,而汇编指令与机器指令(如:01010)是一一对应的。因此当CPU在执行当前指令的时候处于读等待,CPU不工作了?岂不是浪费?为了提高性能,它会尝试下一条指令能不能先执行了?,如果可以,那么CPU就不会闲下来了。

但有个前提,这个指令跟前一个指令没有依赖关系才会执行。 有了乱序执行这个机制,一连串的指令就看起来变得可以并行执行了(其实没有,只是利用了CPU处于读等待的空隙做事情)。

因此,为了提高执行效率,编译器和CPU都会进行指令的重排序。

4.1.2 happens-before

顾名思义,如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

不用太纠结这个概念。这只是一个规范而已,本质上就是说这个规范实现的代码肯定是加了内存屏障的。

如下这些代码实现方式,就是符合happens-before规则的:

  • 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作、
  • join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
4.2 synchronize

synchronize不管是修饰方法还是代码块,都需要用到锁对象。而锁对象的锁是有状态的,它会升级也会降级。锁的是对象,而不是把代码块锁住了!

锁的状态(jdk1.6后):

  • 无锁态
  • 偏向锁
  • 轻量级锁(也叫自旋锁)
  • 重量级锁 用户态向内核态申请锁,消耗锁资源。mutex
4.2.1 对象头

在jvm中,每个Object对象在内存中的布局有三部分组成:

  1. 对象头 : 包含markword(32位系统是4个字节,64位是8个字节)、class对象指针占4个字节
  2. 实例对象 :
  3. padding对齐: 为了让对象能够被8整除,需要补齐的字节数

举个例子: Object0=new Object(); o对象占多少内存?

假如是64位系统:

对象头+实例对象,也就是 8+4+0=12,不能被8整除,所以还需要加上padding 4. 因此,最终的o对象占用了16个字节。

请注意!!! markword就是用来存储锁信息的地方。 一共32/64位,多少位没有太大关系。我们只需要知道里面有什么即可。

4.2.2 锁的升级过程
  1. 一开始new 出锁对象时,还没有线程进入临界区,此时是无锁态。
  2. 有线程进入,则改成偏向锁,同时markword存入线程的id。
  3. 如果是同一线程则还是偏向锁。
  4. 当另外线程也进入临界区,请求锁对象,发现对象头已经有偏向锁了。产生了竞争!!那不好意思,先撤销偏向锁,然后两个线程通过CAS自旋的方式开始争抢锁对象,都往锁对象头里面写入自己线程栈的lock record。一旦有线程争抢成功,那么其他线程就会失败,此时锁对象变成了轻量级锁。
  5. 失败的线程会一直CAS循环下去,此间也可能还会有其他线程参与进来自旋。
  6. 当自旋次数超过一定值如10次,或者参与自旋的线程数太多。系统会进行干预。
  7. 这样干耗着会浪费CPU资源,所以干脆升级为重量级锁。其他线程全部进入mutex中的队列中去排队,线程进入wait或者block状态,不消耗CPU。
  8. 但synchronize修饰的是非公平的队列。

讲了这么多,synchronize的底层到底是怎么实现的?

其实还是 lock cmpxchg 指令。

4.3 volatile

volatile修饰变量后有两个作用: 1,内存可见性 线程间工作内存和主存实现了及时同步 2,防止指令重排 这对这个变量的操作被JMM加入内存屏障来保证指令不会乱序执行。

volatile到底是怎么解决指令重排的??

JVM层通过加入内存屏障,是一个逻辑实现,是jvm的要求规范而已,具体要看汇编语言。

  1. loadload 屏障 读
  2. storestore 屏障 写
  3. loadstore 屏障
  4. storeload 屏障

四个逻辑。 具体 就是在volatile读/写的前后加入内存屏障,保证顺序执行。内存屏障前后的指令不能重排序!

汇编层面: 最终就是调用了 lock: andl 指令。表示在寄存器中加0操作。

为什么这条指令能实现内存可见和禁止指令重排序??

内存可见性: 该指令能够将当前处理器对应缓存内容刷新到内存,并且是其他处理器的缓存失效。 重排序: 该指令本身就是内存屏障,它前面的指令和后面的指令都不能重排序。

4.4 CAS和原子操作
4.4.1 乐观锁和悲观锁
  • 悲观锁: 在访问共享资源的时候总是认为别人会来抢,所以只要访问临界区就直接上锁。通俗讲就是因为怕被抢,所以无脑上锁。比如用synchronize来对临界区上锁。
  • 乐观锁: 在访问共享资源时候,认为别的线程不会来抢资源。所以是“无锁”状态。但是可以通过CAS来保证数据的安全。ReentrantLock互斥锁(互斥别人,不互斥自己)
4.4.2 CAS

CAS(compare and swap): 比较并且交换。 目的: 在没有锁的状态下,可以保证多个线程对一个值的更新。

CAS实现思想:

  • E:拿到变量当前原始值(期望值)
  • V:计算的结果值
  • N:再次获取变量的值(当前值)

举个例子: 假设i=0,对i做++操作。 CAS的过程是这样的:

  1. 拿当i的前值 E=0;
  2. 计算结果值V=1;
  3. 再次拿i的当前,有可能如下情况: N=0;N=3(因为某个线程更改了)
  4. 如果E=N,表示没有被修改,我们可以更新,直接修改i=1。
  5. 如果E!=N,表示被修改过,我们这次修改就不能执行。然后我们在重头开始,再次比较,最终实现交换。

流程图:

- 实现的本质:

AtomicInteger内部就是通过CAS的方式来保证线程安全的。

内部会调用UnSafe类的方法。

private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();


/**
 * Atomically decrements by one the current value.
 *
 * @return the previous value
 */
public final int getAndDecrement() {
      //U表示 UnSafe类
    return U.getAndAddInt(this, VALUE, -1);
}



UnSafe类直接调用的是C++层的native方法compareAndSwapInt()

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
        // while中 native 方法
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

// native 方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);



通过源码发现compareAndSwapInt()最终会调用c++层的 Atomic::cmpxchg方法,有如下指令:

//_asm_  表示汇编指令
//LOCK_IF_MP: 如果是multi-processs 多处理器, 现在处理器都是多CPU的。
_asm_ volatile (LOCK_IF_MP(%4) "cmpxchg1 %1, (%3)")...//后面省略



如果是多个处理器则 进行lock。为什么? 多个CPU就会出现多线程同时执行,出现并发问题。

而CAS方法会直接通过汇编指令:lock cmpxchg 指令 来完成CAS真正的操作。 cmpxchg 这个指令也就体现了比较和交换的本质了。

所以, 现在的问题变成 lock cmpxhg 指令 是如何实现线程安全的? 写入的过程是没有原子性保证的。 由 lock 指令来保证原子性。 最终由硬件来支持,硬件怎么实现的啊? 好啦,到这里就可以啦。 硬件通过锁定北桥信号?(我也不清楚了啊)。

4.5 AQS(AbstractQueuedSynchronizer) 抽象队列同步器

高并发编程的核心: AQS。

里面通过维护一个volatile int state变量和一个存储线程的队列(双向链表)来实现同步的。 它本身是一个抽象的类,定义了同步模板方法。具体逻辑需要子类去继承实现。 可通过构造方法传入是否是公平锁。

线程通过CAS去获取state值,state初始值为0,那么拿到锁state=1。 后续如果再有线程进来,那么就封装成Node节点看,然后放到队列中去阻塞,知道之前的线程释放锁。 支持公平锁和非公平锁两种方式。

4.5.1 ReentrantLock和synchronize的区别?

synchronize: 最终要通过用户态到内核态的切换,但是有锁的升级优化。悲观锁

ReentrantLock(jdk1.5后新增的锁): 基于AQS同步机制,其实内部还是通过CAS来获取锁,不用到内核态,轻量级。更加灵活。属于 乐观锁。 需要自己手动try catch,在finally中释放锁。

文末福利

这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析

架构师筑基包括哪些内容

我花了将近半个月时间将:深入 Java 泛型.、注解深入浅出、并发编程.、数据传输与序列化、Java 虚拟机原理、反射与类加载、高效 IO、Kotlin项目实战等等Android架构师筑基必备技能整合成了一套系统知识笔记PDF,相信看完这份文档,你将会对这些Android架构师筑基必备技能有着更深入、更系统的理解。

由于文档内容过多,为了避免影响到大家的阅读体验,在此只以截图展示部分内容

注:资料与上面思维导图一起看会更容易学习哦!每个点每个细节分支,都有对应的目录内容与知识点!



这份资料就包含了所有Android初级架构师所需的所有知识!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

图片转存中…(img-GLujTblD-1714409265848)]
[外链图片转存中…(img-40OtVVuA-1714409265848)]
这份资料就包含了所有Android初级架构师所需的所有知识!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值