谈谈你对Java平台的理解?
这个问题让我想起了自己刚刚学习Java的时候。那时候对其最大的理解,恐怕就是它最好就业、需求量大了。 那么其实这是一个最大的表象,这表明了Java作为一款编程语言的用途之广泛,能力之强大。
Java能被称为一个平台,表面上看是有许多除了Java以外的语言,如Scala、kotlin、JRuby等等,是运行在JVM上的,由于Java是能“一次编写,随处运行”,得益于JVM屏蔽了本地操作系统和硬件的复杂性。
所以,许多人认为Java是解释执行的,也并无问题,只不过JIT的引入,能将Java代码编译为机器语言,提高执行效率,而JVM本身也支持通过参数来实现混合编译。
对比Java中的Exception和Error?
Java中的Exception和Error都是Java类,他们用来表达异常信息。

所有的异常都实现Throwable接口。 其中Error,通常是那些在运行中,虚拟机抛出的错误,几乎是不可恢复的错误,比如类未定义,内存不足等。 而Exception,是在运行中,程序引发的错误。
Exception又有两类,一类是检查的,需要在代码中显式try catch的异常,另一类,则是不可检查的,诸如 数组下标越界这样的异常。
在异常的处理之中,需要有几点注意的事情:
- 不可宽泛地抛出异常Exception,这使得代码可读性降低,对后期的维护埋下隐患。
- 异常的try catch要具有针对性,不可一次包含很多代码,原因有:
- try catch会影响到JVM的运行效率,(影响JVM的代码优化)
- JVM会对异常发生进行堆栈快照,消耗资源
- 不可以直接抛出异常又不处理,这样的程序是存疑的。
另外,要提早检查问题,并抛出。
下面是子问题,关于异常:
NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
首先一个是Error,一个是Exception。 NoClassDefFoundError在虚拟机加载类的时候发现,通常是那些在编译期存在的类,而运行时却不见的类,打包时遗漏或jar损坏。 ClassNotFoundException,当程序中Class.forName()时候找不到类,会抛出此错误。另外,当一个类已经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。通过控制动态类加载过程,可以避免上述情况发生。
Vector、ArrayList、LinkedList等数据结构有何区别?
三者均是集合类List的实现,都可以利用迭代器来进行遍历,实现基本的集合操作。 Vector是早期Java的集合类,是由同步动态数组实现的,故有并发性能,当数组容量不够,会创建新数组并复制过去。 ArrayList是动态数组实现,缺乏了并发性能,但根据需要调整容量。 LinkedList,顾名思义是链表,其由双向链表实现,无需调整容量,也非线程安全。
Java容器类有哪些,有关集合的一些问题
大致上的分类有这些(不全面):

迭代器是什么,有什么特点?
Iterator 允许用户在迭代所有Collection对象中移除元素,但是当其迭代集合时,如果出现了当前被遍历的元素被修改了,则会抛出ConcurrentModificationException异常。
Iterator 可以遍历Set List集合,只能单向遍历。
ListIterator可以双向遍历,其从Iterator继承。
关于HashTable和HashMap
HashTable是线程安全的哈希表实现,而HashMap则是单线程使用的,需要多线程环境的情况,也建议使用ConcurrentHashMap,因为HashTable已经不建议使用了(作为保留类)。
关于HashMap和TreeMap
当用到有序遍历的情况,使用TreeMap,而增删改、定位时,用HashMap。
HashMap的实现原理与注意细节
HashMap内部维护数组来记录Node节点。
当发现冲突时,会采用尾插入形成链表。而在Java8以前,是采用头部替换的方法的,而修改了的原因有:
当出现扩容的时候,会出现循环引用。
某个数组位置上的:A->B->C->null,会可能变成A->B->A产生无限循环。
hash算法是:当key不为null,(h = key.hashCode()) ^ (h >>> 16),采用高位下调的方法,减少冲突:

HashMap的默认初始化长度是16,在使用是2的幂的数字的时候,Length-1的值是所有二进制位全为1,index的结果等同于HashCode后几位的值,实现均匀分布。
HashMap在冲突增多的时候,会使用红黑树。
另外,其中有一些重要变量:
// 默认初始容量必须是2的幂,这里是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量(必须是2的幂且小于2的30次方,如果在构造函数中传入过大的容量参数将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子,HashMap通过负载因子与桶的数量计算得到所能容纳的最大元素数量
// 计算公式为threshold = capacity * loadFactor
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转化为链表的阈值,扩容时才可能发生
static final int UNTREEIFY_THRESHOLD = 6;
// 进行树化的最小容量,防止在调整容量和形态时发生冲突
static final int MIN_TREEIFY_CAPACITY = 64;
谈谈final、finally、 finalize有什么不同?
首先在,这三者表面很像,其实根本不同。 final: final关键字被用来做:
- 修饰变量(引用)
- 类中成员变量,被final修饰后,必须在原地赋值,否则编译期报错,且不可修改。
- 基本变量被修饰,则其值不可变。
- 引用变量被修饰,则不可改变其所引用,但可以改变对象本身的内容。
- 修饰方法 不可被子类所覆写,但是可以被子类所继承。
- 修饰类 不可以被继承
finally: 用做try catch中,通常被用来作为资源的回收,保证重点代码被执行。
finalize: 是Object类中的一个方法,被用来指定对象被回收前要执行的操作。 但是已经不推荐使用,JDK9中被指定废弃。 使用新的Cleaner来完成清理操作。
强引用、软引用、弱引用、幻象引用有什么区别?具体使用场景是什么?
强引用即最普通的Java对象,只有当其显式为null才会在GC中被回收,回收的时机取决垃圾回收的策略。 软引用比强应用要弱一些,当一个对象持有软引用,当虚拟机发生内存不足的情况下,对象就会被回收。 弱引用要比软引用更为弱,当垃圾回收时,不论内存足够不足够,都会将扫描到的软引用对象给回收。 幻想引用就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
理解 Java 的字符串,String、StringBuffer、StringBuilder 有什么区别?
String是Java中默认的字符串类,后两者用于拼接字符串,提高效率。
String中所有域都是被final修饰的,故String对象具有Immutable(不可变性),故拼接字符串、裁剪等操作均会创建新的String实例,这给性能带来影响。 StringBuffer的字符串拼接是有线程安全性保障的,而StringBuilder则没有,两者均继承了AbstractStringBuilder类,不过StringBuffer中的方法,均有synchronized修饰。
在Java中,String有许多优化。 如被@HotSpotIntrinsicCandidate修饰的许多String中的方法,会采用硬编码native的方式来执行,这是Intrinsic机制。
在代码编译中,Java9以后的JDK会将显式的字面字符串拼接,由Java8中用StringBuilder逐步构造改为利用一段调用直接实现:StringConcatFactory.makeConcatWithConstants,这样是Java本身智能优化的体现。
在8u20后,intern显示排重机制被JVM自身优化,能使一样的字符串对象指向同一份实例。 JDK9以后,String类支持Compact Strings,压缩char数组成byte数组,并添加coder表示编码,以减少字符编码的冗余。
int和Integer有什么区别?
这是Java中的包装类,Java提供自动装箱拆箱的语法题。
在编译后的字节码中,实际上是调用Integer的intValue和valueOf静态工厂方法来完成自动装箱拆箱。 Integer类中有一个IntegerCache类,用于在valueOf方法中快速构造对象,其缓存值从-128~127,而上限值可以在JVM参数中指定。
Integer类中所存的值,是final修饰的,故不可变。
另外原始类型和包装类型是不可以在泛型中共用的,而包装类型在数组中,是有其性能上的不便的。在达到一定数量级的情况下,不建议使用包装类,其对象头也占用相当的内存。
Java 有几种文件拷贝方式?哪一种最高效?
- 利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作。
- java.nio 类库提供的 transferTo 或 transferFrom 方法
- 利用nio中的 Files.copy()方法
利用输入输出流的拷贝,其实现包含了上下文的切换: 程序在操作系统的用户空间内,操作系统和磁盘则是在内核空间内,java程序在读写时,会都需要经过操作系统内核来访问磁盘。
而利用nio的transferTo 或 transferFrom方法,利用了Linux 和 Unix 的零拷贝技术,文件的拷贝,并不需要用户空间参与,而直接在内核和磁盘中完成。另外,其不仅仅可以用在文件读写上,也可以用在Socket传输上,提高效率。
另外,利用NIO Buffer可以提高IO效率:
public static void bufferDo(String path) throws IOException {
RandomAccessFile aFile = new RandomAccessFile(path, "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while (buf.hasRemaining()) {
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
}
Buffer有一些属性:
-
capacity,它反映这个 Buffer 在内存中的占用多大,也就是数组的长度。
-
position,要操作的数据起始位置。
-
limit,相当于操作的限额。
-
mark,记录上一次 postion 的位置,默认是 0,算是一个便利性的考虑,往往不是必须的。
下图表示的是两个参量的含义:

当读取时,limit是数据的上限位置。当写入时,limit是capacity值(或者容量以下的可写限度),意味最多可写至此。
Buffer的概念用法:
读取所有数据后,需要清除缓冲区,以使其可以再次写入。可以通过两种方式执行此操作:通过调用clear或通过调用compact。
clear方法清除整个缓冲区。
compact方法仅清已经读取的数据。 任何未读的数据都将移至缓冲区的开头,并且现在将在未读的数据之后将数据写入缓冲区。
此外,还有Direct Buffer 和MappedByteBuffer的用法
还有DirectBuffer 和MappedByteBuffer的用法
DirectBuffer:堆外缓存
MappedByteBuffer:用FileChannel.map创建的一种缓存,将文件的部分直接映射到内存中,Java程序操作时省去了将数据从内核空间向用户空间传输的损耗。
关于接口、抽象类
这是面向对象的设计方面。
接口是对行为的抽象,Java中的接口,定义所有的方法默认是public abstract的,用于被实现。接口可以用来多继承。另外,接口中不能定义普通成员变量,其中若要定义field,都是隐含 public static final 的。
另外,Java 8 以后,接口也是可以有方法实现的,default方法用于抽取默认行为。
抽象类是对共有的行为、变量的抽象,抽象类不可被实例化,但是可以被继承,故抽象类不可用被修饰为final。而抽象类中可以没有抽象方法。
设计模式
设计模式可以分为创建型模式、结构型模式和行为型模式:
创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。
synchronized 和 ReentrantLock 等Java并发中的锁机制
synchronized是java5以前就有的语言内建的线程上锁机制,提供互斥性的语义,当一个线程获取锁,其他线程都要等待并阻塞,一直到锁被释放。
ReentrantLock是Java5开始支持的,再入锁通过lock unlock方法来实现加解锁,另外还提供了公平性的支持。
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,在Java6以前,moniter实现完全依靠操作系统内部的互斥锁,而在此之后,JVM修改了,增加了几种:偏斜锁、轻量级锁和重量级锁。
JVM 优化 synchronized 运行的机制,就有了所谓的锁升级降级。
在JVM内部,当一个对象没有线程竞争的时候,此时是采用偏斜锁,即利用CAS来实现修改对象头上的线程ID来简单完成上锁。而当有其他的线程来竞争对象时,JVM 会撤销偏斜锁,并切换到轻量级锁实现。轻量级锁,利用CAS操作mark word来尝试获取锁,成功了就会进入轻量级锁实现,否则升级为重量级锁实现。
ReadWriteLock(读写锁)用来解决synchronized或者ReentrantLock全占用对象的局限性,因为并发场景下,读操作不会有什么干扰,故读写分别控制,提高性能。
当写锁被占用时,读锁无法取得,这一语义就是读写锁。
另外,还有自旋锁的概念:
首先,在JVM中,线程上下文切换挂起是耗费CPU指令等资源的,而自旋锁的含义是:当一个线程竞争临界资源失败的时候,不那么快地将它实施挂起(重量的操作系统行为),而是在JVM层面上,对其先进行一小段时间的while空循环,循环结束后,JVM会去尝试让线程获取临界资源,如果这次没有获取成功,就真的将这个线程挂起。
有几点注意的是:自旋锁所做的while循环,会占用CPU资源,所以,如果一个线程尝试获取的临界资源需要被很长时间占用,那么自旋锁所做的循环就是无用功的;另外,在单核CPU上,自旋锁是无用的,当自旋锁尝试获取锁不成功会一直尝试,这会一直占用CPU,其他线程无法运行。
多线程
并行与并发:多个处理器同时处理多个任务是并行,单个处理器处理多个任务是并发。
守护线程:
守护线程运行在后台的特殊线程,其独立于进程,等待某些发生的事件并处理,比如JVM中的垃圾回收线程就是守护线程。
Thread daemonThread = new Thread();
//设置线程为守护线程
daemonThread.setDaemon(true);
//必须在启动前设置才行
daemonThread.start();
在Java中,JVM 发现只有守护线程存在时,将结束进程。
线程的创建:
通过继承Thread的run方法,实现Runnable接口或者Callable都可以创建线程。
Callable的方式可以返回执行结果。
线程的几个状态:
- 新建(NEW) 尚未启动
- 就绪(RUNNABLE) 在运行中或者等待被分配计算资源以运行
- 阻塞(BLOCKED) 线程等待获取临界资源时(等待锁)
- 等待(WAITING) 一个等待其他线程通知的等待状态,Thread.join() 也会令线程进入等待状态。
- 计时等待(TIMED_WAIT) 等待指定时间被唤醒
- 终止(TERMINATED)执行完成,终结态
sleep() 与 wait():
sleep方法来自Thread,不释放锁,时间到会自动恢复。
wait方法来自Object,会释放锁,需要等notify或者notifyAll来唤醒。
notify() 与 notifyAll():
唤醒全部线程和单个线程的区别,notify唤醒一个线程(由JVM决定),notifyAll则唤醒全部线程然后一起竞争锁。
start()能否重复调用?
不可,不同于run方法,一旦start方法被第二次调用,将抛出IllegalThreadStateException异常。
线程池的使用与原理
创建线程池
首先看看线程池的构造方法:
public ThreadPoolExecutor(
//线程池核心线程数最大值
int corePoolSize,
//线程池中的线程数量最大值
int maximumPoolSize,
//线程池中非核心线程空闲的存活时间大小
long keepAliveTime,
//上个参数的时间单位
TimeUnit unit,
//存放待运行任务的阻塞队列
BlockingQueue<Runnable> workQueue,
//创建线程的工厂,用来初始化线程(指定名称等)
ThreadFactory threadFactory,
//线程池饱和后的拒绝策略,在后面会讲解
RejectedExecutionHandler handler
)
- newSingleThreadExecutor()
线程数量始终为1,而队列是无界的,能保证所有的任务按照顺序执行
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
- newCachedThreadPool()
线程会被缓存(可以重用),当没有缓存的线程可以用的时候,就需要创建新的工作线程。线程运行完任务后,有60s的时间等待新的任务(等待到了就去执行,相当于这个线程被复用了),如果60s过了,则线程会被销毁。长时间闲置时,此线程池不会销毁什么资源。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- newFixedThreadPool()
线程池里,最多有nThreads数量的线程在运行,所一,当一个任务被提交,但是运行中的线程数量已经达到nThreads,就会被添加到队列中,直到那些运行中的线程有完成任务并退出了,这个任务才会被创建线程,补足nThreads。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
此线程池,没有非空闲时间,即keepAliveTime为0,所以线程不会被复用,而是直接销毁。线程池能保证同时运行的线程数量是固定的(Fixed)。
- newScheduledThreadPool()
顾名思义,是一个可以调度(间隔性或者周期性)的线程池。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
以上的构造函数里,添加了DelayedWorkQueue作为队列,故可以进行调度。
线程池的执行,结合其中的工作队列来实现功能
下面将给出线程池基本的工作流程(一个最基础的自定义的线程池的能力):
当我们手动创建一个线程池的时候,例如 用以下参数构造:
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
着重看一下一个参数:60L,这个时间参数。构造方法里的解释是:
@param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
意思是当线程数大于内核数时,这是多余的空闲线程将在终止之前等待新任务的最长时间。
所以当这个参数被设置为0的时候,线程完成任务后,就不会是所谓的空闲,而是被直接销毁。
当这个参数被设置为60的时候,一个线程完成任务后,将进入60秒的空闲状态,这个时间以内,如果线程池指派这个线程任务,它会从空闲状态进入工作状态,如此往复。但是如果,60秒的时间内都没有新的任务被指派给这个空闲线程,那么它就会被真正的销毁。
这个机制可以帮助线程复用。
其次,有一个参数对线程池的能力有至关重要的作用:
BlockingQueue<Runnable> workQueue //在执行任务之前用于保留任务的队列
通过这个参数,注入一个实行不同策略的阻塞队列。
什么是阻塞队列?
在队列为空时,获取元素的线程会等待队列变为非空。而当队列满时,存储元素的线程会等待队列可用。
联想一下生产者-消费者机制就能理解阻塞队列的作用。
下面来看看,有哪些阻塞队列,它们都被用在了哪些线程池上,实现了什么样的期望:
-
LinkedBlockingQueue 链表阻塞队列,按FIFO排序,
newFixedThreadPool线程池使用了这个队列:
在构造的时候,创建了一个无界的LinkedBlockingQueue,故线程池可以放入无限数量的任务,等着一个一个被消费。
newSingleThreadExecutor线程池也用了这个队列:
构造时创建了一个无界的LinkedBlockingQueue,线程池可以一直放入任务,没有限制,只不过能运行的线程只有一个而已。
-
SynchronousQueue 无元素阻塞队列,每个插入必须等到另一个线程移除。用“配对”来形容会更合适,当一个线程进行插入的时候,必须有一个移除线程和它配对,拿走它插入的数据,否则就会一直阻塞。
newCachedThreadPool线程池使用了这个队列:
这个线程池核心的线程数量为0,而最大的线程数量为无穷,而有60s的空闲延迟时间,所以当一个任务进入时,必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程(非核心线程)。感觉有点像是没有线程池一样,但是有一个好处就是,线程不像是“野生”创建的那样执行完后就销毁,而是会有被保留的机会,被重用。而SynchronousQueue的作用只不过是在于让所有请求都得到回复。
-
DelayQueue 延迟队列:支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素。在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
newScheduledThreadPool线程池使用了这个队列:
容易理解,这个队列可以满足延迟的需求。
线程池的执行:
线程池的执行,不论是否为通过上面所述的方法创建的,还是手动使用构造方法创建的,都遵循以下的运行规则:
- 因为线程池区分核心线程,故一个任务被提交后,如果核心数量还没满,就创建核心线程并执行任务。
- 核心线程已满,如果线程池中的队列(不管是哪种队列)没有满,那就让任务进入队列。
- 如果队列也满了,那线程池会检查“是否当前整个线程池的线程数量已经超过了线程池的最大值”(线程池中的线程数量最大值 int maximumPoolSize),如果没有超过了话,线程池就立即创建一个非核心的线程并执行任务。
- 如果很不幸,以上几个条件都依次没有满足,那只能采取拒绝策略(RejectPolicy)了。
线程池的四种拒绝策略
- AbortPolicy(直接抛出一个异常)
- DiscardPolicy(丢弃任务,不做处理)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy (直接由调用线程池的线程来显式执行了)
状态转换
线程池的状态:
-
RUNNING 运行中,平常处理。
-
SHUTDOWN 不接受新的任务 但继续处理队列的任务。
当队列为空,并且线程池中执行的任务也为空,就会进入TIDYING状态。
-
STOP 不接受新任务 不处理队列任务 甚至切断正在执行任务的线程。
当线程池里的任务为空,就会进入TIDYING状态。
-
TIDYING 任务为空了,会执行terminated方法。
-
TERMINATED 执行了terminated方法后,会进入这个状态,线程池终结。
