【爆炒洋葱圈】 第一次面试的总结笔记:Java:多态、多线程、基本工具类

这次的面试问的问题比较简单。总的流程是这样的:

自我介绍 -> 项目了解 -> Java基础知识 -> 询问对方

自我介绍和项目了解的话,后面自己总结一下吧。先趁热打铁把问的Java基础知识总结一下。

这次面试官有两个,应该都是搞java的。

由于这次面试是正式岗,而我是冲着实习生去的,然后我还是一个大三的本科生,所以面试官问的问题不是很难。在我看来是很简单的。

在这里只列出了一些我认为需要继续深挖的一些知识点。

Java基础

1、多态带来的益处?

这一问比较枯燥

首先回忆一下什么是多态

摘自:
Java多态性理解,好处及精典实例

  1. 多态的定义: 指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
  2. 实现多态的技术称为: 动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
  3. 多态的作用: 消除类型之间的耦合关系。
  4. 现实中,关于多态的例子不胜枚举。 比方说按下 F1键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。
多态存在的三个必要条件:
  • 要有继承
  • 要有重写
  • 父类引用指向子类对象
多态的好处:
  1. 可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环Ring,也同样工作。
  2. 可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
  3. 接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。
  4. 灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
  5. 简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高扩充性和可维护性。

1、Java多线程是怎么实现的?如何安全访问域?

实现多线程

在Java中,创建一个Runnable/Callable接口的实例(可以使用lambda表达式),或者构造一个Thread的子类(不推荐)。

然后,创建Thread类实例,将刚刚创建的对象传入Thread对象,并调用Thread.start()

不能直接调用Runnable里的run方法。 这样只会在当前线程跑run方法,并没有创建新的线程。

安全访问域

我的回答是这样的,安全访问域的三个方面:

  1. 设置域为private,然后为域的getter设置锁或者设置串行化(synchronized)
  2. 声明域为final
  3. 声明域为volatile

三个方法对了,然而我对三个方法的理解出现了偏差。特别是在回答的时候颠倒了后两个的实现机制。现在进行纠正。

第一个方法有一个解决方案叫做监视器。 在java中监视器可以用类实现。而监视器对类的定义比较严苛,包括:

  • 监视器是只包含私有域的类
  • 每个监视器的类的对象有一个相关的锁(java的类的内部锁)
  • 使用该锁对所有的方法进行加锁。也就是说监视器里所有的方法都要声明串行化(synchronized)。这样保证了一个线程在操作对象的时候其他的线程无法操作。

volatile为实例域的同步访问提供了一种免锁机制,告诉编译器和虚拟机该域可能被另一个进程并发并更新。他阻止CPU直接从寄存器读取值(可能会造成由于不同线程修改同一个变量时造成寄存器中的值和内存的值不同),也阻止了编译器对本条指令的优化(见下例),而是完整地编译代码并且安全地、直接地从内存读取值。

对于代码

XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;

对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。 如果键入volatile,则编译器会逐一地进行编译并产生相应的机器代码(产生四条代码)。

但是,volatile不能对操作实例域的方法保证原子性。

final也可以保证不变量的安全发布。在并发当中,final阻止cpu进行指令重排序来提供现成的可见性,来保证对象的安全发布,防止对象引用被其他线程在对象被完全构造完成前拿到并使用(比如说其他线程在域初始化完成之前就调用,从而导致获得null的引用进而导致发生错误)。

final与volatile有相似作用,不过final主要用于不可变变量(基本数据类型和非基本数据类型),进行安全的发布(初始化)。而volatile可以用于安全的发布不可变变量,也可以提供可变变量的可见性。


2、死锁是怎么产生的?怎么避免死锁?

死锁的规范定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。

死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁的产生

产生死锁的四个条件:

  • 互斥条件:一个资源每次只能被一个线程所使用(互斥锁)
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放(无法完整获得所有资源的锁而导致进程阻塞,从而无法释放已经获得的锁,进而导致其他线程无法获得完整资源,造成整个系统死锁)
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

可以使用哲学家就餐的例子解释

详情可见哲学家就餐问题与死锁总结

避免死锁

在哲学家就餐的例子里,可能产生死锁的代码是所有的线程统一先拿起左边的筷子,再拿起右边的筷子。 而如果两个资源如果有任意一个没有获取到,就会产生死锁。

破坏死锁的循环等待条件

上面存在一个循环等待:大家拿到左手边的筷子之后,都会等待拿到右边的筷子。如果一直拿不到右边的筷子,左边的筷子也不会被释放。进而造成系统思索。

破坏循环等待条件的其中一个方法是,不统一从左边拿取筷子,而是改为给每根筷子编号(0-4),然后拿编号较大的那一根。具体来看,处于4和0中间的哲学家,会先拿起右手边的筷子,这样就破坏了循环等待。

破坏死锁的请求和保持条件

当获取到左边的筷子之后,使用tryLock尝试获取右边的筷子,如果在等待一定的时间之后没有获取到右边的筷子,则释放左边筷子的锁。具体代码:

left.lock();
try{
        if(right.tryLock(1000,TimeUnit.MILLISECONDS)){
            try{
                Thread.sleep(1000);//进餐一段时间
            }finally {
                right.unlock();
            }
        }
        else{
            //没有获取到右手的筷子,放弃并继续思考
        }
    }finally {
        left.unlock();
    }
判断邻座状态

这个例子里面有个隐藏条件,就是当左右的哲学家都没有进餐的时候,那么所在的哲学家就可以进餐。

那么可以对每个哲学家设置一个isEatting的状态位,用于判断当前的状态。当左右两边的哲学家都没在进餐的时候,所在的哲学家才能进餐。

当然,这样的一个状态的判断和状态转换的时机需要精确地设计,要不然无法解决死锁的问题。

思考:上面给出的链接中,作者解决方法三的思路。


Java中,ArrayList、LinkedList、Vector的区别?

虽然问了区别,这里同时发散一下共同点

  • 他们共同处于util工具包内
  • 他们都继承自AbstractList,有共同的add, get, Iterator, ListIterator等方法
  • (还有啥?)

区别在哪呢?

首先说ArrayList和Vector。

他们两个都是实现了标记接口RandomAccess,也就是说他们是可随机访问的。通过阅读源码我们发现,在这两个类的内部维护着一个可膨胀的数组。当当前维护的数组容量超过了限度(由膨胀因子计算而得),那么在调用add的时候这两个类将会将原来的数组拷贝到一个更大的数组上。也可以手工通过ensureCapacity方法来指定这两个类可以直接接受的元组个数。

他们的最大元素个数是

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

Vector的出现比ArrayList要早,在1.2就出现了。他们的最大的区别就是ArrayList的访问不是线程安全的,而Vector的是线程安全的。摘录JavaDoc中的一句话

As of the Java 2 platform v1.2, this class was retrofitted toimplement the {@link List} interface, making it a member of the Java Collections Framework. Unlike the new collectionimplementations, {@code Vector} is synchronized. If a thread-safeimplementation is not needed, it is recommended to use {@link ArrayList} in place of {@code Vector}.

也就是说,当不需要串行化访问的时候,采取ArrayList是最好的。而如果需要串行化访问,那么请采用Vector。

和LinkedList相比,他们的最大优点就是可以做到随机访问。而向中间插入和删除元素的时候就没有LinkedList那么方便了。因为要把插入或者删除的元素的后面的所有的元素向后或者向前移动一格。

那么LinkedList与他们两个的区别就很大了。

通过阅读源码我们发现,LinkedList其实包装了一个双向链表。在类的内部定义了一个Node类,每次使用add方法,就会创建一个Node类的实例并且维护这个实例的前驱和后继。

理论上来说,LinkedList的元素个数可以无限增长,直至内存空间耗尽。

和ArrayList、Vector相比,他可以很快速的从中间删除和插入元素。然而他做不到随机访问。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值