参考答案:封装、继承、多态。
封装的好处是:隐藏实现细节,提供公共的访问方式;提高代码的复用性;提高安全性。
继承的好处:使用父类的属性方法,更大程度的实现代码复用。把共性的全部抽到父类。
多态:多态是同一个行为具有多个不同表现形式或形态的能力。维护和拓展。
静态代码块 > 构造代码块 > 构造方法 > 局部代码块
final可以修饰类,该类不能被继承。final可以修饰方法,该方法不能被重写。(覆盖,复写)final可以修饰基本类型变量,该变量不能被重新赋值。因为这个变量其实常量。final可以修饰引用类型变量,引用类型的地址值不能发生改变,但是,该对象的堆内存的值是可以改变的。finally是一个关键字。finally在处理异常机制的提供finally方法来执行一切操作,不管有没有异常捕捉或者抛出,finally都会执行操作,通常用于释放资源,关闭资源的操作。注:不要在fianlly中使用return、不要在finally中抛出异常。
finalize()是Object中的protected方法,该方法是由垃圾收集器在确定这个对象没有被引用时调用,子类可以覆盖方法来实现对象从内存中清除出去之前做必要的清理工作。
注:使用Integer时,对于-128到127之间的数,会进行缓存,Integer i1 = 127时,会将127进行缓存,下次再写Integer i2 = 127时,就会直接从缓存中取,不会新new一个Integer。
String字符串内容是不能被改变的。而StringBuffer和StringBuilder字符串对象可以直接进行修改。StringBuffer类中的方法都添加了synchronized关键字,保证了线程安全,但效率低。StringBuilder是在单线程环境下使用的,数据不安全,但它的效率也比StringBuffer要高。clone方法是浅拷贝。序列化可以实现深拷贝
抽象类是对一组具有相同属性和方法共性功能的一种抽象,接口中定义的是该继承体系的扩展功能。
Java中的集合类可以分为两大类:一类是实现 Collection 接口;另一类是实现 Map 接口。Collection有两个重要的子接口 List和 Set。
List是Collection的子接口。特点:有序(存储顺序和取出顺序一致)、可重复。ArrayList:数组、LinkedList:链表。List特有遍历:size()和get() ListIterator 可以通过下标获取、增加、修改、删除下标对应的元素值。
Set是Collection的子接口。特点:无序、唯一。Set常见子类有HashSet:哈希表 hashCode() 和 equals()、LinkedHashSet:哈希表和链表(存储和取出是一致)、TreeSet:排序、唯一。红黑树,Comparable Comparator 。
Map子类:HashMap:哈希表;哈希表的作用是用来保证键的唯一性的、LinkedHashMap:哈希表和链表、Hashtable:线程安全。TreeMap:红黑树。
三个类都实现了List接口,都是有序集合,数据是允许重复的;
Vector基于数组实现存储的,使用synchronized同步方法,线程安全。ArrayList基于数组实现存储的,线程不安全,性能高。插入修改删除慢,查询快。LinkedList基于双向链表实现存储的,线程不安全,插入修改删除快,查询慢。HashTable:是早期哈希表实现,本身是同步的,不支持 null 键和值,由于同步带来的开销,导致性能低,基本不使用。HashMap:基于哈希表。相比较HashTable不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选。高并发使用 ConcurrentHashMapLinkedHashMap:哈希表和链表。TreeMap:基于红黑树的顺序访问的Map,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。在传统集合框架内部,除了Hashtable等同步容器,还提供了同步包装容器(如Collections.synchronizedMap),利用了 this 作为互斥的mutex,但在高并发情况下,性能比较低下。
更加普遍的选择是利用并发包提供的线程安全容器类:ConcurrentHashMap、CopyOnWriteArrayList、SynchronousQueue等等。
ConcurrentHashMap通过分段锁的形式来实现高效的并发操作。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放哪一个分段中,然后对分段加锁,所以当多线程put的时候,只要不是放在一个分段这种,就实现了真正的并行插入。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
泛型其实指的就是参数化类型,可以在编译期明确参数类型,防止运行期转换异常的问题。
Java泛型的类型参数的实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型。集合类容纳的对象都是Object类的实例,一旦把一个对象置入集合类中,它的类信息将丢失。
也就是说,集合类中容纳的都是指向Object类对象的引用。这样的设计是为了使集合类具有通用性,因为Object类是所有类的祖先,所以可以在这些集合中存放任何类而不受限制。
Java中类的泛型会在编译期间经过泛型擦除的过程,当泛型类型被擦除后,退化成为默认类型也就是Object类型,然后Object类型是一个类,如int、double等这种基本类型并不是引用类型,其父类不是Object(它们本身也没有父类),因此泛型不能为基本数据类型。
Exception和Error都继承自Throwable。
Error:JVM本身的错误。错误不能被程序员通过代码处理,绝大部分的Error或导致程序崩溃,如OutOfMemoryError、StackOverflowError。
Exception:代表程序运行时发生的各种不期望发生的事件。程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
运行时异常 RuntimeException:NullPointerException、IndexOutOfBoundsException、ClassCastException。编译时异常:Exception 中除 RuntimeException及其子类之外的异常:IOException、ClassNotFoundException。按流的数据单位分为:字节流 和 字符流。
同步和异步描述的是消息通信的机制。
callback、状态通知的方式获得结果。阻塞和非阻塞描述的是程序在等待调用结果时的状态。
挂起,当服务方得到response后就唤醒挂起线程,并将response返回给调用方。不会被挂起,通过select通知调用者,并且直到得到response后才返回。阻塞和非阻塞最大的区别就是看调用方线程是否会被挂起。
同步阻塞IO:针对Sender而言,请求发送出去以后,一直等到Receiver有结果了才返回,这是同步。在Sender获取结果的期间一直被block住了,也就是在此期间Sender不能处理其它事情,这是阻塞。
同步非阻塞IO:针对Sender而言,请求发送出去以后,立刻返回,然后再不停的发送请求,直到Receiver处理好结果后,最后一次发请求给Receiver才获得response。Sender一直在主动轮询,每一个请求都是同步的,整个过程也是同步的。在Sender等待Receiver的response期间一直是可以处理其它事情的(比如:可以发送请求询问结果),这是非阻塞。
异步阻塞IO:针对Sender而言,请求发送出去以后,立刻返回,然后再等待Receiver的callback,这整个过程是异步。在Sender等待Receiver的callback期间一直被block住了,也就是在此期间Sender不能处理其它事情,这是阻塞。
异步非阻塞IO:针对Sender而言,请求发送出去以后,立刻返回,然后再等待Receiver的callback,最后再次请求获取response,这整个过程是异步。在Sender等待Receiver的callback期间一直是可以处理其它事情的,这是非阻塞。
同步阻塞IO(BIO):同步堵塞。一个连接一个线程。客户端有连接请求时服务器端就需要启动一个线程并处理,可以通过线程池机制改善。
同步非阻塞IO(NIO):同步(多路复用)非阻塞。一个IO请求一个线程。即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。
提供了Channel(通道)、Selector(NIO实现多路复用的基础,检测到注册在Selector上的多个Channel中是否有Channel处于就绪状态,进而实现了单线程对多Channel的高效管理。)、Buffer(高效的数据容器)
异步非阻塞IO(AIO):异步非阻塞。一个有效请求一个线程。客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。程序调用AIO的accept方法并传入Completionhandler,该方法是非阻塞方法。等数据准备完成后回调Completionhandler处理响应操作。
注:select,pselect,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
IO是面向流的,NIO是面向缓冲区的。
IO的各种流是阻塞的。NIO的IO多路复用模型 通过系统调用select,单个线程不断的轮询select系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。
第一种,使用java.io包下的库,使用FileInputStream读取,再使用FileOutputStream写出。需要调用函数 read()、write() ,由于内核指令的调用会使得当前用户线程切换到内核态,然后内核线程负责把相应的文件数据读取到内核的IO缓冲区,再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。
第二种,利用java.nio包下的库,使用transferTo或transfFrom方法实现。常说的零拷贝实现。利用现代操作系统底层机制,避免不必要拷贝和上下文切换,因此在性能上表现比较好。
public static void copyFileByChannel(File source, File dest) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel();){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
第三种,Java 标准类库本身已经提供了 Files.copy 的实现。
corePoolSize:核心线程数。
maximumPoolSize:最大线程数。
keepAliveTime:空闲线程存活时间。
TimeUnit:时间单位。
BlockingQueue:线程池任务队列。
ThreadFactory:创建线程的工厂。
RejectedExecutionHandler:拒绝策略。
如果忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏。
private static修饰ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值。
防止弱引用ThreadLocal被回收后,其对应的value没有被回收,从而导致内存泄露。
JDK动态代理:利用反射机制。CGLIB动态代理:利用asm开源包,将代理对象类的class文件加载进来,通过修改其字节码动态生成子类来处理。
JDK的动态代理基于Java反射机制实现;cglib的动态代理基于ASM机制实现,将代理对象类的class文件加载进来,通过修改其字节码动态生成子类来处理。
引用计数法:通过判断对象的引用数量判断对象是否可以被回收 无法检查循环引用可达性分析法:通过判断对象的引用链是否可达来决定对象是否可以被回收标记-清除算法:第一个阶段先标记,看看哪些对象可以是垃圾,第二个阶段是清除,把垃圾对象所占用的空间释放。优点:速度快,缺点:容易产生内存碎片。标记-整理算法:第一个阶段先标记,看看哪些对象可以是垃圾,第二个阶段是整理,清理过程中,将可用的对象的内存前移,使内存变得更紧凑,防止碎片产生。优点:避免了内存碎片,缺点:速度慢。复制算法:新生代的内存区域又分成Eden、From、To三个区域,垃圾回收时,将From区标记不是垃圾的对象复制到To区,From区对象全部回收,再将From区和To区交换。优点:不会产生内存碎片,缺点:占用双倍的内存空间。分代收集算法:年轻代:存活率低,使用复制算法。老年代:存活率高,区域大,标记清除(内存碎片不太多就清除)+标记压缩混合实现。这里可能会有一个小误区,
释放是不是意味着把整个内存每个字节进行一个清零操作呢,注意:不会,它只需要把对象
所占用内存的起始、结束的地址给记录下来,放在一个空闲的地址列表里就可以了,下次再分配新对象的时候就到空闲
地址列表中去找看有没有一块足够的空间容纳新对象,如果有,那就进行一个内存分配,并不会把可以作为垃圾的对象
占用的内存做一个清零的处理。
参考文章:
垃圾回收之垃圾回收算法
堆(新生区、老年区)。
分代收集算法:
当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,后父类自己无法完成这个加载请求,子加载器才会尝试自己去加载。 Bootstrap Classloader > Extention ClassLoader > Application ClassLoader > Custom ClassLoader
通过这种层次模型,可以避免类的重复加载,也可以避免核心类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全。
继承ClassLoader,重写findClass,并生成字节数组,然后获取的字节数组通过defineClass()生成了Class对象。打破双亲委托机制。
对象包括:
markWord 64位占8字节,class pointer开启压缩占4字节,关闭压缩占8字节。也就是对象头默认占12字节。
Object o= new Object() 空对象,实例数据为0,而虚拟机默认占用字节要为8的整数倍,则对齐填充4,大小为12 + 4 =16字节。
原子性,可见性,有序性。
自旋锁的实现基础是CAS算法机制。CAS自旋锁属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。缺点:CPU开销较大。不能保证代码块的原子性。 AtomicInteger
volatile主要是保证内存的可见性,即变量在寄存器中的内存是不确定的,需要从主存中读取。synchronized主要是解决多个线程访问资源的同步性。volatile 作用于变量,synchronized作用于代码块或者方法。volatile 仅可以保证数据的可见性,不能保证数据的原子性。synchronized可以保证数据的可见性和原子性。volatile 不会造成线程的阻塞,synchronized会造成线程的阻塞。synchronized是Java语法的一个关键字,加锁的过程是在JVM底层进行。Lock是一个类,是JDK应用层面的。synchronized在加锁和解锁操作上都是自动完成的,Lock锁需要我们手动加锁和解锁。synchronized能修饰方法和代码块,Lock锁只能锁住代码块。Lock锁有丰富的API,可根据不同的场景,在使用上更加灵活无锁->偏向锁->轻量级锁->重量级锁
通过分段锁的形式来实现高效的并发操作。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放哪一个分段中,然后对分段加锁,所以当多线程put的时候,只要不是放在一个分段这种,就实现了真正的并行插入。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
📌代理模式:代理类与委托类之间通常会存在关联关系,我们在访问实际对象时,是通过代理对象来访问的。代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。代理类提供附加功能。
📌装饰模式:动态地给一个对象增加一些额外的职责。就扩张功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案。维护一个指向抽象构建对象的引用,通过该引用可以调用装饰之前构建对象的方法,并通过其子类扩展该方法,已达到装饰的目的。
装饰器模式强调的是增强自身,增强过后还是你,只不过能力变强了。代理模式中最终都是通过代理类对象来访问。
代理模式应用有Spring AOP和Spring 事务等;装饰模式应用IO类,Spring过滤器也会用到装饰模式,可以链式调用。
📌模板方法模式:模板模式一般只针对一套算法,注重对同一个算法的不同细节进行抽象提供不同的实现。