最后
小编精心为大家准备了一手资料
以上Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术
【附】架构书籍
- BAT面试的20道高频数据库问题解析
- Java面试宝典
- Netty实战
- 算法
BATJ面试要点及Java架构师进阶资料
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“subThread change flag to:” + flag);
flag = true;
}
}
在主线程的 while
循环中,加入内存屏障,测试是否能够感知到 flag
的修改变化:
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); //加入读内存屏障
if (flag){
System.out.println(“detected flag changed”);
break;
}
}
System.out.println(“main thread end”);
}
运行结果:
subThread change flag to:false
detected flag changed
main thread end
而如果删掉上面代码中的 loadFence
方法,那么主线程将无法感知到 flag
发生的变化,会一直在 while
中循环。可以用图来表示上面的过程:
了解java内存模型( JMM
)的小伙伴们应该清楚,运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。
3、对象操作
a、对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的 putInt
、 getInt
方法外,Unsafe提供了全部8种基础数据类型以及 Object
的 put
和 get
方法,并且所有的 put
方法都可以越过访问权限,直接修改内存中的数据。阅读openJDK源码中的注释发现,基础数据类型和 Object
的读写稍有不同,基础数据类型是直接操作的属性值( value
),而 Object
的操作则是基于引用值( reference value
)。下面是 Object
的读写方法:
//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);
除了对象属性的普通读写外,Unsafe还提供了 volatile读写 和 有序写入 方法。 volatile
读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和 Object
类型,以 int
类型为例:
//在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);
相对于普通读写来说, volatile
读写具有更高的成本,因为它需要保证可见性和有序性。在执行 get
操作时,会强制从主存中获取属性值,在使用 put
方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。
有序写入的方法有以下三个:
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序写入的成本相对 volatile
较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:
Load
Store
顺序写入与 volatile
写入的差别在于,在顺序写时加入的内存屏障类型为 StoreStore
类型,而在 volatile
写入时加入的内存屏障是 StoreLoad
类型,如下图所示:
在有序写入方法中,使用的是 StoreStore
屏障,该屏障确保 Store1
立刻刷新数据到内存,这一操作先于 Store2
以及后续的存储指令操作。而在 volatile
写入中,使用的是 StoreLoad
屏障,该屏障确保 Store1
立刻刷新数据到内存,这一操作先于 Load2
及后续的装载指令,并且, StoreLoad
屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。
综上所述,在上面的三类写入方法中,在写入效率方面,按照 put
、 putOrder
、 putVolatile
的顺序效率逐渐降低,
b、使用Unsafe的 allocateInstance
方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
分别基于构造函数、反射以及Unsafe方法的不同方式创建对象进行比较:
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
打印结果分别为1、1、0,说明通过 allocateInstance
方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了 Class
对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将A类的构造函数改为 private
类型,将无法通过构造函数和反射创建对象,但 allocateInstance
方法仍然有效。
4、数组操作
在Unsafe中,可以使用 arrayBaseOffset
方法可以获取数组中第一个元素的偏移地址,使用 arrayIndexScale
方法可以获取数组中元素间的偏移地址增量。使用下面的代码进行测试:
private void arrayTest() {
String[] array=new String[]{“str1str1str”,“str2”,“str3”};
int baseOffset = unsafe.arrayBaseOffset(String[].class);
System.out.println(baseOffset);
int scale = unsafe.arrayIndexScale(String[].class);
System.out.println(scale);
for (int i = 0; i < array.length; i++) {
int offset=baseOffset+scale*i;
System.out.println(offset+" : "+unsafe.getObject(array,offset));
}
}
上面代码的输出结果为:
16
4
16 : str1str1str
20 : str2
24 : str3
通过配合使用数组偏移首地址和各元素间偏移地址的增量,可以方便的定位到数组中的元素在内存中的位置,进而通过 getObject
方法直接获取任意位置的数组元素。需要说明的是, arrayIndexScale
获取的并不是数组中元素占用的大小,而是地址的增量,按照openJDK中的注释,可以将它翻译为 元素寻址的转换因子 ( scale factor for addressing elements
)。在上面的例子中,第一个字符串长度为11字节,但其地址增量仍然为4字节。
那么,基于这两个值是如何实现的寻址和数组元素的访问呢,这里需要借助一点在前面的文章中讲过的Java对象内存布局的知识,先把上面例子中的String数组对象的内存布局画出来,就很方便大家理解了:
在String数组对象中,对象头包含3部分, mark word
标记字占用8字节, klass point
类型指针占用4字节,数组对象特有的数组长度部分占用4字节,总共占用了16字节。第一个String的引用类型相对于对象的首地址的偏移量是就16,之后每个元素在这个基础上加4,正好对应了我们上面代码中的寻址过程,之后再使用前面说过的 getObject
方法,通过数组对象可以获得对象在堆中的首地址,再配合对象中变量的偏移量,就能获得每一个变量的引用。
5、CAS操作
在 juc
包的并发工具类中大量地使用了CAS操作,像在前面介绍 synchronized
和 AQS
的文章中也多次提到了CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在Unsafe类中,提供了 compareAndSwapObject
、 compareAndSwapInt
、 compareAndSwapLong
方法来实现的对 Object
、 int
、 long
类型的CAS操作。以 compareAndSwapInt
方法为例:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
参数中 o
为需要更新的对象, offset
是对象 o
中整形字段的偏移量,如果这个字段的值与 expected
相同,则将字段的值设为 x
这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用 compareAndSwapInt
的例子:
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField(“a”));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
运行代码会依次输出:
在上面的例子中,使用两个线程去修改 int
型属性 a
的值,并且只有在 a
的值等于传入的参数 x
减一时,才会将 a
的值变为 x
,也就是实现对 a
的加一的操作。流程如下所示:
需要注意的是,在调用 compareAndSwapInt
方法后,会直接返回 true
或 false
的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在 AtomicInteger
类的设计中,也是采用了将 compareAndSwapInt
的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。
6、线程调度
Unsafe类中提供了 park
、 unpark
、 monitorEnter
、 monitorExit
、 tryMonitorEnter
方法进行线程调度,在前面介绍AQS的文章中我们提到过使用 LockSupport
挂起或唤醒指定线程,看一下 LockSupport
的源码,可以看到它也是调用的Unsafe类中的方法:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
LockSupport的 park
方法调用了Unsafe的 park
方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用 unpark
方法唤醒当前线程。下面的例子对Unsafe的这两个方法进行测试:
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(“subThread try to unpark mainThread”);
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println(“park main mainThread”);
unsafe.park(false,0L);
System.out.println(“unpark mainThread success”);
}
程序输出为:
park main mainThread
subThread try to unpark mainThread
unpark mainThread success
程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用 park
方法阻塞自己,子线程在睡眠5秒后,调用 unpark
方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示:
此外,Unsafe源码中 monitor
相关的三个方法已经被标记为 deprecated
,不建议被使用:
//获得对象锁
@Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
@Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
@Deprecated
public native boolean tryMonitorEnter(Object var1);
monitorEnter
方法用于获得对象锁, monitorExit
用于释放对象锁,如果对一个没有被 monitorEnter
加锁的对象执行此方法,会抛出 IllegalMonitorStateException
异常。 tryMonitorEnter
方法尝试获取对象锁,如果成功则返回 true
,反之返回 false
。
7、Class操作
Unsafe对 Class
的相关操作主要包括类加载和静态变量的操作方法。
a、静态属性读取相关的方法:
//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要实例化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);
创建一个包含静态属性的类,进行测试:
@Data
public class User {
public static String name=“Hydra”;
int age;
}
private void staticTest() throws Exception {
User user=new User();
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField(“name”);
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
运行结果:
false
Hydra
在Unsafe的对象操作中,我们学习了通过 objectFieldOffset
方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用 staticFieldOffset
方法。在上面的代码中,只有在获取 Field
对象的过程中依赖到了 Class
,而获取静态变量的属性时不再依赖于 Class
。
在上面的代码中首先创建一个 User
对象,这是因为如果一个类没有被实例化,那么它的静态属性也不会被初始化,最后获取的字段属性将是 null
。所以在获取静态属性前,需要调用 shouldBeInitialized
方法,判断在获取前是否需要初始化这个类。如果删除创建User对象的语句,运行结果会变为:
true
null
b、使用 defineClass
方法允许程序在运行时动态地创建一个类,方法定义如下:
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,ProtectionDomain protectionDomain);
在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器( ClassLoader
)和保护域( ProtectionDomain
)来源于调用此方法的实例。下面的例子中实现了反编译生成后的class文件的功能:
private static void defineTest() {
String fileName=“F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class”;
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod(“getAge”).invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
面试题总结
其它面试题(springboot、mybatis、并发、java中高级面试总结等)
nt=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod(“getAge”).invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
面试题总结
其它面试题(springboot、mybatis、并发、java中高级面试总结等)
[外链图片转存中…(img-TmZ0dIHH-1715548672885)]
[外链图片转存中…(img-1G2QlfaL-1715548672886)]
[外链图片转存中…(img-JN77kUX9-1715548672886)]