Java老司机带你刨面试题(集合与线程篇_标题党请进)

史上最全面试题(集合与线程篇_标题党请进)

1.1 Collection 和 Map

掌握Collection和Map的继承体系(有时候他让你画)
image
image

1.2 List和Set区别

List,Set 都是继承自 Collection 接口
List 特点:元素有放入顺序,元素可重复
Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉
(注意:元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals() 方法 ,另外 list 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是set 只能用迭代,因为他无序,无法用下标来取得想要的值.)
Set 和 List 对比:
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变.
List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

1.3 List和Map区别

List 是对象集合,允许对象重复
Map 是键值对的集合,不允许 key 重复

1.4 ArrayList与Vector区别

ArrayList 和Vector 都是用变长数组实现的,主要有这么三个区别:
Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而 ArrayList 不是,这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这样就导致了 Vector 在效率上无法与 ArrayList 相比;两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的
增加方式是不同。
Vector 可以设置增长因子,而 ArrayList 不可以。
Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用.

适用场景分析:

Vector 是线程同步的,所以它也是线程安全的,而 ArrayList 是线程异步的, 是不安全的。如果不考虑到线程的安全因素,一般用 ArrayList 效率比较高。
如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用 Vector 有一定的优势

1.5 ArrayList与LinkedList区别

Arraylist:
优点:ArrayList 是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)
缺点:因为地址连续, ArrayList 要移动数据,所以插入和删除操作效率比较低

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

1.6 HashMap和HashTable的区别

  1. hashMap 去掉了 HashTable 的 contains 方法,但是加上了 containsValue
    ()和 containsKey()方法。
  2. hashTable 同步的,而 HashMap 是非同步的,效率上逼 hashTable 要高.
  3. hashMap 允许空键值,而 hashTable 不允许。
    注意:
    TreeMap:非线程安全基于红黑树实现。TreeMap 没有调优选项,因为该树总处于平衡状态。
    Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

1.7 HashSet和HashMap区别

set 是线性结构,set 中的值不能重复,hashset 是 set 的 hash 实现,hashset中值不能重复是用 hashmap 的 key 来实现的。
map 是键值对映射,可以空键空值。HashMap 是 Map 接口的 hash 实现,key 的唯一性是通过 key 值 hash 值的唯一来确定,value 值是则是链表结构。
他们的共同点都是 hash 算法实现的唯一性,他们都不能持有基本类型,只能持有对象

多线程篇(简单总结了一下)

1.1 创建线程的方式及实现

Java 中创建线程主要有三种方式:

一、继承 Thread 类创建线程类
  • 定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表了线程要完成的任务。因此把 run()方法称为执行体。
  • 创建 Thread 子类的实例,即创建了线程对象。
  • 调用线程对象的 start()方法来启动该线程。
二、通过 Runnable 接口创建线程类

(1) 定义 runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样是该线程的线程执行体
(2) 创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象.
(3) 调用线程对象的 start()方法来启动该线程.

三、通过 Callable 和 Future 创建线程

(1) 创建 Callable 接口的实现类,并实现 call()方法,该 call()方法将作为线程执行体,并且有返回值.
(2) 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象, 该 FutureTask 对象封装了该 Callable 对象的 call()方法的返回值.
(3) 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程.
(4) 调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值

创建线程的三种方式的对比

采用实现 Runnable、Callable 接口的方式创建多线程时,优势是:线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类.
在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想.
劣势是:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法.
使用继承 Thread 类的方式创建多线程时优势是:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程.
劣势是:线程类已经继承了 Thread 类,所以不能再继承其他父类

线程池创建的几种方式

newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
newCachedThreadPool()

创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制
newSingleThreadExecutor()
这是一个单线程的 Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行
newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer。

线程的生命周期

生命周期的五种状态

新建(new Thread)
当创建 Thread 类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1=new Thread();
就绪(runnable)
线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队等候得到 CPU 资源。例如:t1.start();
运行(running)
线程获得CPU 资源正在执行任务(run()方法),此时除非此线程自动放弃 CPU
资源或者有优先级更高的线程进入,线程将一直运行到结束。
死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行.
自然终止:正常运行 run()方法后终止
异常终止:调用 stop()方法让一个线程终止运行
堵塞(blocked)
由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。
正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态.
正在等待
调用 wait()方法。(调用 motify()方法回到就绪状态)
被另一个线程所阻塞:调用 suspend()方法。(调用 resume()方法恢复)

悲观锁 乐观锁

乐观锁 悲观锁是一种思想。可以用在很多方面.
比如数据库方面.
悲观锁就是 for update(锁定查询的行)
乐观锁就是 version 字段(比较跟上一次的版本号,如果一样则更新,如果失败则要重复读-比较-写的操作。)
JDK 方面
悲观锁就是 sync
乐观锁就是原子类(内部使用 CAS 实现)

本质来说,就是悲观锁认为总会有人抢我的。乐观锁就认为,基本没人抢。

乐观锁的业务场景及实现方式

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

发布了16 篇原创文章 · 获赞 7 · 访问量 670

猜你喜欢

转载自blog.csdn.net/lovemore2/article/details/104525538