Android 面试题目集合

355 阅读39分钟

一、乐观锁和悲观锁

何谓悲观锁与乐观锁

乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

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

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。 乐观锁常见的两种实现方式

乐观锁一般会使用版本号机制或CAS算法实现。

1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。 举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

2. CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

需要读写的内存值 V 进行比较的值 A 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。 乐观锁的缺点

ABA 问题是乐观锁一个常见的问题

1 ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。 JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。 CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

二、Java锁机制

原文

juejin.cn/post/684490…

本文章主要讲的是Java多线程加锁机制,有两种:

Synchronized 显式Lock

一、synchronized锁

1.1synchronized锁是什么?

synchronized是Java的一个关键字,它能够将代码块(方法)锁起来

它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~

public synchronized void test() {
    // 关注公众号Java3y
    // doSomething
}

复制代码synchronized是一种互斥锁

一次只能允许一个线程进入被锁住的代码块

synchronized是一种内置锁/监视器锁

Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用**对象的内置锁(监视器)**来将代码块(方法)锁定的!

1.2synchronized用处是什么?

synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问) synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

1.3synchronized的原理

我们首先来看一段synchronized修饰方法和代码块的代码:

public class Main {
	//修饰方法
    public synchronized void test1(){

    }
    public void test2(){
		// 修饰代码块
        synchronized (this){

        }
    }
}

复制代码来反编译看一下:

同步代码块:

  • monitorenter和monitorexit指令实现的

同步方法(在这看不出来需要看JVM底层实现)

  • 方法修饰符上的ACC_SYNCHRONIZED实现。

synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。 具体可参考:

1.4synchronized如何使用

synchronized一般我们用来修饰三种东西:

修饰普通方法 修饰代码块 修饰静态方法

1.4.1修饰普通方法:

用的锁是Java3y对象(内置锁)

public class Java3y {


    // 修饰普通方法,此时用的锁是Java3y对象(内置锁)
    public synchronized void test() {
        // 关注公众号Java3y
        // doSomething
    }

}

1.4.2修饰代码块:

用的锁是Java3y对象(内置锁)--->this

public class Java3y {
    
    public  void test() {
        
        // 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this
        synchronized (this){
            // 关注公众号Java3y
            // doSomething
        }
    }
}

复制代码当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁) 所以,我们可以这样干:

public class Java3y {
    // 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
    private Object object = new Object();


    public void test() {

        // 修饰代码块,此时用的锁是自己创建的锁Object
        synchronized (object){
            // 关注公众号Java3y
            // doSomething
        }
    }

}

复制代码上面那种方式(随便使用一个对象作为锁)在书上称之为-->客户端锁,这是不建议使用的。 书上想要实现的功能是:给ArrayList添加一个putIfAbsent(),这需要是线程安全的。 假定直接添加synchronized是不可行的

使用客户端锁,会将当前的实现与原本的list耦合了:

书上给出的办法是使用组合的方式(也就是装饰器模式)

1.4.3修饰静态方法

获取到的是类锁(类的字节码文件对象):Java3y.class

public class Java3y {

    // 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class
    public synchronized void test() {

        // 关注公众号Java3y
        // doSomething
    }
}

1.4.4类锁与对象锁

synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。

它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!

public class SynchoronizedDemo {

    //synchronized修饰非静态方法
    public synchronized void function() throws InterruptedException {
        for (int i = 0; i <3; i++) {
            Thread.sleep(1000);
            System.out.println("function running...");
        }
    }
    //synchronized修饰静态方法
    public static synchronized void staticFunction()
            throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
            System.out.println("Static function running...");
        }
    }

    public static void main(String[] args) {
        final SynchoronizedDemo demo = new SynchoronizedDemo();

        // 创建线程执行静态方法
        Thread t1 = new Thread(() -> {
            try {
                staticFunction();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 创建线程执行实例方法
        Thread t2 = new Thread(() -> {
            try {
                demo.function();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // 启动
        t1.start();
        t2.start();
    }
}

结果证明:类锁和对象锁是不会冲突的!

1.5重入锁

我们来看下面的代码:

public class Widget {

	// 锁住了
	public synchronized void doSomething() {
		...
	}
}

public class LoggingWidget extends Widget {

	// 锁住了
	public synchronized void doSomething() {
		System.out.println(toString() + ": calling doSomething");
		super.doSomething();
	}
}

当线程A进入到LoggingWidget的*doSomething()方法时,此时拿到了LoggingWidget实例对象的锁。 随后在方法上又调用了父类Widget的doSomething()*方法,它又是被synchronized修饰。 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?

不需要的! 因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续**“开锁”**进去的! 这就是内置锁的可重入性。

1.6释放锁的时机

当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

不会由于异常导致出现死锁现象~

二、Lock显式锁

2.1Lock显式锁简单介绍

Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~ Lock显式锁是一个接口,我们来看看:

随便翻译一下他的顶部注释,看看是干嘛用的:

可以简单概括一下:

  • Lock方式来获取锁支持中断、超时不获取、是非阻塞的
  • 提高了语义化,哪里加锁,哪里解锁都得写出来
  • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
  • 支持Condition条件对象
  • 允许多个读线程同时访问共享资源

2.2synchronized锁和Lock锁使用哪个

前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要?? 必须是有的!!Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化(毕竟亲儿子,牛逼)

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患) 所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~

2.3公平锁

公平锁理解起来非常简单:

  • 线程将按照它们发出请求的顺序来获取锁

非公平锁就是:

  • 线程发出请求的时可以**“插队”**获取锁

Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁

  • 公平锁会来带一些性能的消耗的

三、最后

本文讲了synchronized内置锁和简单描述了一下Lock显式锁,总得来说:

synchronized好用,简单,性能不差 没有使用到Lock显式锁的特性就不要使用Lock锁了。

三、线程

1. 什么是线程

线程是操作系统能够进行调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。

2. 编写多线程的几种方式

一种是继承Thread类; 另一种是实现Runnable接口。 两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。 实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值

3. 什么是FutureTask

FutureTask实现了Future接口和Runnable接口,可以对任务进行取消和获取返回值等操作。

4. 如何强制启动一个线程

做不到,和gc一样,只能通知系统,具体何时启动有系统控制

5. 启用一个线程是调用run()还是start()方法

启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行

6. 说出线程调度和线程同步的方法

线程调度

  • wait( ):Object方法,必须在同步代码块或同步方法中使用,使当前线程处于等待状态,释放锁
  • notify ( ):Object方法,和wait方法联合使用,通知一个线程,具体通知哪个由jvm决定,使用不当可能发生死锁
  • notifyAll ( ):Object方法,和wait方法联合使用,通知所有线程,具体哪个线程获得运行权jvm决定
  • sleep( ):使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常

7. 线程同步

Synchronized修饰方法 Synchronized修饰代码块 Lock/ReadWriteLock ThreadLocal:每个线程都有一个局部变量的副本,互不干扰。一种以空间换时间的方式 java中有很多线程安全的容器和方法,可以帮助我们实现线程同步:如Collections.synchronizedList()方法将List转为线程同步;用ConurrentHashMap 实现hashmap的线程同步。BlockingQueue阻塞队列也是线程同步的,非常适用于生产者消费者模式 扩展:volatile(volatile修饰的变量不会缓存在寄存器中,每次使用都会从主存中读取):保证可见性,不保证原子性,因此不是线程安全。在一写多读/状态标志的场景中使用

8. 什么是可重入锁

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的

9. Java中如何停止一个线程

Java提供了很丰富的API但没有为停止线程提供API 可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程

10. 一个线程运行时发生异常会怎样

如果异常没有被捕获该线程将会停止执行 可以用UncaughtExceptionHandler来捕获这种异常

11. 多线程共享数据

使用同一个runnable对象 使用不同的runnable对象,将同一共享数据实例传给不同的runnable 使用不同的runnable对象,将这些Runnable对象作为一个内部类,将共享数据作为成员变量

12. 多线程的最佳实践/好习惯

  • 给线程起个有意义的名字
  • 避免使用锁和缩小锁的范围
  • 多用同步辅助类(CountDownLatch、CyclicBarrier、Semaphore)少用wait、notify
  • 多用并发集合少用同步集合

13. ThreadLocal的设计理念与作用

  • 供线程内的局部变量,线程独有,不与其他线程共享
  • 适用场景:多线程情况下某一变量不需要线程间共享,需要各个线程间相互独立

14. ThreadLocal原理,用的时候需要注意什么

  • ThreadLocal通过获得Thread实例内部的ThreadLocalMap来存取数据
  • ThreadLocal实例本身作为key值
  • 如果使用线程池,Threadlocal可能是上一个线程的值,需要我们显示的控制
  • ThreadLocal的key虽然采用弱引用,但是仍然可能造成内存泄漏(key为null,value还有值)
  • 扩展:Android中的ThreadLocal实现略有不同,使用Thread实例中的是数组存值,通过ThreadLocal实例计算一个唯一的hash确定下标。

15. 线程的基本状态及状态之间的关系

16. 如果同步块内的线程抛出异常会发生什么

线程内的异常可以捕获,如果没有捕获,该线程会停止运行退出 不论是正常退出还是异常退出,同步块中的锁都会释放

17. 什么是死锁(deadlock)

  • 两个线程互相等待对方释放资源才能继续执行下去,这个时候就形成了死锁,谁都无法继续执行(或者多个线程循环等待)

18. N个线程访问N个资源,如何避免死锁

  • 以同样的顺序加锁和释放锁

19. 为什么应该在循环中检查等待条件

  • 处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出

20. Java中的同步集合与并发集合有什么区别

  • 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合 并发集合性能更高

21. Java中活锁和死锁有什么区别

  • 这是上题的扩展,活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个 人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行

22. 怎么检测一个线程是否拥有锁

  • java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁

23. Java中ConcurrentHashMap的并发度是什么

  • ConcurrentHashMap把实际map划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap类构造函数的一个可选参数,默认值为16,这样在多线程情况下就能避免争用

24. 什么是阻塞式方法

  • 阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。这里的阻塞是 指调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。

25. 多线程中的忙循环是什么

  • 忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可 能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。

26. 如何保证多线程下 i++ 结果正确

  • 可以使用synchronized保证原子性,也可以使用AtomicInteger类 扩展:volatile只能保证可见性,不能保证原子性,因此不行

27. 简述Java中具有哪几种粒度的锁

  • Java中可以对类、对象、方法或是代码块上锁

同步方法和同步代码块的对比

  • 同步代码块可以指定更小的粒度
  • 同步代码块可以给指定实例加锁

28. 类锁和对象锁

类锁其实时一种特殊的对象锁,它锁的其实时类对应的class对象

四、线程中的关键字和类

0. sleep和wait方法的对比

  1. 两个方法都是暂停线程,释放cpu资源给其他线程
  2. sleep是Thread的静态方法,wait是Object的方法。
  3. sleep使线程进入阻塞状态;wait使线程进入等待状态,靠其他线程notify或者notifyAll来改变状态
  4. sleep可以在任何地方使用,必须捕获异常;而wait必须在同步方法或者同步块中使用,否则会抛出运行时异常
  5. 最重要的:sleep继续持用锁,wait释放锁 扩展:yield停止当前线程,让同优先级或者优先级高的线程先执行(但不会释放锁);join方法在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程

1. 线程的sleep()方法和yield()方法有什么区别

sleep方法使当前线程阻塞指定时间,随后进入就绪状态 yield方法使当前线程进入就绪状态,让同优先级或者更高优先级的线程先执行 sleep方法会抛出interruptedException

2. 为什么wait, notify 和 notifyAll这些方法不在thread类里面

  1. JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通 过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁 就不明显了

3. 为什么wait和notify方法要在同步块中调用

  1. java规定必须在同步块中,不在同步块中会抛出异常 如果不在同步块中,有可能notify在执行的时候,wait没有收到陷入死锁

4. synchronized关键字的用法

  1. synchronized 用于线程同步

  2. 可以修饰方法

  3. 可以修饰代码块

  4. 当持有的锁是类时,那么所有实例对象调用该方法或者代码块都会被锁

5. synchronized 在静态方法和普通方法的区别

  1. synchronized修饰静态方法时,锁是类,所有的对象实例用同一把锁 修饰普通方法时,锁是类的实例

6.当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?

  1. 不能。其它线程只能访问该对象的非同步方法。第一个线程持有了对象锁,第二个线程的同步方法也需要该对象的锁才能运行,只能在锁池中等待了。

7.Java中的volatile 变量是什么

  1. volatile是一个修饰符,只能修饰成员变量
  2. volatile保证了变量的可见性(A线程的改变,B线程马上可以获取到)
  3. volatile禁止进行指令重排序

8.写一个双检锁的单例

private static volatile Singleton instance;  

private Singleton(){}  
    
public Singleton getInstance(
if(singleton == null){
  synchronized(Singleton.class){
  if(singleton == null){
  singleton = new Singleton();
  }
  }
}
return sinlgeton;
)

9.单例的DCL方式下,那个单例的私有变量要不要加volatile关键字,这个关键字有什么用

  1. 要加
  • 两个线程同时访问双检锁,有可能指令重排序,线程1初始化一半,切换到线程2;因为初始化不是一个原子操作,此时线程2读到不为null直接使用,但是因为还没有初始化完成引起崩溃

10. Synchronized 和Lock\ReadWriteLock的区别

  • Synchronized时java关键字,Lock/ReadWriteLock接口,它们都是可重入锁
  • Synchronized由虚拟机控制,不需要用户去手动释放锁,执行完毕后自动释放;而Lock是用户显示控制的,要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  • Lock可以用更多的方法,比如tryLock()拿到锁返回true,否则false;tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间;Lock有lockInterruptibly()方法,是可中断锁 ReentrantLock可以实现公平锁(等得久的先执行)
  • ReadWriteLock是一个接口,ReentrantReadWriteLock是它的一个实现,将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁,提高了读写效率。

11. LockSupport

LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语

park 方法获取许可。许可默认是被占用的,调用park()时获取不到许可,所以进入阻塞状态 unpark 方法颁发许可

12. ReadWriteLock

*读写分离的锁,可以提升效率 *读读能共存,读写、写写不能共存

13. 可重入锁(RetrantLock)实现原理

  1. RetrantLock 是通过CAS和AQS实现的
  2. CAS(Compare And Swap):三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。原子性操作
  3. RetrantLock内部有一个AbstractQueuedSynchronizer实例,AbstractQueuedSynchronizer是一个抽象类,RetrantLock中有两种对他的实现,一种是公平锁,一种是非公平锁
  4. 在lock时,调用一个CAS的方法compareAndSet来将state设置为1,state是一个volitale的变量,并将当前线程和锁绑定
  5. 当compareAndSet失败时,尝试获取锁:如果和锁绑定的线程时当前线程,state+1
  6. 如果获取锁失败,将其加入到队列中等待,从而保证了并发执行的操作变成了串行
  7. 扩展:公平锁和非公平锁的区别:非公平锁无视队列,直接查看当前可不可以拿到锁;公平锁会先查看队列,队列非空的话会加入队列

14. Others

  1. synchronized 的实现原理以及锁优化?:Monitor
  2. volatile 的实现原理?:内存屏障
  3. CAS?CAS 有什么缺陷,如何解决?CompareAndSwap,通过cpu指令实现的
  4. AQS :AbstractQueueSynchronizer,是ReentrantLock一个内部类
  5. 如何检测死锁?怎么预防死锁?:死锁必须满足四个条件,破坏任意一个条件都可以解除死锁
  6. Fork/Join框架

五、线程池

0. 什么是线程池(thread pool)

  1. 频繁的创建和销毁对象很耗费资源,所以java引入了线程池。Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。
  2. Executors 是一个工具类,可以帮我们生成一些特性的线程池
  • newSingleThreadExecutor:创建一个单线程化的Executor,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newFixedThreadPool:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

我们常用的ThreadPoolExecutor实现了ExecutorService接口,以下是原理和参数说明

原理:

  • step1.调用ThreadPoolExecutor的execute提交线程,首先检查CorePool,如果CorePool内的线程小于CorePoolSize,新创建线程执行任务。
  • step2.如果当前CorePool内的线程大于等于CorePoolSize,那么将线程加入到BlockingQueue。
  • step3.如果不能加入BlockingQueue,在小于MaxPoolSize的情况下创建线程执行任务。
  • step4.如果线程数大于等于MaxPoolSize,那么执行拒绝策略。

参数说明:

  • ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

  • corePoolSize 核心线程池大小

  • maximumPoolSize 线程池最大容量大小

  • keepAliveTime 线程池空闲时,线程存活的时间

  • TimeUnit 时间单位

  • ThreadFactory 线程工厂

  • BlockingQueue任务队列

  • RejectedExecutionHandler 线程拒绝策略

扩展:ThreadPoolExecutor 的submit和excute方法都能执行任务,有什么区别?

  1. 入参不同:excute只能接受Runnable,submit可以接受Runnable和Callable
  2. submit有返回值
  3. 在异常处理时,submit可以通过Future.get捕获抛出的异常

1. 线程池如何调优,最大数目如何确认

  1. 线程池的调优优根据具体情况具体分析,尽量使系统资源利用率最大
  2. 例如如果cpu效率明显高于IO,那么就应该创建更多线程提高cpu利用率,避免io等待
  3. Android中最大数目可以是:cpu数目*2+1,但也要根据具体场景,例如picaso会根据网络状况调整最大数目

2. 如果你提交给ThreadPoolExcuter任务时,线程池队列已满,这时会发生什么

  1. 如果还没达到最大线程数,则新建线程
  2. 如果已经达到最大线程数,交给RejectExecutionHandler处理。
  3. 如果没有设置自定义RejectExecutionHandler,则抛出RejectExecutionExcuption

3.线程池的用法与优势

  • 优势: 实现对线程的复用,避免了反复创建及销毁线程的开销;使用线程池统一管理线程可以减少并发线程的数目,而线程数过多往往会在线程上下文切换上以及线程同步上浪费过多时间。

  • 用法: 我们可以调用ThreadPoolExecutor的某个构造方法来自己创建一个线程池。但通常情况下我们可以使用Executors类提供给我们的静态工厂方法来更方便的创建一个线程池对象。创建了线程池对象后,我们就可以调用submit或者excute方法提交任务到线程池中去执行了;线程池使用完毕后我们要记得调用shutdown方法来关闭它。

六、多线程中的工具类

0. Java并发编程:CountDownLatch、CyclicBarrier(栅栏)和Semaphore(信号量)

CountDownLatch:利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了

public class Test {
     public static void main(String[] args) {   
         final CountDownLatch latch = new CountDownLatch(2);
          
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
          
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                     Thread.sleep(3000);
                     System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                     latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
          
         try {
             System.out.println("等待2个子线程执行完毕...");
            latch.await();
            System.out.println("2个子线程已经执行完毕");
            System.out.println("继续执行主线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     }
}

CyclicBarrier: 实现让一组线程等待至某个状态之后再全部同时执行

public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕,继续处理其他任务...");
        }
    }
}

扩展(CyclicBarrier和CountdownLatch的区别):1.CountdownLatch等待几个任务执行完毕,CyclicBarrier等待达到某个状态;2.CyclicBarrier可以调用reset,循环使用;3.CyclicBarrier可以有含Runnable的构造方法,当达到某一状态时执行某一任务。

Semaphore:Semaphore可以控同时访问的某个资源的线程个数

public class Test {
    public static void main(String[] args) {
        int N = 8;            //工人数
        Semaphore semaphore = new Semaphore(5); //机器数目
        for(int i=0;i<N;i++)
            new Worker(i,semaphore).start();
    }
     
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }
         
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("工人"+this.num+"占用一个机器在生产...");
                Thread.sleep(2000);
                System.out.println("工人"+this.num+"释放出机器");
                semaphore.release();           
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1. java中的信号量(Semaphore)

Semaphore可以控制当前资源被访问的线程个数,超过最大个数后线程处于阻塞等待状态 当线程个数指定为1时,可以当锁使用

2. 怎么实现所有线程在等待某个事件的发生才会去执行

  1. 所有线程需要阻塞等待,并且观察到事件状态改变满足条件时自动执行,可以用以下方法实现

  2. 闭锁CountDownLatch:闭锁是典型的等待事件发生的同步工具类,将闭锁的初始值设置1,所有线程调用await方法等待,当事件发生时调用countDown将闭锁值减为0,则所有await等待闭锁的线程得以继续执行。

  3. 阻塞队列BlockingQueue:所有等待事件的线程尝试从空的阻塞队列获取元素,将阻塞,当事件发生时,向阻塞队列中同时放入N个元素(N的值与等待的线程数相同),则所有等待的线程从阻塞队列中取出元素后得以继续执行。

  4. 信号量Semaphore:设置信号量的初始值为等待的线程数N,一开始将信号量申请完,让剩余的信号量为0,待事件发生时,同时释放N个占用的信号量,则等待信号量的所有线程将获取信号量得以继续执行。

3. 生产者-消费者实现之阻塞队列

扩展:通过sychronized关键字实现 阻塞队列的特征是当取或放元素是,队列不满足条件(比如队列为空时进行取操作)可以阻塞等待,知道满足条件

public class BlockingQueueTest {
 private int size = 20;
 private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(size); 
 public static void main(String[] args) { 
 BlockingQueueTest test = new BlockingQueueTest();
 Producer producer = test.new Producer();
 Consumer consumer = test.new Consumer(); 
 producer.start();
 consumer.start(); 
 }
 class Consumer extends Thread{ 
 @Override public void run() {
 while(true){
 try { 
      //从阻塞队列中取出一个元素 
      queue.take(); 
      System.out.println("队列剩余" + queue.size() + "个元素"); 
      } catch (InterruptedException e) { 
      } } } 
} 
 class Producer extends Thread{
       @Override public void run() {
        while (true) {
         try { 
         //向阻塞队列中插入一个元素 
         queue.put(1); 
         System.out.println("队列剩余空间:" + (size - queue.size())); 
         } catch (InterruptedException e) {} }} 
}
}

4. ArrayBlockingQueue, CountDownLatch类的作用

ArrayBlockingQueue:一个基于数组实现的阻塞队列,它在构造时需要指定容量。当试图向满队列中添加元素或者从空队列中移除元素时,当前线程会被阻塞。 CountDownLatch:同步计数器,是一个线程工具类,可以让一个或几个线程等待其他线程

5. Condition

  1. Condition是一个接口,有await和signal方法,和Object的wait、notify类似 2.Condition 通过lock获得:Condition condition = lock.newCondition(); 3.相对于Object的wait、notify,Condition的控制更加灵活,可以满足唤起某一线程的目的

七、进程

0. 进程的三个状态

  1. 就绪状态:获得CPU调度时由 就绪状态 转换为 运行状态
  2. 运行状态:CPU时间片用完了由 运行状态 转换为 就绪状态 运行状态
  3. 阻塞状态:因等待某个事件发生而进入 阻塞状态,事件发生后由 阻塞状态 转换为 4. 就绪状态

1.进程的同步和互斥

  1. 互斥:两个进程由于不能同时使用同一临界资源,只能在一个进程使用完了,另一进程才能使用,这种现象称为进程间的互斥。
  2. 对于互斥的资源,A进程到达了该点后,若此时B进程正在对此资源进行操作,则A停下来,等待这些操作的完成再继续操作。这就是进程间的同步

2.死锁产生的必要条件

  1. 互斥:一个资源一次只能被一个进程所使用,即是排它性使用
  2. 不剥夺条件:一个资源仅能被占有它的进程所释放,而不能被别的进程强占
  3. 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源要求,而该资源又已被其它进程占有,此时请求进程阻塞,但又对已经获得的其它资源保持不放 环路等待条件:当每类资源只有一个时,在发生死锁时,必然存在一个进程—资源的环形链

八、类加载

0. 描述一下JVM加载class文件的原理机制

类加载器的作用是根据指定全限定名称将class文件加载到JVM内存中,并转为Class对象。 ###加载器的种类

  1. 启动类加载器(根加载器 Bootstrap ClassLoader):由native代码实现,负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中
  2. 扩展加载器(Extension ClassLoader):java语言实现,父加载器是Bootstrap,:负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
  3. 应用程序类加载器(Application ClassLoader):java实现,负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
  4. 自定义类加载器:有时为了安全会将类加密,或者从远程(服务器)加载类 ,这个时候就需要自定义类加载器。自定义通过继承ClassLoader类实现,loadClass方法已经实现了双亲委派模式,当父类没有加载成功时,调用当前类的findclass方法,所以我们一般重写该方法。

加载过程

  1. 类加载器采用双亲委派模型进行加载:每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。
  2. 类的生命周期可以分为七个阶段:加载 -> 连接(验证 -> 准备*(为静态变量分配内存并设置默认的初始值)* -> 解析*(将符号引用替换为直接引用)*)-> 初始化 -> 使用 -> 卸载

1. 类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式

  1. 使用双亲委派模式,保证只加载一次该类
  2. 我们可以使用自定义的类加载器加载同名类,这样就阻止了系统双亲委派模式的加载

2. ClassLoader的隔离问题

  1. JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName 两个相同的类可能因为两个ClassLoader加载而不兼容

九、反射和范型

0. 反射的原理和作用

  1. 通过类的class对象类获得类的各种信息,创建对应的对象或者调用方法
  2. App的动态加载或者Android中调用其他对象private方法,都需要反射

1. 类对象的获取方式

  • String.class:不执行静态块和动态构造块
  • "hello".getClass();:执行静态块和动态构造块
  • Class.forName("java.lang.String");:执行静态块,不执行动态构造块

2. 如何通过反射创建对象

  • String.class.newInstance();
  • String.class.getConstrutor(Stirng.class).newInstance("hello word");

3. 如何通过反射获取和设置对象私有字段的值

  • 通过类对象的getDeclaredField()方法获得(Field)对象
  • 调用Field对象的setAccessible(true)方法将其设置为可访问
  • 通过get/set方法来获取/设置字段的值

4. 通过反射调用对象的方法

  • 通过类对象的getMethod方法获得Method对象
  • 调用对象的invoke()方法

5. 范型

  • 范型可以用于类定义和方法定义
  • 范型的实现是通过擦除实现的,也就是说编译之后范型信息会被擦出

6. 通配符

通配符有两种用法:?extends A 和 ? super A

  • ?extends A 表示?的上界是A,具体什么类型并不清楚,适合于获取,获取到的一定是A类型
  • ? super A 表示?的下界是A,具体什么类型并不清楚,适合于插入,一定可以插入A类型

十、OKHttp实现原理

用通俗易懂的话解释OKHttp实现原理:

先Build OkHttpClient 再 Build Request Call call = OkHttpClient.newCall(request),

call对应着同步和异步两种方法。

OkHttpClient算是执行调用请求Call的工厂,这个工厂将会被用来发送Http请求和读取他们的返回,每一个Client都有自己的一个连接池connection pool和线程池thread pools。

Call实际上是被放到了Dispatcher中,Dispatcher中有同步和异步两种队列,使用ExecutorService来执行。

Call 是个接口,实现在RealCall中,RealCall实现了同步和异步两种方法都是调用的Dispatcher中的同步和异步方法。每个Call只能被执行一次。

Response 是通过 getResponseWithInterceptorChain()返回的。

Interceptor是OKHttp最核心的东西,它把实际的网络请求、缓存 、透明压缩等功能都统一了起来。每个功能都是一个Interceptor,他们形成一个Interceptor.Chain。(责任链模式)

HttpStream这个接口的实现类,Http1xStream和Http2xStream,分别对应Http1.1协议和Http/2和SPDY协议,通过writeRequestHeaders开始写入请求头到服务器,createRequestBody用于获取写入流来写入请求体。readResponseHeaders用于读取响应头,openResponseBody用于打开一个响应体。

StreamAllocation

流分配器,该类用于协调连接、流和请求三者之间的关系。通过调用newStream可以获取一个HttpStream实现

RetryAndFollowUpInterceptor

创建StreamAllocation,以此传入到后续的Interceptor中

处理重定向的Http响应

  • BridgeInterceptor 处理Header和Cookie

  • CacheInterceptor使用了DiskLruCache

  • ConnectInterceptor

  • CallServerInterceptor

p1-jj.byteimg.com/tos-cn-i-t2…