之前的一篇总结已经写到了十万字,阅读起来太不方便了,所以按照类别拆分成多个短篇分享给大家。

文章目录

  • 2. 语言类
    • 2.1. 进程和线程的区别
    • 2.2. 协程与线程
      • 2.2.1. 协程的优势
    • 2.3. 线程安全的定义、线程的状态
    • 2.4. 多线程的实现方式(Runnable和Callable的区别)、start/run方法的区别
    • 2.5. 子线程异常捕捉
    • 2.6. wait()/notify()/sleep()/yield()/join()几个方法的意义
    • 2.7. notifyAll实现原理及等待池和锁池的概念
    • 2.8. 线程池的创建方式,7大参数、阻塞队列、拒绝策略、大小
    • 2.9. 乐观锁CAS、悲观锁 synchronized和ReentrantLock、实现原理以及区别
    • 2.10. Lock与synchronized的区别
    • 2.11. ABA问题
      • 2.11.1. 使用AtomicStampedReference避免ABA问题
    • 2.12. 锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等
    • 2.13. volatile和synchronized的区别及JAVA内存模型
    • 2.14. ThreadLocal线程本地存储原理
    • 2.15. 指令重排序
    • 2.16. final关键字
      • 2.16.1. final实现原理
    • 2.17. 反射
      • 2.17.1. 常用的反射流程
      • 2.17.2. 反射调用为什么开销大?
    • 2.18. 内存泄漏问题
    • 2.19. AQS同步队列器原理,CLH队列
    • 2.20. AQS组件:ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore原理掌握
      • 2.20.1. CountDownLatch
      • 2.20.2. CyclicBarrier
      • 2.20.3. Semaphore
    • 2.21. JUC原子类
    • 2.22. JDK
      • 2.22.1. String的实现原理
      • 2.22.2. HashMap实现
      • 2.22.3. LinkedHashMap
    • 2.23. Java集合
      • 2.23.1. HashMap为什么线程不安全?
      • 2.23.2. equals()和hashcode()
      • 2.23.3. hashmap遍历时用map.remove方法为什么会报错?
    • 2.24. 集合框架的多线程实现类
      • 2.24.1. ConcurrentHashMap
      • 2.24.2. ConcurrentLinkedDeque
      • 2.24.3. ConcurrentLinkedQueue
      • 2.24.4. ConcurrentSkipListMap
      • 2.24.5. ConcurrentSkipSet
      • 2.24.6. CopyOnWriteArrayList
      • 2.24.7. CopyOnWriteArraySet
    • 2.25. 谈谈依赖注入 IOC
    • 2.26. 谈谈面向切面编程 AOP
    • 2.27. Java注解
    • 2.28. Java中面向对象的特性
      • 2.28.1. 封装
      • 2.28.2. 继承
      • 2.28.3. 多态
    • 2.29. 动态代理和静态代理
    • 2.30. Spring的两种动态代理:Jdk和Cglib 的区别和实现
      • 2.30.1. 为什么动态代理要实现接口
    • 2.31. JAVA版本特性
      • 2.31.1. Java7
      • 2.31.2. Java8
      • 2.31.3. Java9
    • 2.32. java如何实现泛型?
    • 2.33. JVM
      • 2.33.1. 内存模型
        • 2.33.1.1. 程序计数器(线程私有)
        • 2.33.1.2. Java栈(虚拟机栈)
        • 2.33.1.3. 本地方法栈
        • 2.33.1.4. 堆
        • 2.33.1.5. 方法区
      • 2.33.2. 垃圾判断算法
        • 2.33.2.1. 引用计数
        • 2.33.2.2. 可达性分析
      • 2.33.3. 垃圾回收算法
        • 2.33.3.1. 标记-清除
        • 2.33.3.2. 标记-复制
        • 2.33.3.3. 标记-整理
      • 2.33.4. 垃圾回收器
        • 2.33.4.1. Serial 收集器
        • 2.33.4.2. ParNew收集器其实就是Serial收集器的多线程版本。
        • 2.33.4.3. Parallel Scavenge 收集器
        • 2.33.4.4. Serial Old 收集器
        • 2.33.4.5. Parallel Old 收集器
        • 2.33.4.6. CMS一种以获取最短回收停顿时间为目标的收集器。
        • 2.33.4.7. G1收集器
      • 2.33.5. JVM参数调优
        • 2.33.5.1. HBase jvm参数调整
    • 2.34. 类加载:双亲委派
      • 2.34.1. 类加载过程
    • 2.35. 设计模式
      • 2.35.1. 单例模式
        • 2.35.1.1. 编写单例模式
        • 2.35.1.2. 双重校验锁
      • 2.35.2. 工厂模式
      • 2.35.3. 策略模式
      • 2.35.4. 门面模式
      • 2.35.5. 建造模式
      • 2.35.6. 适配器模式
      • 2.35.7. 装饰模式
      • 2.35.8. 代理模式(委托模式)
      • 2.35.9. 外观模式
      • 2.35.10. 观察者模式
      • 2.35.11. 命令模式(Command)
      • 2.35.12. 访问者模式(Visitor)

2. 语言类

2.1. 进程和线程的区别

进程是系统进行资源分配和调度的一个独立单位

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源

2.2. 协程与线程

协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。

2.2.1. 协程的优势

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;

不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

2.3. 线程安全的定义、线程的状态

参考了 https://blog.csdn.net/weixin_46416295/article/details/109262143

线程同步
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的

线程同步
Java中提供了同步机制 (synchronized) 来解决。有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中, 有几种状态呢?在API中 java.lang.Thread.State这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

2.4. 多线程的实现方式(Runnable和Callable的区别)、start/run方法的区别

java实现多线程有四种方式:继承Thread类、实现Runnable()接口、实现Callable接口、通过线程池启动多线程(实现runable)

Callable通过FutureTask可以有返回值,而且可以抛出异常。

方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程

2.5. 子线程异常捕捉

正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线程的异常,我们需要使用ExecutorService:

  1. 首先在创建线程工厂的时候ExecutorService exec = Executors.newCachedThreadPool(new HandleThreadFactory());将我们自定义的工厂类传入。
  2. 在工厂类中为线程设置t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandle());异常处理。
  3. 重写Thread.UncaughtExceptionHandler中的uncaughtException(Thread t, Throwable e)方法。

如果不需要每个线程单独设置异常处理的话,可以使用Thread的静态方法:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());

2.6. wait()/notify()/sleep()/yield()/join()几个方法的意义

wait方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程

只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。

sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。

yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。

join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。

2.7. notifyAll实现原理及等待池和锁池的概念

obj.notifyAll()方法唤醒所有阻塞在 obj 对象上的沉睡线程,然后被唤醒的众多线程竞争 obj 对象的 monitor 占有权,最终得到的那个线程会继续执行下去,但其他线程继续阻塞。

每个对象都有一个唯一与之对应的内部锁(Monitor)。这里就涉及到锁池和等待池的概念。当多个线程想要获取某个对象的锁,而该对象已经被其他线程拥有,这些线程就会进入该对象的锁池等待锁的释放。当一个线程主动调用wait方法,就会进入等待池不会和其他线程竞争锁的持有。所以这时候就需要调用notify或者notifyAll方法,让该线程进入锁池重新参与锁的争取。notify只会随机选取一个线程,notifyAll则会将所有等待池中的线程加入到锁池。

2.8. 线程池的创建方式,7大参数、阻塞队列、拒绝策略、大小

线程池创建的三种方式

singleThreadExecutor 创建单个线程
newFixedThreadpool 创建固定线程个数的线程
newCacheThreadpool 可伸缩

7大参数是使用原生线程池创建线程使用的参数。

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, //核心线程数3, //最大线程数3, //超时时间TimeUnit.SECONDS,//时间单位new LinkedBlockingDeque<>(3),//阻塞队列Executors.defaultThreadFactory(),//一个线程工厂,一般使用默认的就OKnew ThreadPoolExecutor.AbortPolicy()//一种拒绝策略);

核心线程指的是:在线程中默认创建的线程
最大线程:见名知意指的就是最大的线程数
阻塞队列:用于核心线程池满时,让需要"服务"的线程等待的地方
拒绝策略:指的是最大线程池满,阻塞队列也满的时候,新来的线程如何处置。

4种拒绝策略
new ThreadPoolExecuor.AbortPolicy :不处理这个线程,并抛出异常
new ThreadPoolExecuor.CallerRunsPolicy:这个多的线程不处理,而是交给创建这个线程的去处理
new ThreadPoolExecuor.DiscardPolicy:不处理这个线程,也不抛出异常
new ThreadPoolExecuor.DiscardOldestPolicy:尝试和最老的线程进行竞争,也不会抛出异常

2.9. 乐观锁CAS、悲观锁 synchronized和ReentrantLock、实现原理以及区别

参考:https://blog.csdn.net/ly199108171231/article/details/88098614这篇文章介绍的非常好,下面是对它内容的一些概述。

悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized。然而在真实环境中,大部分时候都不会产生冲突。悲观锁会造成很大的浪费。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试)。这种锁不会阻塞其他的线程,也不涉及上下文切换,性能开销小。代表实现是 CAS

Java 中的并发锁大致分为隐式锁和显式锁两种。隐式锁就是我们最常使用的 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于AQS(AbstractQueuedSynchronizer) 实现的

CAS是 compare and swap 的简写,即比较并交换。它是指一种操作机制。在 Unsafe 类中,调用代码如下:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);

复制代码它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。操作时,先从内存位置读取到值,然后和预期值A比较。如果相等,则将此内存位置的值改为新值 B,返回 true。如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。

这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。CAS 就是通过这种方式(缓存锁)实现比较和交换操作的原子性的。

ReentrantLock内部有两个内部类,分别是 FairSync 和 NoFairSync,对应公平锁和非公平锁。他们都继承自 Sync。Sync 又继承自AQS。
请求锁时有三种可能:
如果没有线程持有锁,则请求成功,当前线程直接获取到锁。
如果当前线程已经持有锁,则使用 CAS 将 state 值加1,表示自己再次申请了锁,释放锁时减1。这就是可重入性的实现。
如果由其他线程持有锁,那么将自己添加进等待队列。

理解 ReentrantLock 和 AQS 之后,再来理解读写锁就很简单了。读写锁有一个读锁和一个写锁,分别对应读操作和锁操作。锁的特性如下:
只有一个线程可以获取到写锁。在获取写锁时,只有没有任何线程持有任何锁才能获取成功;
如果有线程正持有写锁,其他任何线程都获取不到任何锁;
没有线程持有写锁时,可以有多个线程获取到读锁。

2.10. Lock与synchronized的区别

  1. Lock是一个接口,属于JDK层面的实现;而synchronized属于Java语言的特性,其实现有JVM来控制(代码执行完毕,出现异常,wait时JVM会主动释放锁)。
  2. synchronized在发生异常时,会自动释放掉锁,故不会发生死锁现(此时的死锁一般是代码逻辑引起的);而Lock必须在finally中主动unlock锁,否则就会出现死锁。
  3. Lock能够响应中断,让等待状态的线程停止等待;而synchronized不行。
  4. 通过Lock可以知道线程是否成功获得了锁,而synchronized不行。
  5. Lock提高了多线程下对读操作的效率。

2.11. ABA问题

我们假设有两个线程同时使用CAS操作对同一个值为A的变量进行修改。
线程1:A -> B -> A
线程2:A -> C
由于线程2获取A之后,线程1对A进行了修改,所以理论上线程2对于C的修改是应该失败的,但由于线程2在写入C的时候的预期值是A所以这个操作最后成功了。ABA问题实际上就是说虽然CAS能够对预期值进行估计,但是却无法判断相同的预期值是否发生了修改。因此可以使用版本号来解决这个问题。

2.11.1. 使用AtomicStampedReference避免ABA问题

对于需要自己进行CAS处理的地方,我们可以使用“AtomicStampedReference”来进行数据的处理。它既支持泛型,同时还可以避免传统CAS中ABA的问题,使数据更加安全。

private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);

AtomicStampedReference主要是使用CAS机制更新新的值reference和时间戳stamp。而最终调用的底层是一个本地的方法对数据进行的修改。

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

2.12. 锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等

synchronized 是重量级锁,由于消耗太大,虚拟机对其做了一些优化。

自旋锁与自适应自旋
在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

锁消除
虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

锁粗化
当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。

轻量级锁
对绝大部分的锁来说,在整个同步周期内都不存在竞争。如果没有竞争,轻量级锁可以使用 CAS 操作避免使用互斥量的开销。

偏向锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。

2.13. volatile和synchronized的区别及JAVA内存模型

简单概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。

如果要了解volatile就必须先了解java的内存模型:

(1)每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
(2)对该变量操作完成后,在某个时间再把变量刷新回主内存。

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2)它会强制将对缓存的修改操作立即写入主存

3)如果是写操作,它会导致其他CPU中对应的缓存行无效

volatile和synchronized区别

  1. volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
  2. volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
  3. volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
      《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。
  4. volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
  5. 当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。(其他线程也有可能同步修改,无法保证其他线程可以知道自己栈里的情况)
  6. 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域

2.14. ThreadLocal线程本地存储原理

ThreadLocal是线程本地存储的一种实现方案。它并不是一个Thread,我们也可以称之为线程局部变量,在多线程并发访问时,ThreadLocal类为每个使用该变量的线程都创建一个变量值的副本,每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本发生冲突。从线程的角度来看,就感觉像是每个线程都完全拥有该变量一样。

ThreadLocal的使用场合主要用来解决多线程情况下对数据的读取因线程并发而产生数据不一致的问题。ThreadLocal为每个线程中并发访问的数据提供一个本地副本,然后通过对这个本地副本的访问来执行具体的业务逻辑操作,这样就可以大大减少线程并发控制的复杂度;然而这样做也需要付出一定的代价,需要耗费一部分内存资源,但是相比于线程同步所带来的性能消耗还是要好上那么一点点。

链接:https://www.jianshu.com/p/ae653c30b4d9

例如:

    //线程本地存储变量private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};

这样我们在任何时候操作THREAD_LOCAL_NUM.set(n);都只操作了当前线程副本里的值,而不会影响其他线程。

和synchronized区别

  1. 采用synchronized进行同步控制,但是效率略低,使得并发变同步(串行)
  2. 采用ThreadLocal线程本地存储,为每个使用该变量的线程都存储一个本地变量副本(线程互不相干)

2.15. 指令重排序

在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。

在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》

Java提供了两个关键字volatilesynchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现。在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

2.16. final关键字

  1. final成员变量表示常量,只能被赋值一次,赋值后值不再改变(final要求地址值不能改变;当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化
  2. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义,不能被重写;第二个原因是效率,final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。(注:类的private方法会隐式地被指定为final方法)
  3. 当用final修饰一个类时,表明这个类不能被继承。

2.16.1. final实现原理

对于final域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    原因:编译器会在final域的写之后,插入一个StoreStore屏障
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
    编译器会在读final域操作的前面插入一个LoadLoad屏障

2.17. 反射

反射的常用类和函数:Java反射机制的实现要借助于4个类:Class,Constructor,Field,Method;其中class代表的是类对象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象,通过这四个对象我们可以粗略的看到一个类的各个组成部分。

2.17.1. 常用的反射流程

  1. Class.forName 找到类;
  2. newInstance 开辟内存空间,构造实例;
  3. getDeclaredMethod 根据方法签名搜索方法体;
  4. method.invoke 执行方法调用。

2.17.2. 反射调用为什么开销大?

Class.forName 会尝试先去方法区(1.8以后就是 Metaspace) 中寻找有没有对应的类,如果没有则在 classpath 中找到对应的class文件, 通过类加载器将其加载到内存里。注意这个方法涉及本地方法调用,这里本地方法调用的切换、类的搜索以及类的加载都存在开销;newInstance ,开辟内存空间,实例化已经找到的 Class;getDeclaredMethod/getMethod 遍历 Class 的方法,以方法签名匹配出所需要的 Method 实例,也是比较重的开销所在;虽然 java.lang.Class 内部维护了一套名为 reflectionData 的数据结构,其本身是 SoftReference 的。Class 会将匹配成功的 method 缓存到这里,以供下次访问命中,这样一来会减轻 method 遍历的开销,但是会增加额外的堆空间消耗(以空间置换时间)

  1. Method#invoke 方法会对参数做封装和解封操作

我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。

而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。

因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。

  1. 需要检查方法可见性

通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)

  1. 需要校验参数

反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);

  1. 反射方法难以内联

Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。参见 http://www.iteye.com/blog/rednax…

  1. JIT 无法优化

另外,JDK 开发者们也给我们提供了一套优化思路,既然反射调用有性能问题,不好优化,那能不能将反射调用变成普通调用呢?inflation 正是这条思路的解决方案。通过类字节码的生成并加载,实现了 inflation 。好处就是这不仅减少了本地方法和普通方法来回切换的开销,变成普通方法调用后还能享受到 JIT 编译器的优化福利,其中方法内联是最重要的优化点。编译器采集足够多的运行时数据后,根据统计模型得知某个反射方法成为热点,此时该反射方法体有几率会被内联进调用者的方法体中。

2.18. 内存泄漏问题

常见的内存泄漏及解决方法

1、单例造成的内存泄漏

2、非静态内部类创建静态实例造成的内存泄漏

因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。
解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例

3、资源未关闭造成的内存泄漏

4、集合容器中的内存泄露

2.19. AQS同步队列器原理,CLH队列

AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。

AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。 AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。 AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

2.20. AQS组件:ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore原理掌握

参考:https://www.cnblogs.com/jackion5/p/12932343.html

2.20.1. CountDownLatch

CountDownLatch可以理解为是同步计数器,作用是允许一个或多个线程等待其他线程执行完成之后才继续执行,比如打dota、LoL或者王者荣耀时,创建了一个五人房,只有当五个玩家都准备了之后,游戏才能正式开始,否则游戏主线程会一直等待着直到玩家全部准备。在玩家没准备之前,游戏主线程会一直处于等待状态。如果把CountDownLatch比做此场景都话,相当于开始定义了匹配游戏需要5个线程,只有当5个线程都准备完成了之后,主线程才会开始进行匹配操作。

public static void countDownLatchTest() throws Exception{CountDownLatch latch = new CountDownLatch(5);//定义了需要达到条件都线程为5个线程new Thread(new Runnable() {@Overridepublic void run() {for (int i=0; i<12; i++){try {Thread.sleep(1000L);} catch (InterruptedException e) {e.printStackTrace();}new Thread(new Runnable() {@Overridepublic void run() {long count = latch.getCount();latch.countDown();/**相当于准备游戏成功*/if(count > 0) {System.out.println("线程" + Thread.currentThread().getName() + "组队准备,还需等待" + latch.getCount() + "人准备");}else {System.out.println("线程" + Thread.currentThread().getName() + "组队准备,房间已满不可加入");}}}).start();}}}).start();new Thread(new Runnable() {@Overridepublic void run() {try {System.out.println("游戏房间等待玩家加入...");/**一直等待到规定数量到线程都完全准备之后才会继续往下执行*/latch.await();System.out.println("游戏房间已锁定...");} catch (InterruptedException e) {e.printStackTrace();}}}).start();System.out.println("等待玩家准备中...");/**一直等待到规定数量到线程都完全准备之后才会继续往下执行*/latch.await();System.out.println("游戏匹配中...");}

本案例中有两个线程都调用了latch.await()方法,则这两个线程都会被阻塞,直到条件达成。当5个线程调用countDown方法之后,达到了计数器的要求,则后续再执行countDown方法的效果就无效了,因为CountDownLatch仅一次有效。

CountDownLatch的实现完整逻辑如下:

  1. 初始化CountDownLatch实际就是设置了AQS的state为计数的值
  2. 调用CountDownLatch的countDown方法时实际就是调用AQS的释放同步状态的方法,每调用一次就自减一次state值
  3. 调用await方法实际就调用AQS的共享式获取同步状态的方法acquireSharedInterruptibly(1),这个方法的实现逻辑就调用子类Sync的tryAcquireShared方法,只有当子类Sync的tryAcquireShared方法返回大于0的值时才算获取同步状态成功,否则就会一直在死循环中不断重试,直到tryAcquireShared方法返回大于等于0的值,而Sync的tryAcquireShared方法只有当AQS中的state值为0时才会返回1,否则都返回-1,也就相当于只有当AQS的state值为0时,await方法才会执行成功,否则就会一直处于死循环中不断重试。

2.20.2. CyclicBarrier

CyclicBarrier可以理解为一个循环同步屏障,定义一个同步屏障之后,当一组线程都全部达到同步屏障之前都会被阻塞,直到最后一个线程达到了同步屏障之后才会被打开,其他线程才可继续执行。

还是以dota、LoL和王者荣耀为例,当第一个玩家准备了之后,还需要等待其他4个玩家都准备,游戏才可继续,否则准备的玩家会被一直处于等待状态,只有当最后一个玩家准备了之后,游戏才会继续执行。

public static void CyclicBarrierTest() throws Exception {CyclicBarrier barrier = new CyclicBarrier(5);//定义需要达到同步屏障的线程数量for (int i=0;i<12;i++){Thread.sleep(1000L);new Thread(new Runnable() {@Overridepublic void run() {try {System.out.println("线程"+Thread.currentThread().getName()+"组队准备,当前" + (barrier.getNumberWaiting()+1) + "人已准备");barrier.await();/**线程进入等待,直到最后一个线程达到同步屏障*/System.out.println("线程:"+Thread.currentThread().getName()+"开始组队游戏");} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}}).start();}}

本案例中定义了达到同步屏障的线程为5个,每当一个线程调用了barrier.await()方法之后表示该线程已达到屏障,此时当前线程会被阻塞,只有当最后一个线程调用了await方法之后,被阻塞的其他线程才会被唤醒继续执行。

另外CyclicBarrier是循环同步屏障,同步屏障打开之后立马会继续计数,等待下一组线程达到同步屏障。而CountDownLatch仅单次有效。

2.20.3. Semaphore

Semaphore字面意思是信号量,实际可以看作是一个限流器,初始化Semaphore时就定义好了最大通行证数量,每次调用时调用方法来消耗,业务执行完毕则释放通行证,如果通行证消耗完,再获取通行证时就需要阻塞线程直到有通行证可以获取。

public static void semaphoreTest() throws InterruptedException {int count = 5;Semaphore semaphore = new Semaphore(count);System.out.println("初始化" + count + "个银行柜台窗口");for (int i=0;i<10;i++){Thread.sleep(1000L);new Thread(new Runnable() {@Overridepublic void run() {try {System.out.println("用户"+Thread.currentThread().getName()+"占用窗口");semaphore.acquire(1);//获取许可证/**用户办理业务需要消耗一定时间*/System.out.println("用户"+Thread.currentThread().getName()+"开始办理业务");Thread.sleep(5000L);semaphore.release();//释放许可证System.out.println("用户"+Thread.currentThread().getName()+"离开窗口");} catch (Exception e) {e.printStackTrace();}}}).start();}}

2.21. JUC原子类

根据修改的数据类型,可以将JUC包中的原子操作类可以分为4类。

基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater

这些类存在的目的是对相应的数据进行原子操作。所谓原子操作,是指操作过程不会被中断,保证数据操作是以原子方式进行的。

原理:CAS+volatile + native方法来保证操作的原子性

2.22. JDK

2.22.1. String的实现原理

在Java语言中,所有类似“ABC”的字面值,都是String类的实例;String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作;Java语言为“+”连接符(字符串连接符)以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。

  1. String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。
  2. String类实现了Serializable、CharSequence、 Comparable接口。
  3. String实例的值是通过字符数组实现字符串存储的。

注意的点:

  1. "+"在字面量两边的时候,编译器会自动优化为一个字符串,除非编译器无法确定或存在final修饰符。另外一种情况则会隐式创建StringBuilder对象,转为append()操作
  2. 每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。常量池是通过一个StringTable类实现的,它是一个Hash表。
  3. 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中。
  4. String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列。StringBuilder是非线程安全的,StringBuffer是线程安全的。

2.22.2. HashMap实现

  1. HashMap 底层数据结构为 Node 类型数组,而 Node 的数据结构为链表。
  2. 负载因子默认为 0.75,当 HashMap 当前已使用容量大于当前大小 * 负载因子时,自动扩容一倍空间
  3. 树型阀值就是当链表长度超过这个值时,将 Node 的数据结构修改为红黑树,以便优化查找时间,默认值为8
  4. put方法:对 key 进行 hash 运算,然后再与当前 map 最后一个下标(长度-1,分布更均匀)进行与运算确定其在数组中的位置,正是因为这个算法,我们可以得知 HashMap 中元素是无序的。
  5. get方法:根据 key 的 hash 运算值获取数组中对应下标的内容。循环链表或红黑树,然后匹配 value 值直至获得对应的值或返回 null
  6. 1.8我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

2.22.3. LinkedHashMap

  1. LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。
  2. HashMap无序;LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。
  3. LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
  4. LinkedHashMap是线程不安全的。

2.23. Java集合

2.23.1. HashMap为什么线程不安全?

在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和reHash(为key重新计算所在位置),而reHash在并发的情况下可能会形成链表环。总结来说就是在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。为什么在并发执行put操作会引起死循环?是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。jdk1.7的情况下,并发扩容时容易形成链表环,此情况在1.8时就好太多太多了。

因为在1.8中当链表长度达到阈值(默认长度为8)时,链表会被改成树形(红黑树)结构。如果删剩节点变成7个并不会退回链表,而是保持不变,删剩6个时就会变回链表,7不变是缓冲,防止频繁变换。

在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

2.23.2. equals()和hashcode()

使得equals成立的时候,hashCode相等,也就是a.equals(b)->a.hashCode() == b.hashCode(),或者说此时,equals是hashCode相等的充分条件,hashCode相等是equals的必要条件(从数学课上我们知道它的逆否命题:hashCode不相等也不会equals),但是它的逆命题,hashCode相等一定equals以及否命题不equals时hashCode不等都不成立。

2.23.3. hashmap遍历时用map.remove方法为什么会报错?

hashmap里维护了一个modCount变量,迭代器里维护了一个expectedModCount变量,一开始两者是一样的。
每次进行hashmap.remove操作的时候就会对modCount+1,此时迭代器里的expectedModCount还是之前的值。
在下一次对迭代器进行next()调用时,判断是否HashMap.this.modCount != this.expectedModCount,如果是则抛出异常。

解决方法:直接调用迭代器的remove方法

基本上java集合类(包括list和map)在遍历时没用迭代器进行删除了都会报ConcurrentModificationException错误,这是一种fast-fail的机制,初衷是为了检测bug。
通俗一点来说,这种机制就是为了防止高并发的情况下,多个线程同时修改map或者list的元素导致的数据不一致,这是只要判断当前modCount != expectedModCount即可以知道有其他线程修改了集合。

2.24. 集合框架的多线程实现类

CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、ConcurrentLinkedDeque

2.24.1. ConcurrentHashMap

其主要接口方法和HashMap是差不多的。但是,ConcurrentHashMap是使用了ReentranLock(可重入锁机制)来保证在多线程环境下是线程安全的。在JDK6、7后使用了分段锁,在JDK8使用Synchronized对Node头节点加锁和volatile对指针修饰来实现

2.24.2. ConcurrentLinkedDeque

线程安全的双端队列,当然也可以当栈使用。由于是linked的,所以大小不受限制的。

2.24.3. ConcurrentLinkedQueue

线程安全的队列。

2.24.4. ConcurrentSkipListMap

基于跳表实现的线程安全的MAP。除了线程安全的特性外,该map还接受比较器进行排序的map,算法复杂度还是log(n)级别的。

2.24.5. ConcurrentSkipSet

基于4实现的set。

2.24.6. CopyOnWriteArrayList

以资源换取并发。通过迭代器快照的方式保证线程并发的访问。

2.24.7. CopyOnWriteArraySet

基于6实现的。

2.25. 谈谈依赖注入 IOC

IOC(控制反转)理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。所以依赖注入 DI和控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦,除此之外还有另一种方法:服务定位器(Service Locator)。IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。

2.26. 谈谈面向切面编程 AOP

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

在spring中@Aspect 注解用来描述一个切面类。@Component注解将该类交给 Spring 来管理,下面列举spring中AOP相关注解

注解 用法 例子
@Pointcut 用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。 @Pointcut(“execution(* com.mutest.controller….(…))”) // 定义一个切面,拦截 com.itcodai.course09.controller 包和子包下的所有方法
@Around @Around可以自由选择增强动作与目标方法的执行顺序,还可以选择可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。
@Before 注解指定的方法在切面切入目标方法之前执行
@After 指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
@AfterReturning 可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理
@AfterThrowing 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行

2.27. Java注解

注解的好处:本来可能需要很多配置文件,需要很多逻辑才能实现的内容,就可以使用一个或者多个注解来替代,这样就使得编程更加简洁,代码更加清晰。

注解这一概念是在java1.5版本提出的,说Java提供了一种原程序中的元素关联任何信息和任何元数据的途径的方法。

常见的作用有以下几种:

  1. 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@see @param @return 等;
  2. 跟踪代码依赖性,实现替代配置文件功能。比较常见的是spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量;
  3. 在编译时进行格式检查。如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;

2.28. Java中面向对象的特性

继承、多态和封装。

2.28.1. 封装

隐藏类内部的实现机制。

2.28.2. 继承

重用父类的代码。

2.28.3. 多态

多态是指一种事物的变现出来的多种特性。Java实现多态有三个必要条件:继承、重写、向上转型(使用父类声明的指针指向子类实例)。

2.29. 动态代理和静态代理

代理是设计模式的一种,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。Java中的代理分为三种角色:

  1. 代理类(ProxySubject)
  2. 委托类(RealSubject)
  3. 接口(Subject)

静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。或者使用Cglib代理动态生成新的子类,所以不需要实现接口。

动态:在程序运行时运用反射机制动态创建而成。由于创建出来的委托类实例继承自Proxy类,由于java不支持多继承所以委托类必须使用接口。

2.30. Spring的两种动态代理:Jdk和Cglib 的区别和实现

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

  1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
  2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
  3. 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

2.30.1. 为什么动态代理要实现接口

深入分析JDK动态代理为什么只能使用接口

生成的代理类继承了Proxy,我们知道java是单继承的,所以JDK动态代理只能代理接口。

2.31. JAVA版本特性

2.31.1. Java7

  1. 创建泛型对象时应用类型推断。eg:
Map<String, Object> map = new Map<>();
  1. switch语句块中允许以字符串作为分支条件。
  2. try-with-resources(一个语句块中捕获多种异常)。

2.31.2. Java8

  1. lambda表达式。允许将功能当作方法参数或代码当作数据。lambda标识还可以更简洁的方式表示只有一个方法的接口(函数式接口)的实例。
  2. 改进的类型推断。JDK8里,类型推断不仅可以用于赋值语句,而且可以根据代码中上下文里的信息推断出更多的信息
  List<String> stringList = new ArrayList<>();stringList.add("A");//error : addAll(java.util.Collection<? extends java.lang.String>)in List cannot be applied to (java.util.List<java.lang.Object>)stringList.addAll(Arrays.asList());

上面这段代码在java7中会报错,但是在java8中可以编译通过。
3. 支持多重注解。之前同一个注解需要放在类似数组的结构中,现在可以直接多个@,只需要对注解标注为@Repeatable
4. stream接口
5. 新增Optional接口,用来防止NullPointerException异常的辅助类型。在java8中不推荐返回null而是返回Optional。
6. 函数式接口(Functional Interface)。java.util.function 它包含了很多类,用来支持 Java的 函数式编程(接受函数过程传递),该包中的函数式接口有:
Predicate<T> 接受一个输入参数,返回一个布尔值结果。
7. Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下:

interface Formula {double calculate(int a);default double sqrt(int a) {return Math.sqrt(a);}
}
  1. ::方法与构造函数引用。Java 8 允许你使用 :: 关键字来传递方法或者构造函数引用,我们也可以引用一个对象的方法代码如下:
converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

接下来看看构造函数是如何使用::关键字来引用的:

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

2.31.3. Java9

  1. 模块化。模块化系统的主要目的如下:

更可靠的配置,通过制定明确的类的依赖关系代替以前那种易错的类路径(class-path)加载机制。
强大的封装,允许一个组件声明它的公有类型(public)中,哪些可以被其他组件访问,哪些不可以。
这些特性将有益于应用的开发者、类库的开发者和java se平台直接的或者间接地实现者。它可以使系统更加健壮,并且可以提高他们的性能。
一个模块是一个被命名的,代码和数据的自描述的集合。它的代码有一系列包含类型的包组成,例如:java的类和接口。它的数据包括资源文件(resources)和一些其他的静态信息。一个或更多个requires项可以被添加到其中,它通过名字声明了这个模块依赖的一些其他模块,在编译期和运行期都依赖的。最后,exports项可以被添加,它可以仅仅使指定包(package)中的公共类型可以被其他的模块使用。

module com.foo.bar {requires org.baz.qux;exports com.foo.bar.alpha;exports com.foo.bar.beta;
}

2.32. java如何实现泛型?

Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类 型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一 个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛 型。

将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型 出现之前的写法,因为泛型类型都变回了原生类型.

2.33. JVM

2.33.1. 内存模型

根据JVM规范,JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。

2.33.1.1. 程序计数器(线程私有)

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

2.33.1.2. Java栈(虚拟机栈)

每个方法被执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址、附加信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。具体栈帧参考Java虚拟机运行时栈帧结构(周志明书上P237页)

2.33.1.3. 本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码

2.33.1.4. 堆

对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。

2.33.1.5. 方法区

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

2.33.2. 垃圾判断算法

一般来讲有两种垃圾判断的方法,分别是引用计数和可达性分析法。JVM主流采用的是可达性分析算法,因为在Java中引用计数法无法处理循环引用的问题。

2.33.2.1. 引用计数

为避免循环引用的问题,c++中会将互相引用的两个对象中,其中一个引用 weak_ptr,因为 weak_ptr 不会使引用计数加1,所以就不会出现这种互相拖着对方的事情了。
又比如说redis,因为其内部不存在深层次的嵌套,因此也就不存在循环引用的隐患。

2.33.2.2. 可达性分析

JVM采用了另一种方法:定义一个名为"GC Roots"的对象作为起始点,这个"GC Roots"可以有多个,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以进行垃圾回收。
可以作为GC Roots的对象

  1. 虚拟机栈中引用的对象(栈帧中的本地变量)
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中引用的对象

2.33.3. 垃圾回收算法

2.33.3.1. 标记-清除

标记完成后就可以对标记为垃圾的对象进行回收了。但是这种策略的缺点很明显,回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。

2.33.3.2. 标记-复制

将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点,可用内存变为一半了
怎样解决呢?办法总是多过问题的。可以将堆不按1:1的比例分离,而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。‘

2.33.3.3. 标记-整理

根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将所有存活下来的对象都向一端移动。

2.33.4. 垃圾回收器

参考:https://www.cnblogs.com/chenpt/p/9803298.html

新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1

2.33.4.1. Serial 收集器

Serial收集器是最基本的、发展历史最悠久的收集器。

特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。

应用场景:适用于Client模式下的虚拟机。

2.33.4.2. ParNew收集器其实就是Serial收集器的多线程版本。

除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。

特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题

应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。

2.33.4.3. Parallel Scavenge 收集器

与吞吐量关系密切,故也称为吞吐量优先收集器。吞吐量=运行用户代码时间/CPU总执行时间。
用于精确吞吐量的两个参数:1.控制最大垃圾收集停顿时间参数 2.直接设置吞吐量大小的参数。

特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。

该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)

2.33.4.4. Serial Old 收集器

Serial Old是Serial收集器的老年代版本。

特点:同样是单线程收集器,采用标记-整理算法。

应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。

Server模式下主要的两大用途:
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。

2.33.4.5. Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本。

特点:多线程,采用标记-整理算法。

应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。

2.33.4.6. CMS一种以获取最短回收停顿时间为目标的收集器。

特点: 针对老年代;基于标记-清除算法实现。并发收集、低停顿。

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。

CMS收集器的运行过程分为下列4步:

  1. 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。

  2. 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。

  3. 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。

  4. 并发清除:对标记的对象进行清除回收。

CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的缺点

对CPU资源非常敏感。
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。

2.33.4.7. G1收集器

一款面向服务端应用的垃圾收集器。

特点如下:

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。

分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

空间整合:使用标记-整理算法。G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。

可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1为什么能建立可预测的停顿时间模型?

因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。

G1与其他收集器的区别:

其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。

G1收集器存在的问题:

Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。

G1收集器是如何解决上述问题的?

采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。

如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤:

  1. 初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)

  2. 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)

  3. 最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)

  4. 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)

2.33.5. JVM参数调优

命令查看:jmap(jmap -heap PID)

2.33.5.1. HBase jvm参数调整

参考:https://www.cnblogs.com/CtripDBA/p/14210220.html

一台region server的内存使用主要分成两部分:

  1. JVM内存即我们通常俗称的堆内内存,这块内存区域的大小分配在HBase的环境脚本中设置,在堆内内存中主要有三块内存区域,20%分配给hbase regionserver rpc请求队列及一些其他操作,80%分配给memstore + blockcache

  2. java direct memory即堆外内存,

其中一部分内存用于HDFS SCR/NIO操作,另一部分用于堆外内存bucket cache,其内存大小的分配同样在hbase的环境变量脚本中实现

BlockCache用于读缓存;MemStore用于写流程,缓存用户写入KeyValue数据;还有部分用于RegionServer正常RPC请求运行所必须的内存;

Hbase服务是基于JVM的,其中对服务可用性最大的挑战是jvm执行full gc操作,此时会导致jvm暂停服务,这个时候,hbase上面所有的读写操作将会被客户端归入队列中排队,一直等到jvm完成gc操作, 服务在遇到full gc操作时会有如下影响

hbase服务长时间暂停会导致客户端操作超时,操作请求处理异常。服务端超时会导致region信息上报异常丢失心跳,会被zk标记为宕机,导致regionserver即便响应恢复之后,也会因为查询zk上自己的状态后自杀,此时hmaster 会将该regionserver上的所有region移动到其他regionserver上。

具体参数:

  1. 新生代用并行回收器、老年代用并发回收器,另外配置了CMSInitiatingOccupancyFraction,当老年代内存使用率超过70%就开始执行CMS GC,减少GC时间,Master任务比较轻,一般设置4g、8g左右,具体按照群集大小评估
export HBASE_MASTER_OPTS=" -Xms8g -Xmx8g -Xmn1g
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70"
  1. RegionServer专有的启动参数,RegionServer大堆,采用G1回收器,G1会把堆内存划分为多个Region,对各个Region进行单独的GC,最大限度避免Full GC及其影响

初始堆及最大堆设置为最大物理内存的2/3,128G/3*2 ≈80G,在某些度写缓存比较小的集群,可以近一步缩小。

InitiatingHeapOccupancyPercent代表了堆占用了多少比例的时候触发MixGC,默认占用率是整个 Java 堆的45%,改参数的设定取决于IHOP > MemstoreSize%+WriteCache%+10~20%,避免过早的MixedGC中,有大量数据进来导致Full GC

G1HeapRegionSize 堆中每个region的大小,取值范围【1M…32M】2^n,目标是根据最小的 Java 堆大小划分出约 2048 个区域,即heap size / G1HeapRegionSize = 2048 regions

ParallelGCThreads设置垃圾收集器并行阶段的线程数量,STW阶段工作的GC线程数,8+(logical processors-8)(5/8)

ConcGCThreads并发垃圾收集器使用的线程数量,非STW期间的GC线程数,可以尝试调大些,能更快的完成GC,避免进入STW阶段,但是这也使应用所占的线程数减少,会对吞吐量有一定影响

G1NewSizePercent新生代占堆的最小比例,增加新生代大小会增加GC次数,但是会减少GC的时间,建议设置5/8对应负载normal/heavy集群

G1HeapWastePercent触发Mixed GC的堆垃圾占比,默认值5
G1MixedGCCountTarget一个周期内触发Mixed GC最大次数,默认值8
这两个参数互为增加到10/16,可以有效的减少1S+ Mixed GC STW times

MaxGCPauseMillis 垃圾回收的最长暂停时间,默认200ms,如果GC时间超长,那么会逐渐减少GC时回收的区域,以此来靠近此阈值,一般来说,按照群集的重要性 50/80/200来设置

verbose:gc在日志中输出GC情况

export HBASE_REGIONSERVER_OPTS="-XX:+UseG1GC
-Xms75g –Xmx75g
-XX:InitiatingHeapOccupancyPercent=83
-XX:G1HeapRegionSize=32M
-XX:ParallelGCThreads=28
-XX:ConcGCThreads=20
-XX:+UnlockExperimentalVMOptions
-XX:G1NewSizePercent=8
-XX:G1HeapWastePercent=10
-XX:MaxGCPauseMillis=80
-XX:G1MixedGCCountTarget=16
-XX:MaxTenuringThreshold=1
-XX:G1OldCSetRegionThresholdPercent=8
-XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:+PerfDisableSharedMem
-XX:-OmitStackTraceInFastThrow
-XX:+PrintFlagsFinal
-verbose:gc
-XX:+PrintGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+PrintAdaptiveSizePolicy
-XX:+PrintGCDetails
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintReferenceGC
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=100M
Xloggc:${HBASE_LOG_DIR}/gc-regionserver$(hostname)-`date +'%Y%m%d%H%M'`.log
-Dcom.sun.management.jmxremote.port=10102
$HBASE_JMX_BASE"

2.34. 类加载:双亲委派

参考:【JVM】浅谈双亲委派和破坏双亲委派

对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。

基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

2.34.1. 类加载过程

举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点

  1. 启动类加载器,由C++实现,没有父类。
  2. 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
  3. 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
  4. 自定义类加载器,父类加载器肯定为AppClassLoader。

在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:类的完整类名必须一致,包括包名。加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。类加载的过程总体上可以分为5部分。

  1. 加载
    在加载阶段,JVM主要完成下面三件事:
    ①、通过一个类的全限定名来获取定义此类的二进制字节流。
    ②、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    ③、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

  2. 验证
    作用是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  3. 准备
    准备阶段正式为类变量分配内存并设置类变量初始值的阶段,这些内存是在方法区中分配的。上面说的是类变量,也就是被 static 修饰的变量,不包括实例变量。实例变量会在对象实例化时随着对象一起分配在堆中。

  4. 解析
    解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。

直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。

  1. 初始化
    初始化阶段是执行类构造器<clinit>() 方法的过程。

2.35. 设计模式

总体来说设计模式分为三大类:

  1. 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  2. 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  3. 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

2.35.1. 单例模式

该模式主要目的是使内存中保持1个对象

2.35.1.1. 编写单例模式

单例模式有5种,分别是懒汉式、饿汉式、双重校验锁、静态内部类和枚举。

2.35.1.2. 双重校验锁

    private volatile static CSVService csvService;/*** 获取单例 双检锁/双重校验锁(DCL,即 double-checked locking)* @return*/public static CSVService getInstance(){if (csvService == null) {synchronized (CSVService.class) {if (csvService == null) {csvService = new CSVService();}}}return csvService;}

2.35.2. 工厂模式

该模式主要功能是统一提供实例对象的引用

public class Factor {public ClassDao getDao(){ClassDao dao = new ClassDaoImpl();return dao;}
}

2.35.3. 策略模式

这个模式是将行为的抽象,即当有几个类有相似的方法,将其中通用的部分都提取出来,从而使扩展更容易

public class Addition extends Operation{@Overridepublic float parameter(float a, float b){return a+b;}
}

2.35.4. 门面模式

客户端可以调用门面角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,门面角色会将所有从客户端发来的请求委派到相应的子系统去。

2.35.5. 建造模式

将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在这样的设计模式中,有以下几个角色:

Builder:为创建一个产品对象的各个部件指定抽象接口。

ConcreteBuilder:实现Builder的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,并提供一个检索产品的接口。

Director:构造一个使用Builder接口的对象,指导构建过程。

Product:表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。

2.35.6. 适配器模式

适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。

2.35.7. 装饰模式

装饰模式是在不必改变原类文件和使用继承的情况下,能对现有对象的内容或功能性稍加修改。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

2.35.8. 代理模式(委托模式)

代理模式 : 为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。目的为了去除核心对象的复杂性

2.35.9. 外观模式

外观模式是通过在必要的逻辑和方法的集合前创建简单的外观接口,隐藏调用对象的复杂性。

2.35.10. 观察者模式

观察者模式也做发布订阅模式(Publish/subscribe),在项目中常使用的模式。定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并被自动更新。

2.35.11. 命令模式(Command)

命令模式是一个高内聚的模式,其定义为:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

2.35.12. 访问者模式(Visitor)

访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。

[面面面]搞定计算机面试常见知识点——Java篇相关推荐

  1. [面面面]搞定计算机面试常见知识点——框架篇

    之前的一篇总结已经写到了十万字,阅读起来太不方便了,所以按照类别拆分成多个短篇分享给大家. 文章目录 4. 框架类 4.1. Spring与Spring Boot的区别与联系 4.2. Kafka.A ...

  2. 搞定剑桥面试数学题番外篇2:使用多线程并发“加强版”

    0. 概览 我们在之前三篇博文中已经介绍了如何用多种语言(ruby.swift.c.x64 汇编和 ARM64 汇编)实现一道"超超超难"的剑桥数学面试题: · 有趣的小实验:四种 ...

  3. php 面试靠快速排序,搞定PHP面试 - 常见排序算法及PHP实现

    常见排序算法及PHP实现全文代码使用PHP7.2语法编写 0. 五种基础排序算法对比 1. 冒泡排序(Bubble Sort) 冒泡排序 是一种交换排序,它的基本思想是:对待排序记录从后往前(逆序)进 ...

  4. 搞定计算机网络面试,看这篇就够了

    目录 一 OSI与TCP/IP各层的结构与功能,都有哪些协议 1 应用层 2 运输层 3 网络层 4 数据链路层 5 物理层 二 TCP三次握手和四次挥手(面试常客) 为什么要三次握手 为什么要传回s ...

  5. 搞定操作系统面试,看这篇就够了(二)

    三.死锁 必要条件 image 互斥:每个资源要么已经分配给了一个进程,要么就是可用的. 占有和等待:已经得到了某个资源的进程可以再请求新的资源. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占 ...

  6. 想搞定大厂面试官?java读取txt文件并存入数组

    一.码场心得 你是个能吃苦的人吗? 从前的能吃苦大多指的体力劳动的苦,但现在的能吃苦已经包括太多维度,包括:读书学习&寂寞的苦.深度思考&脑力的苦.自律习惯&修行的苦.自控能力 ...

  7. 一小时搞定计算机网络面试

    一小时搞定计算机网络面试 一.计算机网络体系结构参考模型: 七层协议的作用: 1.物理层:主要定义物理设备标准,如网线的接口类型.光纤的接口类型.各种传输介 质的传输速率等.它的主要作用是传输比特流( ...

  8. 声音甜美的美女工程师已就位-帮你搞定React面试的疑难杂症 React面试优化教程

    声音甜美的美女工程师已就位-帮你搞定React面试的疑难杂症 React面试优化教程 React早已经是一线大厂前端的必备技术了,那么在前端跳槽过程中能够帮助同学们的加分的就是在面试这个环节了.Rea ...

  9. 《300分钟搞定算法面试》学习笔记

    之所以不在CSDN直接发,怕说广告直接封了,但是分享还是要说明出处的,感觉老师讲的很好 资料请加群 : 891555732 <300分钟搞定算法面试>学习笔记(一) ------ 常用数据 ...

  10. Java面试常见知识点总结

    目录 面试常见知识点 静态代码块 代码块 构造方法之间的顺序 interface和abstract的区别 abstract能不能继承interface 反正可不可以 interface 和abstra ...

最新文章

  1. leetcode 470. 用 Rand7() 实现 Rand10()
  2. Py之ipykernel:Python库之ipykernel简介、安装、使用方法之详细攻略
  3. js时间戳格式化成日期格式
  4. 31.QT中串口操作
  5. 病毒——隐藏鸟 系统文件夹不见了
  6. 兔子问题or斐波那契数列
  7. RN对接京东支付sdk(IOS)
  8. 12个偏微分方程常用的不等式
  9. 难崩日记——从入门到入土的求生之路(二):文件上传中的路径问题
  10. Flutter开发IOS,上架AppStore的全部流程以及常遇到的坑
  11. 2020中国邮政总行信息技术岗校招笔试经历
  12. 第一次使用虚拟机(VMware)
  13. 【copy】也说嵌入式系统架构设计(linux 平台)
  14. [转载]2012 年 4 月,rating排行榜
  15. 汽车租赁系统(Java)
  16. RoboMongo简单安装和操作
  17. 基于AM335X的EDMA 驱动程序开发
  18. 求标准正交基的一种直观解释
  19. 17岁高中生证明27年数学难题,「他的论文值得任何数学家为之自豪」
  20. 基于 HTML+ECharts 实现物流大数据可视化平台(含源码)

热门文章

  1. 利用leaflet做一个飞机航线 可根据方向动态掉头
  2. 【游戏安利】 益智休闲类游戏安利
  3. 安卓神器-kiwi browser
  4. 必应(Bing)的站内搜索 site:<域名> <搜索内容>
  5. DOTA数据集应用于Yolo-v4(-Tiny)系列2——使用Pytorch框架的Yolov4(-Tiny)训练与推测
  6. python转码时出现'illegal multibyte sequen'错误
  7. 解决无法安装Axure RP 9(Windows 7 SP1 更新补丁)
  8. CA搭建实验和安全基础
  9. java项目视频22套
  10. 菜鸟必看IPC空连接的使用(转载)