Android开发

Java进阶篇

2022-03-23  本文已影响0人  hahaoop

背景

最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将Android开发岗位的知识点做一个系统的梳理,整理成一个系列:Android应用开发岗 面试汇总。本系列将分为以下几个大模块:
Java基础篇Java进阶篇常见设计模式
Android基础篇Android进阶篇性能优化
网络相关数据结构与算法
常用开源库、Kotlin、Jetpack

注1:以上文章将陆续更新,直到我找到满意的工作为止,有跳转链接的表示已发表的文章。
注2:该系列属于个人的总结和网上东拼西凑的结果,每个知识点的内容并不一定完整,有不正确的地方欢迎批评指正。
注3:部分摘抄较多的段落或有注明出处。如有侵权,请联系本人进行删除。

一、多线程和线程同步

线程概念: 指进程中的一个按顺序执行的流程,一个进程中可以运行多个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存

1、启动一个线程的方式

        Thread thread = new Thread() {
            @Override
            public void run() {  //重写Thread的run方法
                System.out.println("Thread started!");
            }
        };
        thread.start();
    }
    static void runnable() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread with Runnable started!");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
static void threadFactory() {
        final AtomicInteger count = new AtomicInteger(0);  //原子类型
        ThreadFactory factory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                return new Thread(runnable, "Thread-" + count.incrementAndGet());//++count
            }
        };
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " started!");
            }
        };
        Thread thread = factory.newThread(runnable);
        thread.start();
        Thread thread2 = factory.newThread(runnable); //复用runnable
        thread2.start();
        
    }
static void executor() {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread with executor started!");
            }
        };
        Executor executor = Executors.newCachedThreadPool();
        executor.execute(runnable);
        executor.execute(runnable);//复用
    }
static void callable() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(5000);//等待5秒模,拟耗时操作
                return "Done!";
            }
        };
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> future = executorService.submit(callable);
        while (true) {  //该循环的逻辑可以理解成加载网络转菊花的情况
            //xx1() 处理其他事情
            //xx2()
            if (future.isDone()) { //判断任务是否执行完
                try {
                    //卡主线程,阻塞式的方法,需要等5秒后(sleep的时长)取到值。用来获取任务执行完的时机
                    String result = future.get();
                    System.out.println("result:" + result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }

                break;
            }
        }
    }

2、线程池的使用

几种不同类型的线程池

用到的类

  • shutdown() 保守的结束,指如果线程池中有正在执行的任务或正在排队的任务,则等他们执行完后再结束。不允许有新的任务加进来或者排队
  • shutdownNow() 积极性的结束,指马上结束线程池中的所有任务,用到的是Thread的interrupt()方法

ThreadPoolExecutor构造方法的参数说明:
corePoolSize: 线程数的默认值(也称核心线程数),即当创建该线程池时,就自动创建默认数个线程,当线程被回收时,保留默认数个线程
maximumPoolSize: 线程数的最大值,当线程增加到最大数个后,线程就不能再增加
keepAliveTime: 线程等待被回收的时间,当线程执行完处于闲置状态时,线程不会被立即回收,需要等待设定的时候后再进行回收,便于复用(避免频繁创建线程损耗性能)
unit: 时间单位
workQueue: 线程池所使用的缓冲队列,达到核心线程数之后,新的线程放入该队列中
threadFactory: 线程池用于创建线程
handler: 线程池对拒绝任务的处理策略

面试题:自定义线程池需要注意什么,核心线程数是多少

分析下线程池处理的程序是CPU密集型,还是IO密集型
CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 * 2
CPU核数 = Runtime.getRuntime().availableProcessors()
如何确定是CPU密集型还是IO密集型?

具体程序是CPU密集型,还是IO密集型请参考链接

3、线程间通信

线程之间有两种通信的方式:消息传递和共享内存

Java内存模型:

Java内存模型中规定所有变量都存储在主内存(虚拟机内存的一部分)中,主要对应Java的堆内存。这里提到的变量实际上是指共享变量,存在线程间竞争的变量,如:实例变量、静态变量和构成数组对象的元素,而局部变量和方法参数因为是线程私有的,所以不存在线程间共享和竞争关系,所以也就不在前面提到的变量范围内。

线程的内存

每个线程有着自己独有的工作内存,工作内存中保存了被该线程使用到的变量,这些变量来自主内存变量的副本拷贝。线程对变量的所有读写操作都必须在工作内存中进行,不能直接读写主内存中的变量。而不同线程间的工作内存也是独立的,一个线程无法访问其他线程的工作内存中的变量。即子线程内的局部变量不能被其他线程共享。

线程间共享内存:

原理:
线程工作时,把需要的变量从主内存中拷贝到自己的工作内存,线程运行结束之后再将自己工作内存中的变量写回到主内存中,而多个线程间对变量的交互只能通过主内存来间接实现。具体的线程、工作内存、主内存的交互关系图如下:

thread_diagram.png
思考:通过上面的图和前面的介绍,我们就很容易明白我们平常所说的多线程编程时遇到数据状态不一致的问题是怎么产生的。例如:线程1和线程2都需要操作主内存中的共享变量A,当线程1已经在工作内存中修改了共享变量A副本的值但是还没有写回主内存,这时线程2拷贝了主内存中共享变量A到自己的工作内存中,紧接着线程1将自己工作内存中修改过的共享变量A的副本写回到了主内存,很明显线程2加载的共享变量A是之前的旧状态的数据,这样就产生了数据状态不一致的问题(即线程同步问题)。
参考链接

4、线程同步

如何解决以上线程同步问题呢?Java中有如下几种方式:

保证变量的原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是temp = a + 1;a = temp,是可分割的,所以他不是一个原子操作。
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。另,大致可以认为基础数据类型的访问和读写是具备原子性的。

使用volatile关键字

保证资源的同步性,即被volatile 修饰的变量,如int aaa = 0,会以最高的积极性去维护同步。当在其他线程中aaa被修改后,会立刻同步到主内存中。当其他线程读取aaa时,会先去主内存中读取,同步到子线程中,以防被其他线程修改。即保证了每次读写变量都从主内存中读。

使用synchronized关键字

该关键字作用在代码块、方法(一般方法、静态方法)上。由于可以使用不同的类型来作为锁,因此分成了类锁和对象锁。给代码块、方法加一个同步的monitor,被监视的地方具有同步性。被同一个monitor修饰的方法、代码块具有互斥性。

    public Object obj = new Object();
     // 静态同步函数,使用本类字节码做类锁(即Demo.class)
    public static synchronized void method1() {
    }
    public void method2() {
        // 同步代码块,使用字节码做类锁
        synchronized (Demo.class) { 
        }
    }
    // 同步函数,使用本类对象实例即this做对象锁
    public synchronized void method3() { 
    }
    // 同步代码块,使用本类对象实例即this做对象锁
    public void method4() {
        synchronized (this) { 
        }
    }
    public void method5() {
        //同步代码块,使用共享数据obj实例做对象锁。
        synchronized (obj) { 
        }
    }
}

synchronized括号里的参数,可以看成是一个monitor。

ReentrantLock

对代码块进行加锁。ReentrantLock允许同一个线程多次调用lock接口获取锁,每调用一次计数便加一。因此在释放锁的时候必须调用相应多次数unlock才能释放锁:

         mLock.lock();
         System.out.println("ThreadOne: start");
         try {
             for (int m = 0; m < Integer.MAX_VALUE; m++) {
                 num++;
             }
             System.out.println("ThreadOne: over");
         } finally {  //需要在finally中进行解锁,避免被锁的代码块出现异常而无法释放锁的情况
             mLock.unlock();
         }

同时,可以使用ReentrantReadWriteLock分别获取读锁和写锁,当只进行读操作时,只用读锁即可。保证了多个线程同时调用被读锁锁住的资源,不会出现同步性问题。

ThreadLocal的作用

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建了ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
作用:
ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。即实现了线程隔离,线程直接是不能共享内存的,即该对象不管在哪个线程中读取数据,读到的都是该线程中自己的数据,不跟其他线程同步。

5、线程间通信

结束线程的方式:

wait()、notifyAll()

join()

功能和wait()、notify()方法一样,当某个线程threadA执行时,需要等待另一个线程threadB先执行完,则可以在线程A中调用threadB.join(),达到等待的效果。

yield()

该方法作用是把自己的线程时间线让出一下下,让给跟自己同优先级的其他线程。作用等价于wait(),更短时间的wait,自动wait然后恢复。

乐观锁与悲观锁

数据库相关的业务常用(高并发控制),安卓中不常用。

死锁

多重锁容易导致死锁

synchronized (obj1) { 
            work();
            synchronized (obj2) {
                work();
            }
        }

sleep和wait区别

2、泛型


参考

3、Java 的 IO、NIO 和 Okio

io是输入输出流,它的作用就是对外部进行数据交互使用的,内部和外部分别表示的是内存以及内存以外的,外部包括手机文件,电脑文件和网络, 服务器等都称为外部 ,外部统称为文件和网络
详情略
参考链接

4、集合

概述:

Java集合里使用接口来定义功能,是一套完善的继承体系。Iterator是所有集合的总接口,其他所有接口都继承于它,该接口定义了集合的遍历操作,Collection接口继承于Iterator,是集合的次级接口(Map独立存在),定义了集合的一些通用操作。

image.png
参考链接

结构:

image.png

List:有序、可重复;索引查询速度快;插入、删除伴随数据移动,速度慢;
Set:无序,不可重复;
Map:键值对,键唯一,值多个;

面试题:

Arraylist:
优点:ArrayList是实现了基于动态数组的数据结构,因地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。
缺点:因为地址连续,ArrayList要移动数据,所以插入和删除操作效率比较低。
LinkedList:
优点:LinkedList基于链表的数据结构,地址是任意的,其在开辟内存空间的时候不需要等一个连续的地址,对新增和删除操作add和remove,LinedList比较占优势。LikedList 适用于要头尾操作或插入指定位置的场景。
缺点:因为LinkedList要移动指针,所以查询操作性能比较低。
适用场景分析:
当需要对数据进行对应访问的情况下选用ArrayList,当要对数据进行多次增加删除修改时采用LinkedList。

ArrayList: ArrayList 的初始大小是0,然后,当add第一个元素的时候大小则变成10。并且,在后续扩容的时候会变成当前容量的1.5倍大小。
LinkedList: LinkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

Java中HashMap是利用“拉链法”处理HashCode的碰撞问题。在调用HashMap的put方法或get方法时,都会首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法。hashMap基于hasing原理,我们通过put和get方法存取对象。当我们将键值对传递给put方法时,他调用键对象的hashCode()方法来计算hashCode,然后找到bucket(哈希桶)位置来存储对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当碰撞发生了,对象将会存储在链表的下一个节点中。hashMap在每个链表节点存储键值对对象。当两个不同的键却有相同的hashCode时,他们会存储在同一个bucket位置的链表中。键对象的equals()来找到键值对。

常见面试题

5、ClassLoader

概念:

Classloader负责将Class加载到JVM中,并且确定由那个ClassLoader来加载(父优先的等级加载机制)。还有一个任务就是将Class字节码重新解释为JVM统一要求的格式

类加载器的双亲委派模型

当一个类加载器收到一个类加载的请求,它首先会将该请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该被传入到顶层的启动类加载器(Bootstrap ClassLoader)中,只有当父类加载器反馈无法完成这个列的加载请求时(它的搜索范围内不存在这个类),子类加载器才尝试加载。其层次结构示意图如下:

image.png
作用:

6、JVM

JVM基本构成

image.png

从上图可知,JVM主要包括四个部分:

方法区(MethodArea):用于存储类结构信息的地方,包括常量池、静态常量、构造函数等。虽然JVM规范把方法区描述为堆的一个辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。

java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域。从存储的内容我们可以很容易知道,方法和堆是被所有java线程共享的。
java栈(Stack):java栈总是和线程关联在一起,每当创一个线程时,JVM就会为这个线程创建一个对应的java栈在这个java栈中,其中又会包含多个栈帧,每运行一个方法就建一个栈帧,用于存储局部变量表、操作栈、方法返回等。每一个方法从调用直至执行完成的过程,就对应一栈帧在java栈中入栈到出栈的过程。所以java栈是现成有的。
程序计数器(PCRegister):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证程切换回来后,还能恢复到原先状态,就需要一个独立计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
本地方法栈(Native MethodStack):和java栈的作用差不多,只不过是为JVM使用到native方法服务的。

GC的原理和回收策略

引用管理

Java中使用可达性算法对对象进行是否可回收的标记算法。通过一系列称为GCRoots的对象作为起始点,从这些节点从上向下搜索,所走过的路径称为引用链,当一个对象没有任何引用链与GCRoots连接时就说明此对象不可用,也就是对象不可达。

回收算法

上一篇 下一篇

猜你喜欢

热点阅读