一、不常用关键字
transient
被修饰的属性变量不被序列化。即序列化时,不会保存对应属性的值。只能修饰变量,不能修饰方法和类。
e.g. 密码等不希望在网络中传输的字段, pwd=123序列化后pwd=null
static
序列化时也不会保存属性的值。
static关键字修饰的成员属性优于非静态成员属性加载到内存中,同时静态也优于对象进入到内存中,被static修饰的成员变量不能被序列化,序列化的都是对象,静态变量不是对象状态的一部分,因此它不参与序列化。所以将静态变量声明为transient变量是没有用处的。因此,反序列化后类中static型变量name的值实际上是当前JVM中对应static变量的值,这个值是JVM中的并不是反序列化得出的。
final
final修饰的class不可再扩展;修饰的方法不可重写;修饰的变量不可修改(修饰集合时,只是集合本身引用不可变,集合增删改操作正常)。直接通过值参与序列化,与transient同时使用时,不能序列化。
transient vs static vs final在序列化方面:
- transient修饰的变量不被序列化;
- 被static关键字修饰的变量不参与序列化,一个静态static变量不管是否被transient修饰,均不能被序列化;
- final变量值参与序列化,final transient同时修饰变量,final不会影响transient:即不会参与序列化。
序列化
计算机之间传递信息的最小单元是字节流,序列化其实就是将一个对象变成所有的计算机都能识别的字节流;反序列化就是将接受到的字节流还原成一个程序能识别的对象。序列化最终的目的是为了对象可以更方便的进行跨平台存储和进行网络传输。
基本上只要是涉及到跨平台存储或者进行网络传输的数据,都需要进行序列化。
比较流行的序列化方式,例如:XML、JSON、Protobuf、Thrift 和 Avro等等。进行序列化技术的选型,主要可以从以下几个方面进行考虑:
是否支持跨平台:尤其是多种语言混合开发的项目,是否支持跨平台直接决定了系统开发难度
序列化的速度:速度快的方式会为你的系统性能提升不少
序列化出来的大小:数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能
Java序列化只需要实现Serializable
接口即可,但注意,如上所述,static、transient修饰的变量不会被序列化。
只要是实现了Serializable
接口的类都会有一个版本号,如果我们没有定义,JDK 工具会按照我们对象的属性生成一个对应的版本号;如果序列化和反序列化时的版本号不一致,会反序列化失败。比如序列化完获得字节流A,再给实体新增属性,然后对A反序列化会失败。
对于父类、子类序列化:
- 父类实现序列化,子类没有
对子类对象进行序列化时,所有的属性(自身+父类)也会全部被序列化;- 父类没有实现序列化,子类实现序列化
对子类对象进行序列化时,仅子类自身属性被序列化,父类属性并不会被序列化- 当父、子类都实现了序列化,并且定义了不同的版本号
所有的属性(自身+父类)也会全部被序列化。这种情况下,版本号是跟着子类的版本号走的
总结:如果用的是SpringBoot + Dubbo
组合的框架,那么在通过rpc
调用的时候,如果传输的对象没有实现序列化,会直接报错;在使用序列化的时候,坑点还不少,尤其是版本号的问题,这个很容易被忽略,在实际开发的时候,强烈推荐自定义版本号,这样可以避免传输的对象属性发生变化的时候,接口反序列化出错的概率!
volatile
volatile 轻量级同步机制 在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
抽象内存模型JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory,抽象概念)中,每个线程都有一个私有的本地内存(Local Memory,抽象概念),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。(个人理解,实质硬件上对应计算机的多核【处理器-多级缓存结构】;Java为实现平台无关性,抽象出的概念)
可以保证可见性,和一定的有序性,无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
优点:不会引起线程上下文切换和调度,简单开销低;
缺点:同步性能较差,易出错(不适用非原子操作)。常用于单写多读场景:
a,对变量的写操作不依赖当前值;
b,该变量没有包含其他变量,即保证对修饰变量操作的原子性,才能保证不出错。
当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
MESI协议-基于写失效的缓存一致性协议
参考:https://www.cnblogs.com/lem985/p/15876943.html
二、基础类
2.1 int和Integer的区别
自动装箱 / 自动拆箱是发生在编译阶段;
valueOf 会使用到缓存机制,自动装箱转换为 Integer.valueOf() 把拆箱替换为 Integer.intValue(),所以会使用缓存;
- Integer 的缓存范围默认是 -128 到 127,可通过jvm参数设置
- Boolean,缓存了 true/false 对应实例,确切说,只会返回两个常量实例 Boolean.TRUE/FALSE
- Short,同样是缓存了 -128 到 127 之间的数值。Byte,数值有限,所以全部都被缓存。
- Character,缓存范围’\u0000’ 到 ‘\u007F’
2.2 String、StringBuffer、StringBuilder区别
String(不可变,线程安全) 定义: private final char value[] 字符串常量创建时,先判断字符串常量池是否存在,不存在则在字符串常量池创建后返回; 通过new创建时:先判断字符串常量池是否存在,不存在则创建;再判断堆中是否存在,不存在则创建。保证字符串常量池和堆中都有该对象。
StringBuilder(线程不安全)
StringBuffer(线程安全)
在循环中synchronized会进行锁粗化,将锁范围扩展到整个操作,从而避免频繁进行锁操作造成性能开销增大
//直接字符串拼接,省事且直观
String a=”aa”+”bb”+”cc”; //a
//构建,麻烦不直观
String strByBuilder = newStringBuilder().append("aa").append("bb").append("cc").append ("dd").toString();//b
实际操作时,两种拼接方法,在一般情况下,没必要写成b(可读性差),Java 9 利用 InvokeDynamic,将字符串拼接的优化与 javac 生成的字节码解耦,假设未来 JVM 增强相关运行时实现,将不需要依赖 javac 的任何修改。
字符串缓存
把常见应用进行堆转储(Dump Heap),然后分析对象组成,会发现平均 25% 的对象是字符串,并且其中约半数是重复的。如果能避免创建重复字符串,可以有效降低内存消耗和对象创建开销。String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。
但java6里,被缓存的字符串是存在所谓 PermGen 里的(“永久代”),这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,OOM 就会光顾。
面试题:
阿里一面:如何将重复性比较高的 String 类型的地址信息从 20GB 降到几百兆?-腾讯云开发者社区-腾讯云
2.3 深拷贝、浅拷贝 des copy source
浅拷贝: des的属性只是source的引用,source值变时,从des获取的值跟着变; 类比于C++里,按引用传递
深拷贝: des的属性值和source一样,source值变时,des的属性不变;类比于C++ 里 按值传递
2.4 Exception 和 Error 的区别
同:都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。
异:Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。 Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,
可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面介绍的不可查的 Error,是 Throwable 不是 Exception。没有继承RuntimeException的Exception属于可检查异常,这类问题在编译期就可以确定的问题,如FileNotFoundException、IOException;
部检查异常:继承了RuntimeException的Exception,非检查异常也叫运行时异常,这类问题大部分属于逻辑问题,如数组越界、空指针异常,只有运行时才能知道的问题,异常在编译时不会检查。通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
try-catch 代码段会产生额外的性能开销,Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
三、 集合
快速失败
应对多线程场景。采用快速失败机制的集合容器,使用迭代器进行遍历集合时,除了通过迭代器自身的 remove() 方法之外,对集合进行任何其他方式的结构性修改(改变集合大小的修改),则会抛出 ConcurrentModificationException 异常。
在 java.util 包下的集合类都采用的是快速失败机制,不能在多线程下发生并发修改(迭代过程中被修改)。排除HashTable(hashTable线程安全,相关方法用synchronized修饰,低效)。
原理:
迭代器在遍历时直接访问集合的内容,为保证集合中的内容在遍历的过程中不能被修改,迭代器内部维护了一个modCount 变量 ,当集合结构改变(添加、删除或者修改),就会改变 modCount 的值。每当迭代器使用 hasNext() 和 next() 方法遍历下一个元素之前,都会检查 modCount 的值是否等于 expectedmodCount 的值,当检测到 modCount != expectedmodCount 时,抛出 ConcurrentModificationException 异常,反之继续遍历。
注意: 迭代器的快速失败机制是无法得到保证的,这里异常的抛出条件是检测到 modCount != expectedmodCount 这个条件。如果集合发生变化时 modCount 的值刚好等于 expectedmodCount 的值,则异常不会抛出。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法,迭代器的快速失败机制应该仅用于检测 bug。
优点:单线程下效率较高;
缺点:多线程下,线程不安全。
安全失败
采用安全失败机制的集合容器,使用迭代器进行遍历时不是直接在集合内容上访问的,而是将原有集合内容进行拷贝,在拷贝的集合上进行遍历。
原理:
迭代器在遍历时访问的是拷贝的集合,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException 异常。
优缺点:
- 由于对集合进行了拷贝,避免了 ConcurrentModificationException 异常,但拷贝时产生大量的无效对象,开销大。
- 无法保证读取到的数据是原集合中最新的数据,即迭代器进行遍历的是拷贝的集合,在遍历期间原集合发生的修改,迭代器是检测不到的。
3.1 CopyOnWriteArrayList底层实现原理
CopyOnWriteArrayList是基于数组实现的,采用多写分离的方式来应对多线程的读写操作,当对内部数组进行操作时,会拷贝一份出来用于写,写操作时会进行加锁避免并发写入导致数据丢失,而读依旧在旧数组进行,当操作完之后,再将引用重新指向新数组完成整个操作。 CopyOnWriteArrayList适合读多写少的场景,但是比较吃内存,并且对于实时性要求很高场景不太适用因为可能读到旧数据
3.2 ConcurrentHashMap
jdk7 实现是基于分段锁
也就是将内部进行分段(Segment),concurrentHashMap由Segment数组和HashEntry数组组成;每个segment包含一个 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。
HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。
Segment 的数量默认是 16,可以在相应构造函数直接指定,需要是 2 的幂数值。
在进行并发操作的时候,只需要锁定相应段,这样就有效避免了类似 Hashtable 整体同步的问题,大大提高了性能。进行并发写操作时:
- ConcurrentHashMap 会获取再入锁,以保证数据一致性,Segment 本身就是基于 ReentrantLock 的扩展实现,所以,在并发修改期间,相应 Segment 是被锁定的。
- 在最初阶段,进行重复性的扫描,以确定相应 key 值是否已经在数组里面,进而决定是更新还是放置操作,你可以在代码里看到相应的注释。重复扫描、检测冲突是 ConcurrentHashMap 的常见技巧。
- ConcurrentHashMap 扩容时,不是整体的扩容,而是单独对 Segment 进行扩容
- java 8 数组+链表/红黑树
- 结构和hashMap类似,移除Segment(一种reentryLock),使用synchronize+CAS,使用数组+链表/红黑树,。 链表/红黑树节点Node结构如下:
- Key 是 final 的,因为在生命周期中,一个条目的 Key 发生变化是不可能的;
- val,则声明为 volatile,以保证可见性。
- 每个Node声明为 volatile 来保证可见性。
-
/** * Key-value entry. This class is never exported out as a * user-mutable Map.Entry (i.e., one supporting setValue; see * MapEntry below), but can be used for read-only traversals used * in bulk tasks. Subclasses of Node with a negative hash field * are special, and contain null keys and values (but are never * exported). Otherwise, keys and vals are never null. */ static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; .... }
-
使用 CAS 等操作,在特定场景进行无锁并发操作。
-
使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。
-
CAS(V,O,N)
核心思想为:若当前变量实际值 V 与期望的旧值 O 相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值 N 赋值给变量;若当前变量实际值 V 与期望的旧值 O 不相同,则表明该变量已经被其他线程做了处理,此时将新值 N 赋给变量操作就是不安全的,在进行重试。 -
3.3 HashMap
- 初始容量:16; 负载因子0.75;
-
jdk7 数组+链表
-
jdk8 数组+链表/红黑树
-
put(key,value):对key进行hash运算,(h = key.hashCode()) ^ (h >>> capacity); 得到的结果既为在数组中的位置;链表头插法;
-
扩容时,双重循环 对数组元素循环遍历(对每个数组元素下的链表遍历,链表每个元素按新容量重新计算下标,放入新数组对应位置)。从而将旧数组数据转移至新数组,原先hash冲突的数据可能就不再冲突了;对于jdk8来说,扩容时是原来的2倍(16->32),优化后的hash算法会使得旧数组元素 要么在原位置8,要么在【原位置+原容量】(8+16)的位置。
-
负载因子:hash冲突&空间利用率 间的平衡;为什么是0.75? 统计学确定,超过0.8 CPU的缓冲命中率指数下降。
-
多线程下线程不安全
- 主要因为插入、删除、扩容时,会导致链表发生变化
-
1.jdk7 多线程扩容导致死循环
由于扩容时使用头插法,会将链表后面的节点插入前面。当两个线程交替对该map扩容时,导致两个节点循环指向。 jdk8不存在该问题。
2. 多线程执行put操作,可能会导致元素丢失
-
// put步骤②:计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
e.g. A线程执行了if判断(=null),被挂起;
B线程执行if判断(=null),创建新节点;
A再继续执行,创建新节点
如果两个元素的hash值一样,B插入元素后,A再插入,会覆盖掉B操作
3. put和get并发时,可能会导致获取null
put触发扩容,扩容时oldTable=newTable, 此时元素还没转移,get从oldTable获取元素获取不到。
四、线程&并发
4.1 线程方法
Thread. run() vs start()
run方法只是对象的方法,不会创建线程;
start方法会从用户态切换到内核态,创建线程。
Thread.join()
作用:让主线程等待(waiting),直到join的线程执行结束。
join方法是用wait()方法实现,但为什么没有通过notify()系列方法唤醒?java的Thread类线程执行完run()方法后,会自动执行notifyAll()方法。
Thread.sleep()
用于线程休眠。不释放对象锁,即如果有synchrized同步块,其他线程不能访问共享数据;高优线程sleep后,低优线程有机会执行;
sleep(500)表示休眠时间;sleep(0)表示当前线程重新触发一次CPU竞争=yeild,触发一次OS分配时间片的操作;无参会报错。
Thread.yield()
让出CPU,使线程重新回到就绪状态(有可能马上又被执行),所以yield()后,只能使同优先级/高优的线程有机会执行;会进行上下文切换。
Thread.stop()
强制终止未执行完的线程。不优雅
Object.wait()
有参数表示『等待时间』,无参数表示无限等待;wait是Object的方法。
做的事情:
1).使当前的线程进行等待
2).释放当前的锁;
3).被唤醒时,重新尝试获取这个锁
wait结束等待的条件:
1).其他线程(也可以不是线程)调用了该对象的notify或notifyAll方法(唤醒后,进入就绪状态)
2).其他线程(也可以不是线程)调用该等待线程的interrupted方法
3).等待时间超时:wait(有参) 注意:wait(0)表示的是无限等待
wait的用法:
1).必须配合synchronized使用
2).且使用的必须为同一个对象:synchronized (A)配合A.wait()使用
3).当线程执行到object.wait()时,此线程会同时释放锁synchronized (object);当它结束了wait后,此线程又会重新去争抢锁synchronized (object)。
注意:上图object.wait()不是使object等待,而是当前执行wait的线程等待。比如用于生产者、消费者问题。对共享变量加锁,共享变量.wait(), 线程等待;共享变量.notifyAll(),线程唤醒。
LockSupport.park()/
LockSupport.unpark(): 挂起/恢复线程
4.2 CAS 乐观锁
没有获取到锁的线程应该怎么办?
通常有两种处理方式:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。
自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)
并发保证
原子性:一个或多个操作要么全部执行 并且执行过程不会被任何因素打断;要么都不执行。
可见性:多个线程访问同一个变量时,一个线程修改了该变量的值,其他线程能立即看得到修改后的值;
有序性:程序执行的顺序按照代码的先后顺序执行。
Compare And Swap,比较并交换。它采用三个参数:一个是当前内存值V,旧的预期值A,即将更新的值B。当且仅当旧的预期值A和当前内存值V相同时,才将内存值修改为B并返回true;若当前内存值域旧的预期值不相等时,则不操作,返回false。如果CAS操作失败,则通过自旋的方式等待并再次尝试,直到成功。
CAS没有使用锁,通过硬件(指令集提供专门指令)在内存中进行原子操作,(native修饰为本地方法,非java编写)因此性能比synchronized要好很多。调用同步组件中大量使用了CAS技术实现Java多线程的并发操作,整个AQS同步组件,Atomic原子类都是以CAS实现的,CAS是整个JUC的基石。
4.3 synchronized 悲观锁
锁升级过程:无锁->偏向锁->轻量锁->重量锁
synchronized属于悲观锁,在操作同步之前先给对象加锁,加的锁是存在Java对象头里的。针对hotspot虚拟机,对象头包括Mark Word(标记字段) 和 Klass Pointer(类型指针)。
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在32位虚拟机和64位虚拟机的 Mark Word 所占用的字节大小不一样,32位虚拟机的 Mark Word 和 Klass Pointer 分别占用 32bits 的字节,而 64位虚拟机的 Mark Word 和 Klass Pointer 占用了64bits 的字节。以 32位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的:
JVM基于进入和退出 Monitor 对象来实现方法同步和代码块同步。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行 monitorexit 指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
偏向锁:为了解决只有在一个线程执行同步时提高性能。大多数情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况。
轻量级锁:是指当前锁是偏向锁的时候,被另外的线程所访问,那么偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
重量级锁:Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
4.4 ReentrantLock 独占锁
java类实现锁,比synchronize方法丰富,更灵活强大。内部设置两种锁:
- 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
一个线程A来了,需要加锁,首先看排队队列有无等待线程,有则将A放入队列等待;无则加锁,加不上放队列。 - 非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
一个线程A来了要加锁,直接尝试加锁,加锁成功则执行;失败则放入队列等待。公平&非公平:当前执行线程释放锁后,唤醒队列第一个线程
4.5 同步工具类
countDownLatch
设置一个初始值=线程数,每个线程执行完任务,计数器-1,直到=0表明所有线程已执行完毕,await()唤醒主线程继续执行。常用于:
1). 某个线程需要在其他n个线程执行完毕后再向下执行
2). 多个线程并行执行同一个任务,提高响应速度
五、进阶
5.1 Java 反射机制
反射机制是 Java 语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
通过运行时操作元数据或对象,Java 可以灵活地操作运行时才能确定的信息。
5.2 动态代理是基于什么原理
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)、Javassist 等。很多繁琐的重复编程,都可以被动态代理机制优雅地解决。