【020 标记接口】
标记接口没有任何成员和方法,表明的是能够具备某种功能,通常是在编译器进行类型校验。
① Cloneable接口,代表类可以被拷贝,否则调用对象的clone方法会抛出异常;另外clone方法是Object类的方法,底层实现通过本地方法,效率更高,因此重写clone方法最好先调用super.clone方法;
② Serializable接口,代表类可以被序列化,否则使用ObjectInputStream的readObject和ObjectOutputStream的writeObject方法会抛出异常;
③ RandomAccess接口,代表类对象是否支持快速访问,例如ArrayList和LinkedList,前者是连续存储数据的,可以声明为RandomAccess接口,则编译器对于foreach的处理,前者会使用for+下标遍历,而后者只能使用迭代器。
【021 序列化与反序列化】
① 序列化的对象建议自定义SerializableID成员的值,否则编译器会自动分配,其作用是用来比较序列化前的类和序列化后的类是否是同一个版本;
② 序列化的过程,可以调用ObjectOutputStream的writeObject方法,static和transient类型的数据不会被写入;写入时,首先会写入类类型,然后写入父类类型,接着依次写入各个成员类型和赋值;
③ 反序列化的类型,可以调用ObjectIutputStream的readObject方法,首先利用反射的方法,根据类型名称生成对象,对于成员对象,也会根据成员类型,生成成员对象;
④ 父类类型不支持可序列化,则必须提供无参构造函数,成员必须支持可序列化。
【022 Java异常处理机制】
Java异常可以分为Error和Exception。Error是虚拟机错误,如OutOfMemoryError;Exception可以分为其他异常和RunTimeException,RunTimeException可以通过虚拟机JVM捕获处理,如空指针异常、数组越界异常等,而其他异常必须主动捕获,否则编译不过,如IOException。
【023 Java动态代理】
动态代理可以分为Java代理和CGLib代理,Java代理是以接口为基础的,被代理的类需要继承某个接口,然后InvocationHandler的invoke方法实际上是对接口方法的封装;而CGLib代理是以抽象类为基础的.
【024 Java注解】
Java注解可以理解为一种特殊的接口类型@Interface,是描述类/方法/成员的一种元数据。注解可以拥有成员并为其指定默认值,通过注解形式标注的类/方法/成员,可以通过反射,获取注解的成员并做出适当的操作。
【025 String原理】
String的内部核心成员是private final char[],编码格式是UNICODE,意味着String的内容不可修改,但是String的引用可以替换的。另外String是final类型的,说明String不可以被继承。此外,StringBuilder和StringBuffer也是final类型的,但是StringBuilder是非线程安全的,而StringBuffer通过Synchronized实现了线程安全。
【026 String的基本操作】
① String是唯一实现了运算符+重载的类,其底层实现是通过StringBuilder的appender方法完成的,最后再通过toString转回字符串;
② 常量类型或者final类型的字符串进行+运算,编译器会进行优化,直接记录最终结果。
【027 集合的分类】
集合可以分为Collection和Map,Collection又可以分为List和Set。List是有序可重复的,通过继承AbstractCollection和AbstractList抽象类,实现了三个分支:ArrayList、Vector和LinkedList,其中Vector是栈Stack的父类,而LinkedList是队列Queue的实现类。Set的实现是基于Map的Key值,因此是无顺序不可重复的,通过继承AbstractCollection和AbstractSet实现了TreeSet、HashSet和LinkedHashSet。
【028 Iterator接口】
Iterator是集合的内部实现类,帮助遍历集合容器。其提供了一种Fast-Fail的线程遍历安全机制,使用modeCount记录当前的版本号,如果在遍历过程中对集合进行过增删改操作,会导致版本号变化,和遍历的版本号不相同,从而抛出异常。Iterator内部还有两个成员cursor游标和lastRet,分别指向下一个要访问的位置和最后访问的位置,通过这两个成员实现方法hasNext、next和remove等。
【029 ArrayList的内部结构】
ArrayList是非线程安全的,允许空值,其内部核心是private transient Object[] elementData成员,默认大小为10,每次扩容为1.5倍的原有size+1。transient的作用是对象在序列化的过程,不会直接把elementData直接序列化,而是提供了readObject和writeObject方法,序列化的是elementData已使用的空间大小和保存的数据,避免序列化无意义的元素。同时ArrayList声明了Cloneable接口,并重写clone方法实现深拷贝。
【030 Vector的内部结构】
Vector与ArrayList最大的区别是通过Synchronized实现了线程安全,其内部核心是不带有属性transient的elementData成员,可以直接序列化,默认大小为10,每次扩容为2倍。
【031 LinkedList的内部结构】
LinkedList本质上是一个非线程安全的带头指针的双向链表,核心成员是transient Entry<E> header。Transient同样表明其不可直接序列化,而是直接保存Entry中的数据内容,节省序列化空间。双向链表在查找指定位置的元素时,可以根据index与size的关系,决定是从头开始查找还是从尾倒序查找。
【032 HashMap的内部结构】
HashMap是非线程安全的,无序不可重复,Key和Value都允许是Null。核心成员transient Entry[] table,初始化大小为2的指数,数组的每个元素都是一个单向链表的首元素,没有头指针。Transient序列化是直接保存key和value的值。元素存储时首先根据key值计算hashcode,对hsahcode二次hash后(尽量避免冲突),找到table中对应的元素,然后在对应的链表中依次通过equals比较key是否相同,key为NULL默认保存在table[0]中。HashMap的遍历只能通过迭代器,依次对数组中的每个链表元素进行访问,数组扩容和插入时,都是从链表元素单向访问,因此是无序的。
【033 LinkedHashMap的内部结构】
LinkedHashMap是在HashMap的基础上,提供了一种快速访问机制,也就是在HashMap原有的数据结构上,添加一个带头指针的双向链表,每次插入元素到数值的同时,也会插入一份到这个双向链表,在对元素进行遍历时直接使用。可以指定元素在双向链表的遍历顺序,默认按照插入顺序,也可以按照访问顺序,在每次访问元素后,把元素移动到链表最前端。
【034 HashTable的内部结构】
HashTable与HashMap最大的区别是通过Synchronized实现了线程安全,由于支持多线程并发,因此Key和Value都不允许是Null。假设允许,通过get方法返回NULL的两种可能性:元素不存在或者value为Null无法判断,而单线程通过consistKey可以判断。
【035 ConcurrentHashMap的内部结构】
ConcurrentHashMap在HashMap的基础上,增加了Segments数组,每个元素都是可重入互斥锁ReentrantLock的子类,拥有HashEntry数组的成员,与HashMap的核心成员一致。可以理解为,Segments的每一个元素都是一个加锁HashMap,不同的HashMap之间支持并发访问。不过Segment的HashMap中链表的next是final的,这会导致的删除元素时,该元素链表中排在元素之前的元素会被从头复制一份并调用插入函数倒序插入链表。
【036 TreeMap的内部结构】
TreeMap的底层是红黑树,是在二叉搜索树的基础上,对有序数列的优化。通常使用的链表或者数组,要么插入耗时,要么增删耗时,而平衡二叉树可以均衡这一现象。
【037 IO流分类】
IO流可以分为字节流/字符流,以读取的内容是原始字节还是封装后的编码字符来区分;或者节点流和处理流,节点流直接从指定位置获取内容,而处理流作为封装,对字节流添加辅助功能。
【038 FileInputStream和FileOutputStream的工作原理】
字节流,节点流。FileInputStream的方法read,通过参数设置读取一个字节或者读取一个字节数组,但核心都是调用native方法的read0,一个一个读取字节,效率较低。FileOutputStream的方法write,每次输出一个字节,核心也是调用native的write0,可以通过append属性指定是追加到文件末尾还是重头写入。多个流可以同时访问一个文件,各自维护一套对文件的索引。此外,文件流支持NIO操作,通过getChannel来获取通道,但还是会阻塞在read和write方法上。
【039 BufferedInputStream和BufferedOutputStream的工作原理】
字节流,处理流。可以封装在FileInputStream和FileOutputStream上,通过提供一个volatile的字节数组来支持缓存,默认大小是8M。与FileInputStream和FileOutputStream的read和write带字节参数方法相比,好处是主动维护了一个可控制的字节数组,通过pos和count来记录数组的读写,写时一定要通过flush主动输出,否则会等到缓冲区满时再一次性输出。其read和write方法是synchronized的,保证一次输入输出的字节流是安全的。
【040 DataInputStream和DataOutputStream的工作原理】
字节流,处理流。提供了一次读写多个字节并按编码转为int、char等各种类型的方法,例如readUTF,但是在调用过时方法readLine时,由于不需要指定编码,会把2个byte直接转化为char,然后把char数组转化为String,出现乱码;同时对于System.In输入流,还会因为没有读到\r\n阻塞。
【041 ObjectInputStream和ObjectOutputStream的工作原理】
字节流,处理流。主要是实现对象序列化使用,readObject和writeObject。
【042 FileReader和FileWriter的工作原理】
字符流,节点流。底层实现主要是靠父类InputStreamReader和OutputStreamWriter来完成,通过StreamEncoder和StreamDecoder对FileInputStream和FileOutputStream读取的字节进行编解码。
【043 RandomAccessFile的工作原理】
RandomAccessFile支持双向读写,同时实现接口DataInput和DataOutput接口,底层的read和write都是通过native方法实现,按照byte读写,可以通过seek(pos)支持快速访问。
【044 NIO的工作原理】
NIO由Buffer、Channel和Selector三部分组成。Channel是一个双向通道,从Buffer中读取或写入数据。Buffer是一块连续的内存空间,子类可以指定数据类型,通过capacity、position和limit三个变量控制。Capacity指Buffer可以读写多少个类型数据,position表示下一个读或写的位置,从写模式切换到读模式时,会置为0;limit读模式指向能读取多少个数据,写模式等于capacity。Selector是NIO的核心,通过把channel注册到Selector上,通过select方法轮询访问各个channel,直到channel上有响应事件。
【045 BIO、NIO和AIO的区别】
BIO是同步阻塞的,NIO是同步非阻塞的,而AIO是异步非阻塞的。以Tomcat为例,BIO会使用ServerSocket的accept方法,一直阻塞到请求到来后,分配给线程池的Processor,然后重新accept阻塞;NIO使用ServerSocketChannel,并注册到selector上,通过selector的select轮询获取请求;AIO采用主动通知的方式,获取到的请求会主动推送给Proceesor。
【046 Java的线程状态】
线程状态可以分为就绪、运行、等待、结束。当调用Thread类的start方法,可以启动一个线程进入就绪状态;就绪状态的线程通过系统调度,可以进入运行状态;运行状态的线程调用stop或destroy方法,直接进入结束状态;而调用sleep或者wait等方法,进入等待状态,等待事件通知后重新回到就绪队列。
【047 线程中断的方式及原理】
线程中断的stop方法,会立即释放线程持有的锁进入结束状态,但不会释放所占有的资源;而destroy更加粗暴,直接结束进程,锁和资源都不会被释放;suspend是把线程挂起,直到resume后重新回到就绪状态,但是锁和资源也都不会被释放。因此,以上三种方法都不推荐。Interrupt中断本质上是设置线程的中断标志位,如果线程在等待状态,则抛出中断异常后,进入就绪状态,而在运行状态的线程继续运行,可以通过检查isInterrupt,来主动退出。另外在实际的线程控制中,也是采用标志位来轮询决定是否退出运行状态的。
【048 线程休眠的方式及原理】
Thread的休眠方法有sleep、yield和join。Sleep是使线程自身休眠指定的ms,然后重新回到就绪状态,不会释放锁;yield相当于sleep(0),直接进入休眠状态,也不会释放锁;t.join是挂起线程t,等待调用该方法的线程执行结束或一段时间后,重新进入就绪状态,join函数最大的特点在于它是一个synchronized的方法,内部实现通过wait函数来实现,因此会释放占用的锁。wait是Object的方法,需要结合notify或notifyAll来使用,必须包含在synchronized或者lock来使用,会释放占用的锁。
【049 生产者-消费者案例】
思路:生产者和消费者共享产品的内存队列。多个生产者存放产品时要竞争队列锁,确保资源有序进入队列,然后去判断是否有空间,如果有,存放完产品,通知消费者并退出,如果没有,进入wait等待状态,直到消费者发送notifyAll唤醒;多个消费者消费产品时同样也需要竞争队列锁,确保产品被依次取出。获得锁的消费者判断是否有可用资源,如果有,则取出产品,通知生产者并退出,如果没有,进入wait等待状态,直到生产者发送notifyAll唤醒。只有一把队列锁,可以保证同一时间,最多只有一个生产者或消费者可以访问队列。
【050 启动单个线程的方法】
第一种是继承Thread类或者实现Runable接口,重写run方法,然后调用Thread的start方法,通过底层的native实现多线程。Thread类本身就实现了Runable接口,并且内部有一个Runable的成员,通过判断Runable成员是否为NULL,决定是调用Thread本身还是Runable对象的run方法。这种方法比较常用,缺点是缺少返回值。
第二种是实现Callable接口的call方法,并通过FutureTask的run方法和get方法实现线程的运行以及得到内部成员result的返回值。FutureTask是同时实现了Future和Runable接口。get是一个阻塞方法。
【051 Synchronized和Volatile关键字】
Synchronized在编译阶段会在包含的代码块前后分别加上monitorenter和monitorexit监视器,本质上也是通过锁实现的,在Java对象的内存分配中会通过对象头来标记锁状态,之前提到的偏向锁、自旋锁等都是对synchronized底层实现的优化。Monitor锁对象会拥有_WaitSet和_EntryList,用来保存ObjectWaiter对象列表,ObjectWaiter对象就是封装的等待线程对象。
Volatile是通过JVM的内存模型来实现的,拥有该关键字的对象每次修改后都会把对象的副本写会内存中,而其余线程读取该对象都必须从内存而不是ThreadLocal中加载。
【052 Lock和Synchronized】
虽然Synchronized本质是可重入锁,但是运用上不直接使用锁的lock和unlock方法方便。另外,锁的lock方法可以指定获取锁的时间,如果获取不了,不必阻塞。
【053 Lock的常用类】
ReentrantLock是最常用的可重入锁,核心成员private final Sync sync是一个队列同步器,由一个等待获取锁的队列和一个锁重用计算器count组成。ReentrantLock的lock方法最后会调用sync.lock方法实现。可以指定锁为公平锁还是非公平锁。公平锁是指排在等待队列最前面的线程会获取锁,不会出现饿汉问题,但是效率不是最高的。而非公平锁有JVM根据最优分配,决定下一个获取锁的线程,例如刚刚释放锁的线程马上重新获取,可以避免上下问切换,但会出现饿汉线程。
ReentrantReadAndWriterLock,读写锁,同时拥有一把读锁和一把写锁,读锁之间不互斥,是共享锁,而写锁是排他锁。
【054 线程池的工作原理】
线程池的几个重要变量:分为核心线程数、最大线程数、阻塞队列。但需要一个线程时,首先会判断线程池的核心线程数是否全部创建,没有则直接创建线程放入核心线程池直接使用;如果线程池的核心线程数已全部创建,则先判断核心线程是否有空闲,有空闲则调用空闲线程;如果没有空闲线程,则判断阻塞队列是否有空闲可以放入线程,有则放入,没有则判断非核心线程是否有可以使用的,有则使用,没有则交给系统的RejectExceptHandler处理;同时,完成调用的线程会去阻塞队列中提取等待的线程任务。
【054扩展线程池的调用方法】
线程池的顶层抽象类是Executor,子类ExecutorService,常用的是ThreadPoolExecutor。可以通过构造函数创建常用的4类线程池,然后通过execute方法或者submit方法来实现具体的调用。execute方法可以接收一个runable对象,而submit方法接收一个callable对象并返回Future返回值。
【055 线程池的阻塞队列】
① ArrayBlockingQueue,底层是Array,因此是有界FIFO的,只有一把ReentrantLock,通过putIndex和getIndex维护索引;
② LinkedBlockingQueue,底层是单链表结构,但是有head和last两个节点分别标识头尾,实现尾插入和头取出,各有一把ReentrantLock;
③SynchronizedBlockingQueue,底层没有任何阻塞队列,每个操作必须等待前一个操作执行后才能调用;
④PriorityBlockingQueue,底层是Array,但是会通过comparator排序后,按照优先级放入。
【056 常见线程池】
① FixedThreadPoolExecutor,核心线程数=最大线程数=n,阻塞队列LinkedBlockingQueue;
② CachedThreadPoolExecutor,核心线程数=0,最大线程数=INTEGER_MAX,非核心等待时间60s,阻塞队列SynchronizedBlockingQueue,适用于异步的短任务;
③SingleThreadPoolExecutor,核心线程数=最大线程数=1,阻塞队列LinkedBlockingQueue;
④ScheduledThreadPoolExecutor,阻塞队列是封装了PriorityBlockingQueue的DelayQueue。