2024年安卓最全Android开发-多线程之换个视角理解_乱序执行 android(1),2024年最新2024Android高级面试题及答案

最后

我的面试经验分享可能不会去罗列太多的具体题目,因为我依然认为面试经验中最宝贵的不是那一个个具体的题目或者具体的答案,而是结束面试时,那一刻你的感受以及多天之后你的回味~

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我整理了一些资料,需要的可以免费分享给大家

在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

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

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

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

理解多线程并发和锁的关键在于正确的理清当前代码正处在哪个线程的执行环境下。说白了,这个同步代码/同步方法谁都可以来执行的,关键是有没有其他人在用这个锁。

1 Thread如何理解

1.1 Thread类与普通类的区别?

线程与线程类是不同的概念。 线程是系统CPU资源调度的基本单元,它是一个抽象的概念。Thread类和其他别的类没有什么区别。它只是对线程有着“管理”作用。而真正执行的代码都是在run()方法中,或者外部传入的runnable中。

1.2 中介作用

在线程的start方法之前,所有代码都在老线程上运行,调用start方法之后,内部会调用VMTthread.create,实际上真正在新线程上运行的只有run方法。这个角度理解,thread类只是一个中介,任务就是启动一个新线程来运行用户指定的runnable,而不关心是内部的还是外部传入的。

2 线程的状态

  1. New 线程创建
  2. Runnable 可执行状态
  3. Running 执行状态
  4. wait:当前线程调用某个对象object的wait方法,只有当子线程也同样调用该对象object的 notify/notifyAll方法才会唤醒当前线程。系统可能会有多个线程在wait,所以有notifyAll方法。
  5. block:遇到同步时候(同步方法、同步代码块、class类对象作为锁时,类中的静态方法),如果锁已经被其他的线程占用,那么就会进入阻塞状态,直到获取到锁。
  6. timed_wait: sleep/join, 等的一定时间才执行。
  7. terminated:线程运行完毕

join()用来保证两个线程的顺序执行:

Thread t1=new Thread(xx);
Thread t2=new Thread(xx);
t1.start();
t1.join();
t2.start();



上面代码表示,只有当t1执行完毕,t2才会执行。

2.1 wait和notify是如何绑定的?

通过同一个object对象。当一个线程调用某个object对象的wait方法时候,系统会在object中记录该请求,如果是多个线程调用则会有waitinglist,而当另外一个线程调用object的notify/notifyAll来唤醒一个/多个waitinglist中的线程。

2.2 线程调用wait()方法的条件?
  • 执行这个object的synchronized方法
  • 执行一段synchronized代码,且是基于这个object做的同步
  • 如果object是一个class类,可以实行synchronized static 方法

也就说: 一个线程获得了对象object锁lock,它才可以调用wait方法,而调用wait方法后该线程会释放锁,从而可以让别的线程来获取。

2.4 线程什么时候会释放锁?
  1. 当线程执行完毕
  2. 线程调用wait()方法
2.5 wait方法和sleep方法区别
  • wait方法必须要在同步代码中调用,sleep没有限制
  • wait方法会释放CPU、释放锁,sleep释放CPU,不释放锁(容易引起死锁)

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);



最后

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

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

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

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

eactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。**

[外链图片转存中…(img-prFDm0d2-1715739705060)]

[外链图片转存中…(img-EulOTrMi-1715739705060)]

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

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

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

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值