Android知识点总结(四):Java线程30问,看你到底懂没有

308 阅读23分钟

1、假如只有一个cpu,单核,多线程还有用吗?

  • 有用,会提高效率,这个效率是IO相关的
  • 单核CPU可以通过给每个线程分配时间片来实现多线程控制
  • CPU执行速度远大于IO。
  • CPU需要磁盘文件的时候,不是直接问IO,而是去问DMA,CPU问DMA之后,DMA问磁盘,磁盘把内容加载到内存之后,通知DMA好了,DMA再通知CPU。假设是单线程的话,CPU问了DMA就闲着了,那要等磁盘拷到内存才干活。也就是常说的IO操作不需要占用CPU
  • 假设CPU要执行4个任务,任务1的IO有10秒,那后面3个任务都要等着才能被CPU执行。如果用多线程,我可以让4个任务都开始干活。防阻塞。

多线程操作变量为什么会出错?

因为每个线程会拷贝变量到自己线程的工作内存,没有那么快复制回主线程。

某一个线程进入synchronized代码块前后,执行过程入如下:

  1. 线程获得互斥锁
  2. 清空工作内存
  3. 从主内存拷贝共享变量最新的值到工作内存成为副本
  4. 执行代码
  5. 将修改后的副本的值刷新回主内存中
  6. 线程释放锁

synchronized 锁住的是对象而非代码,只要访问的是同一个对象的 synchronized 方法,即使是不同的代码,也会被同步顺序访问。

3、wait sleep释放锁的事情

4、synchronized保证可见性的原理

反正外边人都进不了synchronized,那么这一段跑完之后再执行不行吗?

  • 不行可以,因为在synchronized中wait,然后权就被别的线程抢走了 image.png

2、sychronized修饰普通方法和静态方法的区别?什么是可见性?

  • 前者锁的是对象,后者锁的是类。锁的东西一变,锁就开了。
  • sychronized用来解决线程安全问题,可以保证原子性、可见性、有序性。
    • 原子性是指一个操作是不可中断的,要么一起成功,要么一起失败
      • 在Java中为了保证原子性,提供了两个高级的字节码指令moniterenter和moniterexit。这两个码指令,在Java对应的关键字是Synchronized。
      • 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是它并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。
      • 就是线程sleep也只是让出时间片不会释放锁
    • 可见性就是一个线程对共享变量修改,另外一个线程能够立马看到
      • 共享变量:如果一个变量在多个线程中都有副本,那么就是这个变量就是这个线程的共享变量
    • 有序性:程序按照代码的先后顺序执行,编译器为了性能优化,有时会改变程序中语句的顺序。

image.png

image.png

  • 1.sleep会使当前线程睡眠指定时间,不释放锁

  • 2.yield会使当前线程重回到可执行状态,等待cpu的调度,不释放锁

  • 3.wait会使当前线程回到线程池中等待,释放锁,当被其他线程使用notify,notifyAll唤醒时进入可执行状态

  • 4.当前线程调用 某线程.join()时会使当前线程等待某线程执行完毕再结束,底层调用了wait,释放锁

3、Synchronized在jdk 1.6之后做了哪些优化

  • 为了减少获得锁和释放锁所带来的性能消耗,对Synchronized锁,进行了优化,包括 偏向锁、轻量级锁、重量级锁。锁的相关信息都是放在对象头。
  • JDK1.6为了减少获得锁和释放锁所带来的性能消耗
  • 所以在JDK1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率.
  • Java中的锁有几种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 对象:对象头、实例数据和对齐填充。而锁的信息就在对象头。
  • Synchronized之所以有这么多锁是为了节约性能,慢慢膨胀锁,需求高才给你高需求的锁,不然就是浪费资源。
  • 锁消除 虚拟机在运行时,?如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。
  • 锁粗化

当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。

4、啥是偏向锁、轻量级锁、重量级锁

image.png 看我的这篇文章 偏向锁、轻量级锁、重量级锁 - 掘金 (juejin.cn)

4、乐观锁和悲观锁

  • 定义

    • 悲观锁,就是觉得我一定抢不赢,所以先不计算,抢到后才计算。乐观锁就觉得我一定抢得赢,先计算再说。在Synchronized中具体变现就是,乐观锁是自旋锁(自旋CAS),悲观锁就重量级锁ObjectMonitor。看我的这篇文章 偏向锁、轻量级锁、重量级锁 - 掘金 (juejin.cn)
  • 性能对比

    • 悲观锁 Synchronized 发生阻塞,上下文切换费时间 35个毫秒之间。上下文切换很多次。**一个时间片210ms左右**。
    • 乐观锁 CAS ,自旋 可能1000次,一个CPU指令也就0.6ns。600ns。总比上下文切换要快得多。没有阻塞状态。只是会不同重试。CAS实现如原子变量。原子变量底层就是靠CAS来做的。cas底层是靠CPU提供的原子操作指令实现的。
  • CAS原理

    • 在主线程中有一个共享变量V,同时在各个子线程中有副本A。乐观锁就是,我先不管抢没抢到,算了再说,计算后的值为B。这个时候我才比较,A和V是否一样,如果一样就把B赋值给主线程的共享变量V。如果不一样,那么把V再赋值给A再计算一遍出B。再比对下V和A,如果不对再往复,如果对了,就B赋值给V。CAS机制中的这个步骤是原子性的(指令层面提供的原子操作),判断相等和赋值这两条指令原子性。所以CAS机制可以解决多线程并发编程对共享变量的原子性问题。
    • CAS的底层原理是利用CPU提供的原子操作指令实现的。在CAS操作中,需要比较内存位置的值和预期值是否相等,如果相等则交换该位置的值。这个比较和交换的过程是一个原子操作,不会被其他线程干扰。因此,CAS能够保证对共享变量的操作是原子的,从而避免竞态条件和死锁等问题。
  • CAS的缺点

    • ABA问题,值可能只通过只通过值比较可能不妥,比如这个值第一次是1,第二次改为2,第三次改为1.和第一次一样,但不能说没被改过对此java除了一个AtomicStampedReference类来解决ABA问题。里边有一个version和引用。先比引用再比version。一样了才通过比对,才能改值。
    • 可能消耗较高CPU。看起来cas把阻塞变成非阻塞,减少了线程等地啊时间,但是如果线程之间竞争大,也就是CAS机制不能更新成功,CAS一直重试,耗费cpu哦。所以线程之间竞争少,用cas,竞争大用锁把。如果并发高还想用原子类,就用AtomicLong吧。
    • 只能控制共享变量原子性,不能控制代码块

5、ReentrantLock和Synchronized的异同点

image.png 相似点: 这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

功能区别: 这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

性能的区别: 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁

就是先等待的线程先获得锁。

ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是

像synchronized要么随机唤醒一个线程要么唤醒全部线程。

ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这

个机制

6、什么是AQS?

  • AQS就是AbstractQueuedSynchronized,抽象队列同步器,用于构建锁和同步器的框架。((ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 就实现了aqs)

  • 内部有个核心变量state表示有没有上锁,还有一个变量记录当前锁是哪个线程。还有一个等待队列专门存放加锁失败的线程的。

  • 这个state是volatile修饰的,所以可见性没有问题。进来的时候用cas去修改state。把它从0修改为1,就拿到锁了。这里保证了一次只进来跑一个线程,修改state失败的线程存在CLS队列当中。

  • 核心思想就是,如果被请求的共享资源限闲置,就将当前请求资源的线程设置为有效工作线程,将共享资源状态设置为锁定状态。如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。主要靠队列来实现,将暂时加锁失败的线程加入到队列

  • AQS支持独占锁(exclusive)和共享锁(share)两种模式。

  1. 独占锁:只能被一个线程获取到(Reentrantlock)。

  2. 共享锁:可以被多个线程同时获取(CountDownLatch,ReadWriteLock)。

无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取。state是一个volatile修饰的int变量,准确说是varhandle,aqs实现类都用cas去操作方式,用来表示锁状态、资源数等。

7、ReentrantLock的实现原理

  • Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于队列同步器—AQS实现的。
  • 可以看到Sync就继承自AQS,而ReentrantLock的lock解锁、unlock释放锁等操作其实都是借助的sync来完成。
  • Sync是个抽象类,ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类FairSync和NonfairSync,分别表示公平锁和非公平锁。

image.png

对于NonfairSync 而言,线程只要执行lock请求,就会马上尝试获取锁,不会管AQS当前管理的等待队列中是否存在正在等待的线程,这对于等待的线程不公平,因此NonfairSync表示非公平锁。

而FairSync表示公平锁,会在lock请求进行时,先判断AQS管理的等待队列中是否已经有正在等待的线程,有的话就不会尝试获取锁,直接进入等待队列,保证了公平性。此时 FairSync#lock 实际上执行的就是AQS的acquire

9、volatile关键字干了什么?(什么叫指令重排?)

  • volatile是java提供额可以声明在成员属性前的一个关键字
  • 保证内存可见性
    • 就是一个线程修改了共享变量,另外一个线程立马能够看见

    • 可见性问题只存在于多核CPU当中,因为单核CPU不存在多线程同时操作共享变量,线程修改后共享变量副本之后,会立马更新到主内存。因为单核CPU的缓存一致性协议。在某些缓存一致性协议中,可能会采用延迟更新的方式来避免频繁的缓存同步。这意味着,当一个线程修改了一个共享变量的值后,这个修改可能不会立即被其他线程看到。

    • 多核会出现这样问题是因为当一个线程修改了共享变量的值时,这个修改只会存在于该线程的工作内存中,而不会立即更新到主内存中。那么就存在滞后性。

    • 当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

    • 当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存。其它CPU的该共享变量副本会失效。

    • volatile 保证一致性具体的实现原理是在硬件层面上通过:MESI缓存:多个cpu从主内存读取数据到高速缓存中,如果其中一个cpu修改了数据 ,会通过总线立即回写到主内存中,其他cpu会通过总线嗅探机制感知到缓存中数据的变化并将工作内存中的数据失效,再去读取主内存中的数据。

    • volatile无法保证原子性:如:两个线程同时read主内存中相同的值,load到工作内存中,两个线程的cpu又同时use了count值并进行了计算且assign回工作内存,但其中一个线程通过总线store回主内存的 速度更快,于是由于(总线)MESI缓存一致性协议下的cpu总线嗅探机制就会使得另一个线程工作内存中的变量副本失效,导致之前的操作结果丢失(可以结合图片理解)。所以适合一写多读

    • 比如A B C 3个线程,修改volatile int v = 0,A修改v为8,然后同步到主内存之前,C已经将v修改为7。那么主内存中的v会从8改为7。B还未计算,所以值会从0该为7再改为8. image.png

  • 禁止指令重排
    • volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
    • JVM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障;

在每个volatile写操作的后面插入一个StoreLoad屏障;

在每个volatile读操作的后面插入一个LoadLoad屏障;

在每个volatile读操作的后面插入一个LoadStore屏障。

指令重排在双重锁定单例模式中的影响

基于双重检验的单例模式(懒汉型)

public class Singleton3 {

private static Singleton3 instance = null;

private Singleton3() {}

public static Singleton3 getInstance() {

if (instance == null) {

synchronized(Singleton3.class) {

if (instance == null)

instance = new Singleton3();// 非原子操作

}
}
return instance;

}

}

image.png

image.png

10、Volatile能否保证线程安全?在DCL上的作用是什么?

  • 只能保证可见性,不能保证原子性。
    • volatile 其中一个线程修改共享变量副本,立马同步到主线程,然后其它线程里的共享就失效了,就再去读主线程里的值,操作都断了,怎么保证原子性。
    • 再DCL中就是防止指令重排序

11、volatile和Synchronized的区别?

  • volatile只能作用于变量,Synchronized可以作用于变量、方法、对象
  • volatile保证可见性和有序性,无法保证原子性。Synchronized可以保证原子性、可见性,和线程间的有序性(无法保证线程内的有序性,线程内的代码可能被CPU指令重排)
  • volatile不阻塞线程,Synchronized阻塞线程
  • volatile本质是当前线程修改了共享变量副本直接立马更新到主内存,让其它线程中共享变量失效,重新去主内存中取。Synchronized是锁定当前共享变量,只有当前线程可以访问,其它线程阻塞。
  • volatile标记的变量不会被编译器优化既指令重排,Synchronized标记的变量可以被编译器优化。

12、死锁的场景和解决方案

  • 死锁是指2个或者以上的线程,因为竞争资源或者彼此通信而造成的一种阻塞现象,若无外力,他们将无法推进下去。

  • 危害

    • 1、程序或者,但是线程不工作
    • 2、没有任何异常信息可供检查
    • 3、死锁只能重启,对于已发布程序很严重
  • 发生的必要条件

    • 1、互斥条件:一个资源每次只能被一个进程使用
    • 2、请求与保持:一个资源因为请求资源而阻塞,但对已获得的资源保持不放
    • 3、不剥夺:进程已获得的资源,未使用完之前,不能强行剥夺
    • 4、环路等待:若干进程之间形成收尾详解的循环等待资源关系
  • 解决方法

    • 有序分配法
      • 把资源排序,
        • 比如先做菜先放油后放盐,A抢到油,B等着,A用完了,用盐,B拿到油。这样子就不会死锁
        • 如果A拿油,B拿盐则会互不相让
      • 银行家算法
        • 是一种避免死锁的策略,它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。在银行家算法中,系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。

13、锁的分类

13.1、乐观锁/悲观锁

  • 乐观锁觉得我肯定能拿到所,就先计算,再看有没有拿到。悲观锁就是觉得我肯定拿不到锁。

13.2、独享锁/共享锁

  • 独享锁就是,只能由持有线程能读写
  • 共享锁,就是可以多个线程持有读,但是只有一个线程能写

13.3、互斥锁/读写锁

  • 独享锁/共享锁就是一种广义说法,而互斥锁/读写锁是具体实现
  • 互斥锁 reentrantlock
  • 读写锁 ReadWriteLock

13.4、可重入锁

也叫递归锁,就同一线程再外层方法获取锁的时候,再进入内层方法会自动获取锁。synchronized和ReentrantLock都是可重入锁。一定程度避免死锁 synchronized void setA() throws Exception{

Thread.sleep(1000);

setB();

}

synchronized void setB() throws Exception{

Thread.sleep(1000);

}

上述代码中,如果synchronized不是可重入锁的话, setA 首先获取锁,在此方法还未释放锁的情况下,调用 setB 也需要获取相同的对象锁,此时会造成死锁。

13.5、公平锁/非公平锁

  • 公平锁,就是多个线程安安申请锁的顺序获取锁
  • 非公平锁,多个线程获取锁的顺序并不是按照顺序申请的,比如AQS中CHL队列,默认是非公平锁的,一个锁释放后,如果由新进来的锁,是可能与队列中的下一个锁竞争的。

13.6、分段锁

  • 是一种锁设计,比如concurrentHashmap,put的时候只对key对应的那个segment进行加锁而不是一整个数组。

13.7、偏向锁/轻量级锁/重量级锁

  • 这三种锁指的是锁的状态
  • 偏向锁是指一段同步代码一直被一个线程锁访问,那么线程自动获取到锁。代价低
  • 轻量锁是指当锁是偏向锁的时候,被另外一个线程锁访问,就锁膨为轻量级锁,其它线程通过自旋拿锁(超过10次,锁膨胀为重量级锁)
  • 重量锁会阻塞其它申请的线程,降低性能
  • 这3个锁目的就是,节约性能,不要一上来就上王炸。那个创造者认为,大部分情况是没竞争或者少竞争的,所以才发明了这三种锁。不然用重量级锁,切换上下文,麻烦。

13.8、自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去CAS尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

14、ThreadLocal是什么?

  • 就是提供了线程本地变量,保证访问的变量属于当前线程,每个线程都保存有一个副本。每个线程变量都不一样,把ThreadLocal作为key,存在ThreadLocalMap里罢了。详情看我这篇文章ThreadLocal 是嘛玩意? - 掘金 (juejin.cn)

15、Java多线程对同一个对象进行操作需要考虑什么?

多线程操作同一对象本质上是线程安全问题。本质上是线程的变量副本没有在修改后立马更新到主内存。这是可见性。另外就是原子操作问题、有序性和死锁。要注意。 解决这个问题呢主要就是加锁来解决。可以使用Synchronized。Reentraintlock、ReadWriteLock。volatile。等锁,看具体需求。

16、线程生命周期,线程可以多次调用start吗?会出现什么问题,为什么不能?

image.png

  • 不能 第二次调用必然会抛出IllegalThreadStateException
  • 多次调用start()方法会导致资源浪费、不确定的行为(线程之间的状态和行为是相互影响的)和程序崩溃等问题。因此,一个线程只能被启动一次,以避免这些问题。

17、sleep 、wait、yield与join的区别,wait 的线程如何唤醒它?

  • sleep 释放CPU不释放锁
  • yield 让出CPU类似于sleep但是不能有用于指定暂停多久
  • wait 释放CPU和锁,notify 唤起队列中随机的线程,notifyall唤起所有
  • join 类似wait,A线程调用B线程join,那么B结束后,A的阻塞状态才结束

18、sleep是可中断的么?

sleep方法是可被中断的。当一个线程正在执行sleep方法时,如果发生了中断,该线程会立即响应中断并抛出InterruptedException异常。因此,sleep方法可以被其他线程中断并停止执行。

19、保证线程执行顺序

  • 两个线程,一个运行完用另外Join。如果是是要顺序执行代码就用线程池的一个ScheduleThreadPool
  • 一个线程中执行到某一步再执行另外一个线程用wait/notifyAll
  • 如果是等某几个线程执行完再执行另外一个,可以用countDownlatch

20、写个非阻塞式生产消费者模型

21、如何开启一个线程

  • 继承Thread
  • 实现runnable
  • callable+futuretask。其实本质上也是用到了runnable。
  • 线程池,就是创建多个线程。

22、pthread 了解吗?new 一个线程占用多少内存?

  • pthread是POSIX线程(Portable Operating System Interface threads)的简称,它是一种线程标准,定义了创建和操纵线程的一套API。这些API使得程序员可以创建和管理多个线程,以及进行线程间的同步和通信。
  • 1.4及以前是256kb,以后是1M

23、HandlerThread是啥

  • HandlerThread 是一种可以使用 Handler 的 Thread。
  • HandlerThread 继承自 Thread 类,内部封装了 Looper 和 Handler 机制,使得我们可以在 HandlerThread 的 run() 方法中通过 Handler 发送和接收消息及 Runnable 对象,实现线程间的通信。
  • Looper.prepare();Looper.loop();都帮我们写好了
  • 使用 HandlerThread 可以避免在主线程中执行耗时任务导致的阻塞和 UI 卡顿等问题

24、AsyncTask的原理

  • AsyncTask是一个轻量级的异步任务类,可以在线程池中执行后台任务,然后把执行的进度和最终的结果传递给主线程,并在主线程中更新UI。
    • doInBackground 是在子线程,然后onProgressUpdate,onPostExecute是在主线程
  • AsyncTask通过线程池和Handler机制实现异步任务,同时可以在主线程中更新UI。

25、AsyncTask中的任务是串行的还是并行的?

AsyncTask中的任务是串行的。AsyncTask在内部使用一个线程池来执行任务,但任务是按照调用的顺序一个一个执行的,而不是并行执行的。这意味着如果你同时启动多个AsyncTask,它们会在一个接一个的顺序上执行,而不是同时执行。这种串行执行的方式可以避免多线程中的并发问题,并且简化了开发者对异步任务的并发控制。

26、 Android中操作多线程的方式有哪些?

第一种:Thread,Runnable

第二种:HandlerThread

第三种:AsyncTask

第四种:Executor

第五种:IntentService。跑在子线程中。通过handler或者广播发给主线程。

27、Android开发中怎样判断当前线程是否是主线程

  • Thread.currentThread().isMainThread()
  • Looper.getMainLooper().getThread() == Thread.currentThread()
  • Looper.getMainLooper() == Looper.myLooper();
  • Looper.getMainLooper().getThread().getId() == Thread.currentThread().getId();

28、在Android开发中,线程间的通信可以通过以下几种方式实现:

  • 如果只是为了同步

    • Synchronized
    • Reentraintlock
  • 需要线程间协作

    • wait\notify
    • CountDownLatch
    • Cyclicbarrier
  • 安卓中主线程和子线程通讯

    • handler
    • 广播
    • runOnUiThread
    • View.post(Runnable r)
      • 确保在正确的时机执行 UI 操作
      • 子线程可以传到主线程
    • AsyncTask