Android阿里面试:炒鸡硬核的高频Java线程面试题,敢来挑战一下吗?

本文大纲

本文作者

Jsonchao

温馨提示:加⭐️的为必会题哦~

1、线程安全问题的本质是什么?

在多个线程访问共同的资源时,在某⼀个线程对资源进⾏写操作的中途,其他线程对这个写了⼀半的资源进⾏了读操作/写操作,从而导致出现数据错误。

2、锁机制的本质是什么?

通过对共享资源进⾏访问限制,让同⼀时间只有⼀个线程可以访问资源,保证了数据的准确性。

不论是线程安全问题,还是针对线程安全问题所衍⽣出的锁机制,它们的核⼼都在于共享的资源,⽽不是某个⽅法或者某⼏⾏代码。

3、如何实现线程安全?

1、不共享资源

使用 ThreadLocal 实现数据的存取,ThreadLocal 的 set 方法中是将 vaule 存储在 Thread.threadLocals(ThreadLocalMap) 中。

ThreadLocal 的使用建议:

  • 1)、声明为全局静态 final 成员。
  • 2)、避免存储大量对象。
  • 3)、用完后及时移除对象。

2、共享不变资源

一个类中有一个 final 的 x 和 普通变量 y,它们都在构造函数中被赋值。另一个线程去读取它们时,由于 final 具有禁止重排序的功能,所以可以获取到正确的值,但是普通变量可能会重排序到构造函数之外被赋值。

3、共享可变资源

1)、可见性

  • 1、使用 final 关键字。
  • 2、使用 volatile 关键字。
  • 3、加锁,锁释放时会强制将缓存刷新到主内存。

2)、操作原子性

  • 1、加锁,保证操作的互斥性。
  • 2、使用 CAS 指令(如 Unsafe compareAndSwapInt)。
  • 3、使用原子数值类型(如 AtomicInteger)。
  • 4、使用原子属性更新器(AtomicReferenceFieldUpdater) 。

3)、禁止重排序

volatile 禁止重排序解决了 1.5 之前 DCL 写法因重排序导致的实例没有完全初始化就被其它线程引用的情况。

4、为什么多线程同时访问(读写)同个变量,会有并发问题?

  • 1、Java 内存模型规定了所有的变量都存储在主内存中,而每个线程都有自己的工作内存,线程的工作内存中保存了该线程中使用变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
  • 2、当线程访问一个变量时,会将变量从主内存拷贝到工作内存,而对变量的写操作,不会马上同步到主内存。
  • 3、不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要在自己的工作内存和主存之间进行数据同步。

5、说说原子性,可见性,有序性分别是什么意思?

  • 原子性:在一个操作中,CPU 不可以在中途暂停然后再调度,即不被中断操作,要么执行完成,要么就不执行。
  • 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

6、实际项目过程中,有遇到过多线程并发问题的例子吗?

有,比如单例模式。由于单例模式的特殊性,可能被程序中不同地方的多个线程同时调用,所以为了避免多线程并发问题,一般要采用volatile+Synchronized的方式进行实例,方法的保护。

private volatile static Singleton singleton;

public static Singleton getSingleton() {    
      if (singleton == null) {        
          synchronized (Singleton.class) {        
                if (singleton == null) {          
                      singleton = new Singleton();
                }        
          }    
      }    
  return singleton;
}

7、说说双重校验锁,以及volatile的作用?

1)、为什么要加锁?

如果不加锁的话,是线程不安全的,也就是有可能多个线程同时访问getInstance方法会得到两个实例化的对象。

2)、为什么不直接给getInstance方法加锁?

如果给getInstance方法加锁,那么每次访问mSingleton都需要加锁,增加了性能开销。

3)、为什么需要双重判断是否为空?

第一次判空是为了判断是否已经实例化,如果已经实例化就直接返回实例,不需要再加锁了。第二次判空是因为走到加锁这一步,如果线程A已经实例化,等线程B获得锁,进入的时候其实对象已经实例化完成了,如果不二次判空的话就会再次实例化。

4)、为什么还要加volatile修饰变量?(⭐️)

加volatile是为了禁止指令重排。指令重排指的是在程序运行过程中,并不是完全按照代码顺序执行的,会考虑到性能等原因,将不影响结果的指令顺序有可能进行调换。所以初始化的顺序本来是这三步:分配内存空间、初始化对象、将对象指向分配的空间。

如果进行了指令重排,由于不影响结果,所以2和3有可能被调换。所以就变成了:分配内存空间、将对象指向分配的空间、初始化对象。这就有可能会导致,假如线程A中已经进行到第二步,线程B进入第二次判空的时候,判断mSingleton不为空,就直接返回了,但是实际上此时mSingleton还没有初始化。

8、synchronized和volatile的区别?(⭐️)

  • 1、volatile的本质是在告诉jvm当前变量在本地内存中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程都会被阻塞住。
  • 2、volatile仅能使用在变量级别,它只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值操作和对象的引⽤赋值操作有效;synchronized则可以使用在变量、方法、和类级别。
  • 3、volatile仅能实现变量的修改可见性(变量位于主内存中,每个线程还有自己的工作内存,变量在自己线程的工作内存中有份拷贝,线程直接操作的是这个拷贝,被 volatile 修饰的变量改变后会立即同步到主内存,以保证变量的可见性),而synchronized则可以保证变量的修改可见性和原子性。
  • 4、volatile 标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

9、volatile 底层原理(⭐️)

1)、字节码层面

加上volatile会给变量加上ACC_VOLATILE访问标识。

2)、JVM层面

加了JVM层面的内存屏障,禁止了指令重排序。

什么是指令重排序?

指令重排序是指指令乱序执行,即在条件允许的情况下直接运行当前有能力立即执行的后续指令,避开为获取一条指令所需数据而造成的等待,通过乱序执行的技术提高执行效率。

volatile如何禁止进行指令重排序?

指令重排序会在被volatile修饰变量的赋值操作前添加一个内存屏障:令重排序时不能把后面的指令重排序移到内存屏障之前的位置。

JVM 是通过内存屏障来限制处理器的重排序的,那么什么是内存屏障呢?

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。它选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序都能得到正确的volatile语义。即:

  • 在每个volatile写操作前插入一个StoreStore屏障:禁止上面的普通写和下面的volatile写重排序。
  • 在每个volatile写操作后插入一个StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序。它保证了上面的写入对所有处理器可见,其开销是四种屏障中最大的(冲刷写缓冲器,清空无效队列)。在大多数处理器的实现中,它是一个万能屏障,兼具其它三种内存屏障的功能。
  • 在每个volatile读操作后插入一个LoadLoad屏障:禁止上面的volatile读和下面的所有普通读重排序。
  • 在每个volatile写操作后再插入一个LoadStore屏障:禁止上面的volatile读和下面的所有普通写重排序。
volatile与普通变量的重排序规则:

仅当第一个操作是普通变量读,第二个操作是volatile变量读,才可以进行重排序。

3)、CPU层面(汇编层面)

JVM仅仅是在内存中运行的一个软件,真正保障volatile的功能还是底层的硬件,主要是使用了CPU的内存屏障或汇编原子指令Lock来实现变量可见性和保持程序顺序:

汇编原子指令 Lock

它是一个全屏障,在执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。

CPU硬件层面的三层内存屏障

  • 写屏障 sfence:在sfence指令前的写操作必须在sfence指令后的写操作前完成。
  • 读屏障 lfence:在lfence指令前的读操作必须在lfence指令后的读操作前完成。
  • 读写屏障 mfence:在mfence指令前的读写操作必须在mfence指令后的读写操作前完成。

它的作用除了阻止屏障两侧的指令重排序外,还会强制把写缓冲区/高速缓冲区中的脏数据等写回主内存,或者让 CPU 缓存(L1、L2)中相应的数据失效。

10、synchronized修饰static方法和修饰普通方法有什么区别?

synchronized修饰普通方法:实际上是对调用该方法的对象加锁,俗称“对象锁”。也就是锁住的是这个对象,即this。如果两个线程分别访问对象的两个同步方法,就会产生互斥,这就是对象锁,一个对象一次只能进入一个操作。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

synchronized修饰静态方法:实际上是对该类对象加锁,俗称“类锁”。也就是锁住的是这个类,即ClassName.class。

如果一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法,由于静态方法会受到类锁限制,但是非静态方法会受到对象限制,所以两个方法并不是使用同一个锁,因此不会排斥,即它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!

11、类锁与对象锁的区别?

synchronized(this)添加的是对象锁,synchronized(ClassName.class)添加的是类锁,它们的区别如下:

对象锁

Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁那么当前线程会等待;synchronized方法正常返回或者抛异常终止时,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

类锁

对象锁是用来控制实例方法之间的同步,类锁是来控制静态方法或静态变量互斥体之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就说类的不同实例之间共享该类的Class对象,Class对象其实也仅仅是1个java对象,只不过有点特殊而已。所以所谓类锁,不过是Class对象的锁而已,获取类的Class对象有好几种方式,最简单的就是MyClass.class的方式。类锁和对象锁不是同一个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:一个线程访问静态sychronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

12、synchronized原理?

synchronized 的本质有两点:

1、保证⽅法内部或代码块内部资源的互斥访问

同⼀时间、由同⼀个 Monitor 监视的代码,最多只能有⼀个线程在访问。

  • 字节码层面:synchronized 代码块是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现,而 synchronized 同步方法使用了ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁。由于需要进行用户态到内核态的切换,所以同步操作是一个重量级的操作。
  • JVM层面:调用了操作系统的同步机制,例如Mutex、Semaphore等。
  • CPU层面(汇编层面):使用了 CPU 的lock指令,如果是CAS操作时,还会用到cmpxchg指令。

2、保证线程之间对监视资源的数据同步

任何线程在获取到 Monitor后的第⼀时间,会先将共享内存中的数据复制到⾃⼰的缓存中,任何线程在释放 Monitor 的第⼀时间,会先将缓存中的数据复制到共享内存中。

优势

哪怕我们一个同步方法中出现了异常,那么jvm也能够为我们自动释放锁,从而能主动规避死锁,不需要开发者手动释放锁。

劣势

必须要等到获取锁对象的线程执行完成,或者出现异常,才能释放掉。它不能够中途释放锁,也不能中断一个正在试图获得锁的线程,另外我们也不知道多个线程竞争锁的时候,是否成功获取到了锁,所以不够灵活。

13、锁分离与锁粗化

读读,读写,写读,写写。只要有写锁进入才需要做同步处理,但是对于大多数应用来说,读的场景要远远大于写的场景,因此一旦使用读写锁,在读多写少的场景中,就可以很好的提高系统的性能,这就是锁分离。

而锁粗化就是多次加锁。

14、lock 和 synchronized的区别?

  • synchronized 是 Java 关键字,是一种内置特性;而 Lock 是一个接口。
  • synchronized 会自动释放锁;而 Lock 需要手动释放,所以需要写到 try catch 块中并在 finally 中释放锁。(这里 finally 的作⽤:保证在⽅法提前结束或出现 Exception 的时候,依然能正常释放锁)
  • synchronized 无法中断等待锁;而 Lock 可以中断。
  • Lock 可以提高多个线程进行读/写操作的效率,当资源竞争激烈时,lock 的性能会明显的优于 synchronized。
  • Lock(ReentrantLock)的底层实现主要是volatile + CAS(乐观锁),而synchronized是一种悲观锁,比较耗性能。但是在JDK1.6以后对synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制,所以建议一般请求并发量不大的情况下使用synchronized关键字。

15、synchronized优化后的锁机制简单介绍一下,包括无锁、偏向锁、轻量级锁(包括自旋操作)、重量级锁?

锁的内存语义

当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中。

而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

对象头

Mark Word 32位不同状态的含义:

epoch为时间戳

每个Java对象都有对象头。如果是非数组类型,则用2个字节来存储对象头,否则用3个字节。对象头内容如下:

  • 1、Mark Word:存储对象的hashCode或锁信息等。
  • 2、Class Metadata Address:存储对象类型数据的指针。
  • 3、Array length:数组的长度(如果是数组)。

关键是要了解Mark Word的格式,针对不同32/64位的操作系统,使用了29/61位空间记录:

  • 偏向锁:线程ID、时间戳、分代年龄。
  • 轻量级锁:指向栈中锁记录的指针。
  • 重量级锁:指向堆中互斥量的指针。

⽆锁状态中的 hashcode 是懒加载的,⼀个对象⼀旦计算过 hashcode 就不会成为偏向锁。 ⽽⼀个偏向锁状态的对象,⼀旦计算过 hashcode (Object 默认的或者是 System 类提供的),那 么会⽴即升级到重量级锁。

所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了”偏向锁“和“轻量级锁”。在Java 6以前,所有的锁都是”重量级锁”。所以在 Java 6 及其之后,一个对象其实有四种锁状态,它们的级别由低到高依次是:

无锁

没有对资源进行绑定,任何线程都可以尝试去修改它。

偏向锁

偏向锁就是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多只会被一个线程锁定,使用偏向锁可以降低无竞争开销。

实现原理

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。当下次该线程进入这个同步块时,会去检查锁的Mark Word里面是不是存放的自己的线程ID。如果是,则表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁;如果不是,就代表有另一个线程来竞争这个偏向锁。

这个时候该线程会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,此时有两种情况:

  • 成功,表示之前的线程不存在了,Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
  • 失败,表示之前的线程仍然存在,那么暂停之前的线程,将自身升级为轻量级锁,此时会按照轻量级锁的方式竞争锁。

轻量级锁(自旋锁)

轻量级锁是由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。

自旋

线程自旋说白了就是让cpu在做无用功,比如:可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。

如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。

所以JDK采用了更聪明的方式-适应性自旋,即线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。自旋失败,线程会阻塞,并且会升级成重量级锁,或是存在同一时间多个线程访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

如何一定程度上防止锁升级为重量级锁?

使用 Object lock = new Object 代替 Hello.class 作为锁。

重量级锁

重量锁在JVM中又叫对象监视器(Monitor),只有重量级锁才是获取锁和释放锁,其本质是依赖于底层操作系统的互斥锁实现,操作系统实现线程之间的切换需要从用户态切换到内核态,成本非常高。

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级(HotSpot JVM 也支持)发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后进行降级。

16、ReentrantLock的内部实现

1)、AQS

ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取、线程的排队等待等。

AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定方法即可(其实就是对于共享变量state的一些简单的获取释放操作)。

它在内部中会通过操作 Sync 来实现同步,这种做法的好处是将线程控制的逻辑控制在 Sync 内部,而对外向用户提供的接口是自定义锁,这种聚合关系能够很好的解耦两者所关注的逻辑。

由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node有两种模式:共享模式和独占模式,这就导致 AQS 有两种不同的实现:独占锁(ReentrantLock 等)和分享锁(CountDownLatch、读写锁等)。

AQS的子类一般只需要重写tryAcquire(int arg)和tryRelease(int arg)两个方法即可。

2)、ReentrantLock的处理逻辑

ReentrantLock.lock 过程

公平和非公平锁

如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。

其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。

Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的公有逻辑,线程排队,阻塞,唤醒等等);

NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。

接着说下这两者的lock()方法实现原理:

NonFairSync(非公平可重入锁)
  • 1、先获取state值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;
  • 2、若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;
  • 3、其他情况,则获取锁失败。
FairSync(公平可重入锁)

公平锁的大致逻辑与非公平锁是一致的,不同的地方在于有了!hasQueuedPredecessors()这个判断逻辑,即便state为0,也不能贸然直接去获取,要先去看有没有还在排队的线程,若没有,才能尝试去获取,做后面的处理。反之,返回false,获取失败。

最后,说下ReentrantLock的tryRelease()方法实现原理:

若state值为0,表示当前线程已完全释放干净,返回true,上层的AQS会意识到资源已空出。若不为0,则表示线程还占有资源,只不过将此次重入的资源的释放了而已,并返回false。

小结

ReentrantLock是一种可重入的,可实现公平性的互斥锁,它的设计基于AQS框架,可重入和公平性的实现逻辑都不难理解,每重入一次,state就加1,当然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比自己申请早的线程在同步队列中等待,若有,则去等待;若没有,才允许去抢占

17、读写锁 ReentrantReadWriteLock

synchronized和ReentrantLock基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

一般情况下,读写锁的性能都会比排它锁好,因为在大多数场景下读是多于写的,在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

18、线程死锁的4个条件?

1)、死锁是如何发生的,如何避免死锁?

当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。

2)、造成死锁的四个条件:

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

在并发程序中,避免了逻辑中出现数个线程互相持有对方线程所需要的独占锁的情况,就可以避免死锁。

19、乐观锁 CAS介绍?

1)、悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

2)、乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

3)、使用场景

乐观锁适用于写比较少的情况下(多读场景),而一般多写的场景下用悲观锁就比较合适。

4)、CAS(Compare and Swap)

即比较并交换,它是设计并发算法时常用到的一种技术,java.util.concurrent.atomic包全完建立在CAS之上,没有CAS也就没有此包,可见CAS的重要性。

它的具体实现逻辑为:如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作。

目前的处理器基本都支持CAS,只不过不同厂家的实现不一样罢了。并且CAS是通过Unsafe(Unsafe是CAS的核心类,它提供了硬件级别的原子操作)实现的,由于CAS都是硬件级别的操作,因此效率会比普通的加锁高一些。

5)、底层原理

CAS 底层会根据操作系统和处理器的不同来选择对应的调用代码,以 Windows 和 X86 处理器为例,如果是多处理器,通过带 lock 前缀的 cmpxchg 指令对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作;如果是单处理器,通过 cmpxchg 指令完成原子操作。

6)、缺点

1、ABA漏洞

CAS看起来很美,但这种操作显然无法涵盖并发下的所有场景,并且CAS从语义上来说也不是完美的,存在这样一- 个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?

如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的"ABA"问题。

java.util.concurrent.atomic包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。

不过目前来说这个类比较"鸡肋",大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能会比原子类更加高效。

2、自旋CAS的开销问题

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

3、CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。

但是,从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

Android面试储备

这份资料我从2020年春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

面试系列文章已整理成了【PDF电子书】上传在GitHub,标星已经1200多star了,感兴趣的小伙伴可以自取哈:https://github.com/Timdk857/Android-Architecture-knowledge-2-

正文总共分为6个部分:

Java 基础(★★)
Java 高级(★★)
Android 基础(★★★)
Android 高级(★★★)
Android 项目(★★★)
项目面试常见问题(★★★)

一、Java 基础(★★)

面向对象思想
多态
异常处理
数据类型
Java 的 IO
集合
Java 多线程

Java 高级(★★)

Java 中的反射
Java 中的动态代理
Java 中的设计模式&回收机制
Java 的类加载器

Android 基础(★★★)

Android 基本常识
Activity
Service
BroadCastReceiver
ContentProvider&数据库

Android 中的布局
ListView
JNI & NDK
Android 中的网络访问
Intent
Fragment

Android 高级(★★★)

Android 性能优化
Android 屏幕适配
AIDL
自定义控件
Android 中的事件处理
Android 签名
Android 中的动画
网络协议
其他

项目面试常见问题(★★★)

  • 开发周期
  • 项目中遇到的难题
  • 项目中最大的收获
  • 项目是如何上线的
  • 项目是如何盈利的
  • 绘制项目架构图
  • 项目开发流程
  • 你在项目中的角色
  • 你负责项目中的哪些模块
  • 讲讲你负责模块的具体实现
  • 项目中都用到了哪些第三发框架
  • 有没有自己写过框架
  • 业余时间你是如何提高自己(学习)的
  • 有没有自己的技术 blog
  • 你的职业规划
  • 为什么离职
  • 为什么选择我们公司
  • 说说你们项目的亮点和不足
  • 你们的项目是如何保持风格一致的
  • 项目架构是如何搭建的
  • 屏幕适配是如何解决的
  • 都看过哪些源码
  • 项目版本是如何升级的
  • 用的什么版本控制工具
  • 你能独立开发吗
  • App 跟服务器是如何交互的
  • 需求文档写过吗
  • 接口文档写过吗
  • 云服务器都用过哪些
  • 第三方平台都用过哪些

简历+社招解答+经典HR面试解析

以上是整理总结的Android中高级面试遇到的真题解析,希望对大家有帮助;同时很多人经常也会遇到很多关于简历制作,职业困惑、HR经典面试问题回答等有关面试的问题。同样我也搜集整理了全套简历制作、金三银四社招困惑、HR面试等问题解析,有疑问,可以提供专业的解答。

对于Android开发的朋友来说应该是最全面最完整的面试资料,为了更好地整理每个模块,我参考了很多网上的优质博文和项目,力求不漏掉每一个知识点。很多朋友靠着这些内容进行复习,拿到了BATJ等大厂的offer,这个资料也已经帮助了很多的安卓开发者,希望也能帮助到你。

面试系列文章已整理成了【PDF电子书】上传在GitHub,标星已经1200多star了,感兴趣的小伙伴可以自取哈:https://github.com/Timdk857/Android-Architecture-knowledge-2-

文末

我一直觉得技术面试不是考试,考前背背题,发给你一张考卷,答完交卷等通知。

首先,技术面试是一个 认识自己 的过程,知道自己和外面世界的差距。

更重要的是,技术面试是一个双向了解的过程,要让对方发现你的闪光点,同时也要 试图去找到对方的闪光点,因为他以后可能就是你的同事或者领导,所以,面试官问你有什么问题的时候,不要说没有了,要去试图了解他的工作内容、了解这个团队的氛围。

找工作无非就是看三点:和什么人、做什么事、给多少钱,要给这三者在自己的心里划分一个比例。

最后,祝愿大家在这并不友好的环境下都能找到自己心仪的归宿!

感谢您阅读这篇文章,如果可以收到您的点赞,我将非常荣幸,希望我们可以成为朋友,一起分享交流Android技术。

posted @ 2022-01-10 11:17  Button123  阅读(78)  评论(0编辑  收藏  举报