Android技术要点概括(一)

一、volatile关键字的做用和应用场景

① volatile保证可见性 ,即它会保证修改的值当即被更新到主存中,当有其余线程须要读取时,它会去内存中读取新值算法

(Java 内存模型规定了 全部的变量都存储在主内存中,每条线程还有本身的工做内存,线程的工做内存中保存了被该线程所使用的到的变量,线程对变量的操做都必须在工做内存中进行,不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递须要经过主内存来完成)数组

② 禁止进行指令重排序缓存

使用volatile 必须具有如下2个条件:安全

  1. 对变量的写操做不依赖当前值bash

  2. 该变量没有包含在具备其余变量的不变式中数据结构

二、谈谈对线程池的理解

线程池的优点:多线程

① 下降系统资源消耗,经过重用已存在的线程,下降线程建立和销毁形成的消耗;并发

② 提升系统响应速度,当有任务到达时,无需等待新线程的建立便能当即执行;app

③ 方便线程并发数的管控,线程如果无限制的建立,不只会额外消耗大量系统资源,更是占用过多资源而阻塞资源或oom等情况,从而下降系统的稳定性,线程池有效管控线程,统一分配、调优,提供资源使用率;ide

线程池关闭方式:

① shutdown() : 将线程池设置成shutdown状态,而后中断全部没有正在执行任务的线程。

② sutdownNow(): 将线程池设置成stop状态, 而后中断全部任务(包括正在执行的任务)的线程,并返回等待执行任务的列表。

Java中有四种不一样功能的线程池:

一、newFixedThreadPool : 该线程池锁容纳的最大线程数就是所设置的核心线程数,若是线程池中的线程处于空闲状态,它们不会被回收,除非是这个线程池被关闭了,若是全部的线程都处于活动状态,新任务就会处于等待状态,直到有线程空闲出来。优点:可以更快速响应外界请求。

二、newCachedThreadPool : 该线程池中核心线程数为0,最大线程数为Integer.Max_Value,当线程池中的线程都处于活动状态的时候,线程池就会建立一个新的线程来处理任务,该线程池中的线程超时时长为60秒,,因此当线程处于闲置状态超过60秒的时候就会被回收

三、newScheduledThreadPool : 核心线程数是固定的,当非核心线程处于限制状态时会当即被回收,经常使用于执行定时任务。

四、newSingleThreadExecutor : 该线程池只有一个核心线程,对于任务队列没有大小限制,也就意味着这一任务处于活动状态时,其余任务都会在任务队列中排队等候依次执行。

三、Java中的锁有哪些? 以及区别?

  1. 公平锁/非公平锁
    • 公平锁是指多个线程按照申请锁的顺序来获取锁的
    • 非公平锁是指多个线程获取锁的顺序并非按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会形成优先级反转后者饥饿现象。
  2. 可重入锁
    • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。 eg:
    synchronized void setA() throws Exception() {
       Thread.sleep(1000);
       setB();
    }
     synchronized void setB() throws Exception() {
       Thread.sleep(1000);
    }
    复制代码
  3. 独享锁/共享锁
    • 独享锁是指该锁一次只能被一个线程所持有
    • 共享锁是指该锁可被多个线程持有
  4. 互斥锁/读写锁
    • 互斥锁:一次只能一个线程拥有互斥锁,其余线程只有等待
    • 读写锁:多个读者能够同时进行,写者优于读者,写者必须互斥。
  5. 乐观锁/悲观锁
    • 悲观锁:当一个线程被挂起的时候,加入到阻塞队列,在必定的时间或条件下,再经过notify(), notifyAll() 唤醒,在某个资源不可用时,就将cpu让出,把当前等待线程切换为阻塞状态,等到资源可用了,就将线程唤醒,让他进入runnable状态等待cpu调度。
    • 乐观锁:每次不加锁而是假设修改数据以前其余线程必定不会修改,若是由于修改过产生冲突而失败就重试,直到成功为止。
  6. 分段锁
    • 分段锁是一种锁的设计,并非具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是经过分段锁的形式来实现高效的并发操做。ConcurrentHashMap 中的分段锁成为Segment,它相似于HashMap 的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表;当须要put元素时,并非对整个hashmap进行加锁,而是先经过hashCode来指导它要放在哪一个分段中,而后对这个分段进行加锁,因此当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行插入。可是在统计size 的时候,可就是获取hashMap 全局信息的时候,就须要获取全部的分段锁才能统计。

四、Java内存区域与内存模型

1、Java 内存区域

  • 方法区(公有):用户存储已被虚拟机加载的类信息、常量、静态常量,即时编译器编译后的代码等数据
  • 堆(公有):是JVM所管理的内存中最大的一块,惟一的目的就是存放实力对象,Java堆是垃圾收集器管理的主要区域,所以不少时候也被称为GC堆。
  • 虚拟机栈(线程私有):Java方法执行的内存模型,每一个方法在执行时都会建立一个栈帧,用户局部变量表,操做数栈,动态链接,方法出口等信息,每一个方法从调用直至完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 本地方法栈(线程私有):本地方法栈为虚拟机使用到的Native方法服务。
  • 程序计数器(线程私有):一块较小的内存,当前线程所执行的字节码的行号指示器,字节码解释器工做时,就是经过改变这个计数器的值来选取下一条须要执行的字节码指令。

2、Java 内存模型

Java 内存模型规定了全部的变量都存储到主内存中,每条线程还有本身的工做内存,线程的工做内存中保存了被该线程使用到的变量,线程对变量的全部操做都必须在工做内存执行,不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递须要经过主内存来完成。

五、Java类加载机制及类加载器

类加载机制定义:把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终造成能够被虚拟机直接使用的Java类型。

双亲委派模型:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器(Bootstrap ClassLoader)中。只有当父加载器在它的搜索范围内没有找到所需的类时,子加载器才会尝试本身去加载该类。

这样的好处是不一样层次的类加载器具备不一样优先级,好比全部Java对象的超级父类Object,位于rt.jar,不管哪一个类加载器加载该类,最终都是由启动加载器进行加载,保证安全,若是开发者本身编写一个Object类放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。

六、JVM中垃圾收集算法

  1. 标记-清除算法 ① 首先标记出全部须要回收的对象 ② 在标记完成后统一回收全部被标记的对象

    缺点:标记和清除两个过程效率不高,标记清除以后产生大量不连续的内存碎片,空间碎片太多可能会致使运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做

  2. 复制算法

    将可用内存按照容量大小划分为大小相等的两块,每次只使用其中的一块,当一块内存使用完了,就将还存活着的对象复制到另外一块上面,而后把使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也不用考虑内存碎片等复杂状况。

    缺点:将内存缩小为原来的一半。

  3. 标记-整理算法 复制收集算法在对象存活率比较高时,就要进行比较多的复制操做,效率就会变低; 标记过程仍然与 “标记-清除”算法同样,可是后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉边界之外的内存。

  4. 分代收集算法 通常把Java堆分为新生代和老生代, 这样就能够根据各个年代的特色采用最适合的收集算法。

    在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法。

    在老生代中,由于对象存活率高,没有额外空间对它进行分配担保,就必须采用“标记-清除”或“标记-整理”算法来进行回收。

七、JVM怎么判断对象是否已死?

1、引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能被再使用的。

主流的JVM里面没有选用引用计数法来管理内存,其中最主要的缘由就是它很难解决对象间的互循环引用的问题。

2、可达性分析算法

经过一些称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用连,当一个对象到GC Roots没有任何引用连相连时,证实此对象是不可用的,因此它们会被断定为可回收对象。如图B的对象是不可达的

在Java中,能够做为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象
 方法区中静态属性引用的对象
 方法区中常量引用的对象
 本地方法栈JNI(通常说的Native方法)引用的对象
复制代码

在可达性分析算法中,要真正宣告一个对象死亡, 至少要经历两次标记过程:

  1. 若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为没有必要执行
  2. 若是这个对象呗断定为有必要执行finalize()方法,那么这个对象将会放置在一个叫作F-Queue队列之中,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身,只要从新与引用链上的任何一个对象创建关联便可,那在第二次标记时它将会被移除出即将回收的集合;若是对象这时候尚未逃脱,那基本上它就真的被回收了。

八、LruCache 原理解析

LruCache的核心思想就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,放在队尾,即将被淘汰,而最近访问的对象放在对头,最后被淘汰。 如图所示:

这个队列是由LinkedHashMap来维护的,而LinkedHashMap是由数组+双向链表 的数据结构来实现的,其中双向链表的结构能够实现访问顺序和插入顺序,使得LinkedHashMap中的key,value对按照必定顺序排列起来。

经过下面构造函数来指定LinkedHashMap中双向链表的结构是访问顺序仍是插入顺序。

public LinkedHashMap(int initialCapacity,
    float loadFactor,
    boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
复制代码

其中accessOrder设置为true 则为访问顺序,为false,则为插入顺序。 以具体的例子解释,当设置为true 时

public static final void main(String[] args) {
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);
    map.put(0, 0);
    map.put(1, 1);
    map.put(2, 2);
    map.put(3, 3);
    map.put(4, 4);
    map.put(5, 5);
    map.put(6, 6);
    map.get(1);
    map.get(2);

    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}
复制代码

输出结果:

0:0
3:3
4:4
5:5
6:6
1:1
2:2
复制代码

即最近访问的最后输出,那么就正好知足的LRU缓存算法的思想,可见LruCache巧妙实现,就是利用了LinkedHashMap的这种数据结构。 下面咱们在LruCache源码中具体查看,怎么应用LinkedHashMap来实现缓存的添加,得到和删除的。

/**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
复制代码

从LruCache的构造函数中能够看到正是用了LinkedHashMap的访问顺序。

put()方法

/**
     * Caches {@code value} for {@code key}. The value is moved to the head of
     * the queue.
     *
     * @return the previous value mapped by {@code key}.
     */
    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }
复制代码

能够看到put()方法并无什么难点,重要的就是在添加过缓存对象后,调用 trimToSize()方法,来判断缓存是否已满,若是满了就要删除近期最少使用的算法。

trimToSize()方法

/**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
复制代码

trimToSize()方法不断地删除LinkedHashMap中队尾的元素,即近期最少访问的,直到缓存大小小于最大值。

当调用LruCache的get()方法获取集合中的缓存对象时,就表明访问了一次该元素,将会更新队列,保持整个队列是按照访问顺序排序。这个更新过程就是在LinkedHashMap中的get()方法中完成的。

先看LruCache的get()方法

public final V get(K key) {
        //key为空抛出异常
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
            //获取对应的缓存对象
            //get()方法会实现将访问的元素更新到队列头部的功能
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
复制代码

其中LinkedHashMap的get()方法以下:

public V get(Object key) {
    LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
    if (e == null)
        return null;
    //实现排序的关键方法
    e.recordAccess(this);
    return e.value;
}
复制代码

调用recordAccess()方法以下:

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    //判断是不是访问排序
    if (lm.accessOrder) {
        lm.modCount++;
        //删除此元素
        remove();
        //将此元素移动到队列的头部
        addBefore(lm.header);
    }
}
复制代码

因而可知LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,若是满了就用LinkedHashMap的迭代器删除队尾元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法得到对应集合元素,同时会更新该元素到队头。


整理了最近一段时间的学习要点,作一个概括总结,后续还会再作更新。。。。

相关文章
相关标签/搜索