面试题汇总+常见题型(三)

• (21)JDK和JRE的区别

       JRE顾名思义是java运行时环境,包含了java虚拟机,java基础类库。是使用java语言编写的程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
       JDK顾名思义是java开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。JDK包含了JRE,同时还包含了编译java源码的编译器javac,还包含了很多java程序调试和分析的工具:jconsole,jvisualvm等工具软件,还包含了java程序编写所需的文档和demo例子程序。

• (22)Java 8有哪些新特性

(1)Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用Lambda 表达式可以使代码变的更加简洁紧凑。
(2) 方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 。
(3) 函数式接口(FunctionalInterface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为lambda表达式。函数式接口可以现有的函数友好地支持 lambda。
(4) Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。我们只需在方法名前面加个default关键字即可实现默认方法。(另外还有个static方法,也有方法体)。
(5) Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
(6) Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。Optional 类的引入很好的解决空指针异常。
(7) jjs是个基于Nashorn引擎的命令行工具。它接受一些JavaScript源代码为参数,并且执行这些源代码。
(8) Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。
(9) 在Java8中,Base64编码已经成为Java类库的标准。Java 8 内置了 Base64 编码的编码器和解码器。
https://blog.csdn.net/yitian_66/article/details/81010434

• (23)Java 中的集合有哪些

       实现Collection接口的三大集合:List,Set,Queue
       自成一派的Map
所以综上,其实容器只有四种:Map,List,Set,Queue
List浅析:
       List手下有三个小弟:ArrayList,LinkedList,Vector。
       ArrayList和Vector是极为相似的,它们的底层都是数组,都是继承了相同的类AbstractList,实现了相同的接口:List, RandomAccess, Cloneable, java.io.Serializable

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

最重要的不同,Vector是线程安全的,它所有的方法都被synchronized修饰了!
Vector和ArrayList的初始容量都是10,ArrayList默认扩容1.5倍增长,Vector默认2倍增长。
LinkedList底层是双向链表。

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。

  • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
    底层节点内部类代码:
    private static class Node {
    E item;
    Node next;
    Node prev;

    Node(Node prev, E element, Node next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
    }
    }
    LinkedList在根据索引查找结点时,会有一个小优化,结点在前半段则从头开始遍历,在后半段则从尾开始遍历,这样就保证了只需要遍历最多一半结点就可以找到指定索引的结点。

HashMap的源代码解析:

默认初始化容量: static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
最大容量:static final int MAXIMUM_CAPACITY = 1 << 30;
扩容因子:static final float DEFAULT_LOAD_FACTOR = 0.75f;
链表树化最小长度:static final int TREEIFY_THRESHOLD = 8;
树化最小容量要求:static final int MIN_TREEIFY_CAPACITY = 64;
底层节点Node内部类的实现:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

初始化容量的方法:

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

• (24)创建线程的方式及实现

1.继承Thread类,重写run方法
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
3.通过Callable和FutureTask创建线程
4.通过线程池创建线程
1,2方法较为常见,就不再举例。
使用Callable创建线程:

public class Ticket implements Callable<Object> {
    @Override
    public Integer  call() throws Exception {
        System.out.println("使用callable实现线程");
        return 1 ;
    }
}
public class MainApp {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Object> callable=new Ticket();
        FutureTask<Object> futureTask=new FutureTask<>(callable);
        Thread t=new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());

    }
}

综上可以看到使用Callable和Runnable的区别:
       相同点:
1.两者都可用来编写多线程程序;
2.两者都需要调用Thread.start()启动线程;

       不同点:

  1. 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
  2. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

如果要使用线程池来创建线程的话,有四种线程池:
1 newCachedThreadPool——创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2.newFixedThreadPool——创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 3.newScheduledThreadPool——创建一个定长线程池,支持定时及周期性任务执行。
4. newSingleThreadExecutor——创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

• (25)线程安全的实现方式

这里使用《深入理解java虚拟机》中总结的内容,

1.互斥同步方案

a)使用Synchronized关键字。

三种表现形式
·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。
Synchronized如何释放?
1.程序出现异常
2.程序运行完同步块
原理:每一个对象都有一个Monitor对象监听与之关联,当Monitor被挟持后,它将处于锁定状态。监视器被占用时会被锁住,其他线程无法来获取该monitor。
synchronized关键字经过编译后会在同步块前后生成monitorenter,monitorexit俩个字节码指令。俩个字节码都需要有一个reference类型参数来指向被锁住或解锁的对象。JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
       线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。如果对象没有被锁定,或者当前线程已经拥有了对象的锁,就会把锁的计数器+1,相应的,在执行monitorexit指令时会将锁的计数器-1,当计数器为0的时候,锁就会被释放。如果获取锁失败就会阻塞等待,知道另外一个线程释放对象的锁。
       synchronized同步块对于同一线程是可重入的,不存在自己把自己锁死的概念。

b)使用java.util.concurrent包中的重入锁ReentrantLock

提到ReentrantLock,我的第一反应就是ConcurrentHashMap。大家都知道ConcurrentHashMap实现了锁分段技术来实现线程安全,不同的线程访问不同的Segment,彼此之间互不影响。
而这个Segment就实现了ReentrantLock。

static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}

Lock锁的使用:
Lock lock = new ReentrantLock();
lock.lock();
try {

}
finally {
lock.unlock();
}
不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

ReentrantLock相比synchronized实现了更多的高级功能:
1.等待可中断。当持有锁的线程长期不释放锁,正在等待的线程可以选择放弃等待,改为处理其他事情。
2.可实现公平锁。公平锁保证多个线程在等待同一锁的时候,必须按照等待的时间来依次获得锁。非公平锁不保证这一点。synchronized和ReentrantLock都是默认不公平的,但是ReentrantLock就可以通过带布尔值的构造函数来要求使用公平锁。
3.锁绑定多个条件。ReentrantLock对象可以绑定多个Condition对象,但在synchronized中,锁对象的wait(),notify(),notifyAll()可以实现一个隐含的条件,如果要和多一个条件关联的时候,就不得不额外添加一个锁,而ReentrantLock无需这样,只需要多次调用newCondition().

在选择锁的时候,尽量优先选择synchronized,不仅仅是因为它是原生的关键字,最重要的是 ReentrantLock的底层使用CAS和volatile实现的。
CAS会有ABA的问题,而volatile不保证原子性、
https://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1

2.非阻塞同步方案

CAS操作。

什么是CAS?
Compare-and-Swap.比较并交换。首先,CAS是基于硬件指令集实现的。其次,CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。无论是否更新,最终都会返回A的旧值。整个比较并替换的操作是一个原子操作。

CAS 的使用:
如何实现实现自增的安全性?
常用的方式:

public class LockDemo {
    private volatile int value;

    public synchronized int incrementAndGet(){
        return ++value;
    }
}

如果使用CAS:

public class AtomicDemo {
   private AtomicInteger value = new AtomicInteger();
   
   public int incrementAndGet(){
       return value.incrementAndGet();
   }
}

CAS 的原理 :使用无限循环来进行自增,如果发现自增成功则跳出循环,返回子增后的值。

 public fianl int incrementAndGet(){
     for( ; ; ){
       int current=get();
       int next=current+1;
       if(compareAndSet(current,next))
              return next;
     }
}

CAS 的缺点:ABA问题

变量V初次读取的A值,准备赋值的时候依旧是A,但在这过程中可能被其他线程改为B再改回来,CAS会认为它从来没有变过。

3.无同步方案

a)使用可重入代码 不依赖存储在堆上的数据和公共的系统资源,用到的状态量都是参数传入,不调用非可重入方法。
b)使用ThreadLocal Storage线程本地存储
http://my.oschina.net/clopopo/blog/149368

• (26)理解volatile

在讲述原理之前,我们先回顾下Java内存模型(JMM)的三大性质:
原子性,可见性,有序性
原子性是指一堆操作要么全部执行,要么全部不执行,可见性在于说明于“缓存一致性”原则。一条线程改变了变量的值,新值对于其他线程来说是可以立即得知的。有序性就是禁止指令重排序。valatile实现了可见性和有序性

valatile的原理:
1.将当前处理器缓存的数据写回到系统主内存。
2.这个写回内存的操作会使其他CPU中存储该内存地址的数据无效。

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

使用volatile写出DCL单例模式:

public class Singleton {
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    /**
     * 当第一次调用getInstance()方法时,instance为空,同步操作,保证多线程实例唯一
     * 当第一次后调用getInstance()方法时,instance不为空,不进入同步代码块,减少了不必要的同步
     */
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么要用 volatile static Singleton instance,就是因为volatile的第二语义:有序性,防止指令重排序,避免返回的Singleton对象没有初始化完全。

• (27)线程的状态

1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

• (28)死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
⑶ 如何预防死锁?
答:根据产生死锁的四个必要条件,只要使其中之一不能成立,死锁就不会出现。为此,可以采取下列三种预防措施:
1、采用资源静态分配策略,破坏"部分分配"条件;
2、允许进程剥夺使用其他进程占有的资源,从而破坏"不可剥夺"条件;
3、采用资源有序分配法,破坏"环路"条件。
⑷ 如何避免死锁?
答:死锁的避免不严格地限制死锁的必要条件的存在,而是系统在系统运行过程中小心地避免死锁的最终发生。最著名的死锁避免算
法是银行家算法。死锁避免算法需要很大的系统开销。
⑸ 如何检测死锁?
答:解决死锁的另一条途径是死锁检测方法,这种方法对资源的分配不加限制,即允许死锁的发生。但系统定时地运行一个"死锁检
测"程序,判断系统是否已发生死锁,若检测到死锁发生则设法加以解除。
⑹ 如何解除死锁?
答:常常采用下面两种方法:
1、资源剥夺法;2、撤消进程法

• (29)switch

Java5以前,switch(expr) 中的表达式只能是byte,short,char,int。从java5开始可以是枚举,从java7开始可以是String。

• (30)内存泄露和内存溢出

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

内存溢出是堆空间存储内存不够,GC后仍然没有足够的内存来存储新的对象,产生OOM错误。
内存泄露是对象虽然是无用的,但是它仍然是GCroot对象可关联的路径,没有办法GC回收,但是一旦积累过多就会造成OOM.

内存泄露的原因:
1.使用静态集合引起内存泄露。 这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放
2.监听器没有删除。
3.各种连接没有手动关闭。比如io流,数据库连接
4.内部类和外部模块的引用。
5.不正确的使用单例模式。单例模式引起内存泄露是常见的问题, 单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值