Android面试问题汇总

2022-06-22 15:44:00 浏览数 (1)

设计模式相关

1.设计模式6大原则

1.1单一职责原则:就一个类而言, 应该仅有一个引起它变化的原因。

1.2开放封闭原则:类、模块、函数等应该是可以拓展的,但是不可修改。

1.3里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。

核心思想:在使用基类的的地方可以任意使用其子类,能保证子类完美替换基类。

1.4依赖倒置原则:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

问题描述:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案:将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1.5接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上。

1.6迪米特原则:一个软件实体应当尽可能少地与其他实体发生相互作用。

2.装饰模式和代理模式的区别

装饰器模式应当为所装饰的对象提供增强功能,而代理模式对所代理对象的使用施加控制,并不提供对象本身的增强功能。

装饰模式是起到了增加作用,例如ViewGroup拓展了view的功能,又能传入view,自己又是view

适配器的特点在于兼容

装饰器模式特点在于增强

代理模式的特点在于隔离

3.通过静态内部类实现单例模式有哪些优点?

1.不用 synchronized ,节省时间。

2.调用 getInstance() 的时候才会创建对象,不调用不创建,节省空间,这有点像传说中的懒汉式。

各种单例的写法参考:Java单例模式的写法及优缺点

另外关于volatile的知识点:如何理解volatile

4.静态代理和动态代理的区别,什么场景使用?

静态代理与动态代理的区别在于代理类生成的时间不同,**即根据程序运行前代理类是否已经存在,可以将代理分为静态代理和动态代理。**如果需要对多个类进行代理,并且代理的功能都是一样的,用静态代理重复编写代理类就非常的麻烦,可以用动态代理动态的生成代理类。

代码语言:javascript复制
// 为目标对象生成代理对象
public Object getProxyInstance() {
    return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
            new InvocationHandler() {

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("开启事务");

                    // 执行目标对象方法
                    Object returnValue = method.invoke(target, args);

                    System.out.println("提交事务");
                    return null;
                }
            });
}

静态代理使用场景:四大组件同AIDL与AMS进行跨进程通信

动态代理使用场景:Retrofit使用了动态代理极大地提升了扩展性和可维护性。

5.设计模式在android中的应用

5.1 AlertDialog、Notification源码中使用了Bulider(建造者)模式完成参数的初始化

5.2 Okhttp内部使用了责任链模式来完成每个Interceptor拦截器的调用,ViewGroup事件传递的递归调用就类似一条责任链

5.3 ListView/RecyclerView的Adapter的notifyDataSetChanged方法、广播、事件总线机制

5.4 ListView/RecyclerView/GridView的适配器模式

5.5 Context/ContextImpl外观模式

集合相关

1、集合框架,list,map,set都有哪些具体的实现类,区别都是什么?

List:有序、可重复;索引查询速度快;插入、删除伴随数据移动,速度慢;

Set:无序,不可重复;元素无放入顺序,元素不可重复,重复元素会盖掉

Map:键值对,键唯一,值多个;

线程安全集合类与非线程安全集合类

LinkedList、ArrayList、HashSet是非线程安全的,Vector是线程安全的;

HashMap是非线程安全的,HashTable是线程安全的;

StringBuilder是非线程安全的,StringBuffer是线程安的。

ArrayList与LinkedList的区别和适用场景

Arraylist:查询快,增删慢

LinkedList:增删快,查询慢

ArrayList和LinkedList怎么动态扩容的吗?

ArrayList:

ArrayList 的初始大小是0,然后,当add第一个元素的时候大小则变成10。并且,在后续扩容的时候会变成当前容量的1.5倍大小。

LinkedList:

linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。

ArrayList与Vector的区别和适用场景

1.Vector是线程同步的,所以它也是线程安全的,而ArraList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。

2.如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。

HashSet与TreeSet的区别和适用场景

1.TreeSet 是二叉树(红黑树的树据结构)实现的,TreeSet中的数据是自动排好序的,不允许放入null值。

2.HashSet 是哈希表实现的,HashSet中的数据是无序的可以放入null,但只能放入一个null,两者中的值都不重复,就如数据库中唯一约束。

3.HashSet要求放入的对象必须实现HashCode()方法,并且,放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复但是同一个类的对象可以放入不同的实例。

适用场景分析:

为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

HashMap与TreeMap、HashTable的区别及适用场景

HashTable是同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。

HashMap:适用于Map中插入、删除和定位元素。

Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

2.HashMap、ConcurrentHashMap、hash()相关原理解析?

HashMap 1.7的原理:

HashMap 底层是基于 数组 链表 组成的

给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容

为什么初始值大小为2的N次方,以后每次扩容后的结果也是2的N次方?

h&(length-1)运算等价于对length取模,也就是h%lenth,但是&比%具有更高的效率,同时也减少了hash碰撞。

和h&(length-1)相关,当容量大小(n)为2的n次方时,n**-1 的二进制的后几位全是1,在h为随机数的情况下,与(n-1)进行与操作时,会分布的更均匀

HashMap 1.8的原理:

就算链表长度超过8,进入了这个转化为红黑树的方法,还是要判断当前容量的大小是否小于64,如果小于这个值还是要进行扩容而不是转化为红黑树,修改为红黑树之后查询效率直接提高到了 O(logn)。

ConcurrentHashMap 1.7原理:

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

ConcurrentHashMap 1.8原理:

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率太低。和 1.8 HashMap 结构类似:其中抛弃了原有的 Segment 分段锁,而采用了 CAS synchronized 来保证并发安全性。

ArrayMap跟SparseArray在HashMap上面的改进?

详细看SparseArray和ArrayMap

泛型

简单介绍一下java中的泛型,泛型擦除以及相关的概念,解析与分派?

1、泛型的类型参数只能是类类型(包括自定义类),不是简单类型。

2、同一种泛型可以对应多个版本(因为参数类型是不确的),不同版本的泛型类实例是不兼容的。

3、泛型的类型参数可以有多个。

4、泛型的参数类型可以使用extends语句,例如。习惯上称为“有界类型”。

5、泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName(“java.lang.String”);

泛型擦除以及相关的概念

指的是可以将类型当作参数传递给一个类或者是方法。

泛型信息只存在代码编译阶段,在进入JVM之前,与泛型关的信息都会被擦除掉。

在类型擦除的时候,如果泛型类里的类型参数没有指定上限,则会被转成Object类型,如果指定了上限,则会被传转换成对应的类型上限。

类型擦除可以参考https://www.cnblogs.com/wuqinglong/p/9456193.html

注解

说说你对Java注解的理解?

注解相当于一种标记,在程序中加了注解就等于为程序打上了某种标记。程序可以利用ava的反射机制来了解你的类及各种元素上有无何种标记,针对不同的标记,就去做相应的事件。标记可以加在包,类,字段,方法,方法的参数以及局部变量上。

其他

String 为什么要设计成不可变的?

String是不可变的(修改String时,不会在原有的内存地址修改,而是重新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,所以char[]数组的内存地址不会被修改,而且String 也没有对外暴露修改char[]数组的方法。不可变性可以保证线程安全以及字符串串常量池的实现。

为什么Java里的匿名内部类只能访问final修饰的外部变量?

因为匿名内部类最终会编译成一个单独的类,而被该类使用的变量会以构造函数参数的形式传递给该类,例如:Integer paramInteger,如果变量不定义成final的,paramInteger在匿名内部类被可以被修改,进而造成和外部的paramInteger不一致的问题,为了避免这种不一致的情况,因次Java规定匿名内部类只能访问final修饰的外部变量。

String,StringBuffer,StringBuilder有哪些不同?

三者在执行速度方面的比较:StringBuilder > StringBuffer > String

String每次变化一个值就会开辟一个新的内存空间

StringBuilder:线程非安全的

StringBuffer:线程安全的

对于三者使用的总结:

1.如果要操作少量的数据用 String。

2.单线程操作字符串缓冲区下操作大量数据用 StringBuilder。

3.多线程操作字符串缓冲区下操作大量数据用 StringBuffer。

为什么复写equals方法的同时需要复写hashcode方法,前者相同后者是否相同,反过来呢?为什么?

要考虑到类似HashMap、HashTable、HashSet的这种散列的数据类型的运用,当我们重写equals时,是为了用自身的方式去判断两个自定义对象是否相等,然而如果此时刚好需要我们用自定义的对象去充当hashmap的键值使用时,就会出现我们认为的同一对象,却因为hash值不同而导致hashmap中存了两个对象,从而才需要进行hashcode方法的覆盖。

equals 和 hashcode 的关系?

hashcode和equals的约定关系如下:

1、如果两个对象相等,那么他们一定有相同的哈希值(hashcode)。

2、如果两个对象的哈希值相等,那么这两个对象有可能相等也有可能不相等。(需要再通过equals来判断)

final,finally,finalize的区别?

final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。

finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。

finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。

Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。

java中==和equals和hashCode的区别?

默认情况下也就是从超类Object继承而来的**equals方法与‘==’是完全等价的,比较的都是对象的内存地址,**但我们可以重写equals方法,使其按照我们的需求的方式进行比较,如String类重写了equals方法,使其比较的是字符的序列,而不再是内存地址。在java的集合中,判断两个对象是否相等的规则是:

代码语言:javascript复制
  1.判断两个对象的hashCode是否相等。
  2.判断两个对象用equals运算是否相等。

Java的四种引用及使用场景?

强引用(FinalReference):在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。

软引用(SoftReference):在内存不足时会被回收。用于实现内存敏感的高速缓存。

弱引用(WeakReferenc):只要GC回收器发现了它,就会将之回收。用于Map数据结构中,引用占用内存空间较大的对象。

虚引用(PhantomReference):在回收之前,会被放入ReferenceQueue,JVM不会自动将该referent字段值设置成null。其它引用被JVM回收之后才会被放入ReferenceQueue中。用于实现一个对象被回收之前做一些清理工作。

类的加载过程,Person person = new Person();为例进行说明。

1).因为new用到了Person.class,所以会先找到Person.class文件,并加载到内存中;

2).执行该类中的static代码块,如果有的话,给Person.class类进行初始化;

3).在堆内存中开辟空间分配内存地址;

4).在堆内存中建立对象的特有属性,并进行默认初始化;

5).对属性进行显示初始化;

6).对对象进行构造代码块初始化;

7).对对象进行与之对应的构造函数进行初始化;

8).将内存地址付给栈内存中的p变量。

Java并发

什么是线程池,如何使用?为什么要使用线程池?

线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间

Java中的线程池共有几种?

Java有四种线程池:

第一种:newCachedThreadPool

不固定线程数量,且支持最大为Integer.MAX_VALUE的线程数量:

代码语言:javascript复制
public static ExecutorService newCachedThreadPool() {
    // 这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE
    // 意思也就是说来一个任务就创建一个woker,回收时间是60s
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>());
}

可缓存线程池:

1、线程数无限制。 2、有空闲线程则复用空闲线程,若无空闲线程则新建线程。 3、一定程序减少频繁创建/销毁线程,减少系统开销。

第二种:newFixedThreadPool

一个固定线程数量的线程池:

代码语言:javascript复制
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    // corePoolSize跟maximumPoolSize值一样,同时传入一个无界阻塞队列
    // 该线程池的线程会维持在指定线程数,不会进行回收
    return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory);
}

定长线程池:

1、可控制线程最大并发数(同时执行的线程数)。 2、超出的线程会在队列中等待。

第三种:newSingleThreadExecutor

可以理解为线程数量为1的FixedThreadPool:

代码语言:javascript复制
public static ExecutorService newSingleThreadExecutor() {
    // 线程池中只有一个线程进行任务执行,其他的都放入阻塞队列
    // 外面包装的FinalizableDelegatedExecutorService类实现了finalize方法,在JVM垃圾回收的时候会关闭线程池
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

单线程化的线程池:

1、有且仅有一个工作线程执行任务。 2、所有任务按照指定顺序执行,即遵循队列的入队出队规则。

第四种:newScheduledThreadPool。

newScheduledThreadPool。

支持定时以指定周期循环执行任务:

代码语言:javascript复制
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

线程池原理?

从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)和HashSet集合构成。 从任务提交的流程角度来看,对于使用线程池的外部来说,线程池的机制是这样的:

代码语言:javascript复制
1、如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待;
2、如果正在运行的线程数 >= coreSize,把该task放入阻塞队列;
3、如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task;
4、如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。

理解记忆:1-2-3-4对应(核心线程->阻塞队列->非核心线程->handler拒绝提交)。

线程池都有哪几种工作队列?

1、ArrayBlockingQueue

是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

2、LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。

3、SynchronousQueue

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

4、PriorityBlockingQueue

一个具有优先级的无限阻塞队列。

怎么理解无界队列和有界队列?

有界队列

1.初始的poolSize < corePoolSize,提交的runnable任务,会直接做为new一个Thread的参数,立马执行 。

2.当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中。

3.有界队列满了之后,如果poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。

4.如果3中也无法处理了,就会走到第四步执行reject操作。

无界队列

与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。

多线程中的安全队列一般通过什么实现?

Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue.

对于BlockingQueue,想要实现阻塞功能,需要调用put(e) take() 方法。而ConcurrentLinkedQueue是基于链接节点的、无界的、线程安全的非阻塞队列。

Synchronized、volatile、Lock(ReentrantLock)相关

Synchronized相关

wait、sleep的区别和notify运行过程。

wait、sleep的区别

**最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。**wait 通常被用于线程间交互,sleep 通常被用于暂停执行。

首先,要记住这个差别,“sleep是Thread类的方法,wait是Object类中定义的方法”。尽管这两个方法都会影响线程的执行行为,但是本质上是有区别的。

Thread.sleep不会导致锁行为的改变,如果当前线程是拥有锁的,那么Thread.sleep不会让线程释放锁。如果能够帮助你记忆的话,可以简单认为和锁相关的方法都定义在Object类中,因此调用Thread.sleep是不会影响锁的相关行为。

Thread.sleep和Object.wait都会暂停当前的线程,对于CPU资源来说,不管是哪种方式暂停的线程,都表示它暂时不再需要CPU的执行时间。OS会将执行时间分配给其它线程。区别是,调用wait后,需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间。

线程的状态参考 Thread.State的定义。新创建的但是没有执行(还没有调用start())的线程处于“就绪”,或者说Thread.State.NEW状态。

Thread.State.BLOCKED(阻塞)表示线程正在获取锁时,因为锁不能获取到而被迫暂停执行下面的指令,一直等到这个锁被别的线程释放。BLOCKED状态下线程,OS调度机制需要决定下一个能够获取锁的线程是哪个,这种情况下,就是产生锁的争用,无论如何这都是很耗时的操作。

notify运行过程

当线程A(消费者)调用wait()方法后,线程A让出锁,自己进入等待状态,同时加入锁对象的等待队列。

线程B(生产者)获取锁后,调用notify方法通知锁对象的等待队列,使得线程A从等待队列进入阻塞队列。

线程A进入阻塞队列后,直至线程B释放锁后,线程A竞争得到锁继续从wait()方法后执行。

synchronized关键字和Lock的区别你知道吗?为什么Lock的性能好一些?

Lock(ReentrantLock)的底层实现主要是Volatile CAS(乐观锁),而Synchronized是一种悲观锁,比较耗性能。但是在JDK1.6以后对Synchronized的锁机制进行了优化,加入了偏向锁、轻量级锁、自旋锁、重量级锁,在并发量不大的情况下,性能可能优于Lock机制。所以建议一般请求并发量不大的情况下使用synchronized关键字。

synchronized 和 volatile 关键字的作用和区别。

Volatile

1)保证了不同线程对这个变量进行操作时的可见性即一个线程修改了某个变量的值,这新值对其他线程来是立即可见的。

2)禁止进行指令重排序。

作用

volatile 本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞住。

区别

1.volatile 仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。

2.volatile 仅能实现变量的修改可见性,并不能保证原子性;synchronized 则可以保证变量的修改可见性和原子性。

3.volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

4.volatile 标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

ReentrantLock的内部实现。

ReentrantLock的处理逻辑:

其内部定义了三个重要的静态内部类,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,当然在释放的时候,也得一层一层释放。至于公平性,在尝试获取锁的时候多了一个判断:是否有比自己申请早的线程在同步队列中等待,若有,去等待;若没有,才允许去抢占。

CopyOnWriteArrayList的了解。

Copy-On-Write 是什么?

在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉。

原理:

CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。

优点和缺点:

优点:

1.据一致性完整,为什么?因为加锁了,并发数据不会乱。

2.解决了像ArrayList、Vector这种集合多线程遍历迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!

缺点:

1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。

2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

使用场景:

1、读多写少(白名单,黑名单,商品类目的访问和更新场景),为什么?因为写的时候会复制新集合。

2、集合不大,为什么?因为写的时候会复制新集合。

3、实时性要求不高,为什么,因为有可能会读取到旧的集合数据。

ConcurrentHashMap加锁机制是什么,详细说一下?

Java7 ConcurrentHashMap

ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的"分段锁"的方案,ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

Java8 ConcurrentHashMap

抛弃了原有的 Segment 分段锁,而采用了 CAS synchronized 来保证并发安全性。结构上和 Java8 的 HashMap(数组 链表 红黑树) 基本上一样,不过它要保证线程安全性,所以在源码上确实要复杂一些。1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

CAS介绍?

Unsafe

Unsafe是CAS的核心类。因为Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。

CAS

CAS,Compare and Swap即比较并交换,设计并发算法时常用到的一种技术,java.util.concurrent包全完建立在CAS之上,没有CAS也就没有此包,可见CAS的重要性。当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。并且CAS也是通过Unsafe实现的,由于CAS都是硬件级别的操作,因此效率会比普通加锁高一些。

CAS的缺点

CAS看起来很美,但这种操作显然无法涵盖并发下的所有场景,并且CAS从语义上来说也不是完美的,存在这样一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的"ABA"问题。java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较"鸡肋",大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能回避原子类更加高效。

什么导致线程阻塞?

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)

yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。

wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。

Java虚拟机面试题

描述一下GC的原理和回收策略?

GC垃圾回收

0 人点赞