0%

[toc]


java8特性#

1. lambda表达式。把函数作为一个方法的参数#

  • 也叫闭包
  • 相对应的,还多了函数式接口,二者可以配合使用

2. Stream api#

  • 提供一种对Java集合运算和表达的高阶抽象,引入map、reduce等方法。
  • map方法中还支持 类名::方法名的调用方式,来单独调用方法
    把map(a->a.getX())简写成了map(A::getX)

3.新增了接口的默认方法,支持default实现了#

4.新增Optional 类,解决空指针判断问题#

5.新增Base64编码库#


JAVA11新特性#

1. 支持局部变量类型推断#

可以写var这种变量声明了,编译器能自动识别,可加快部分情况下的编程速度。
var a = 1;
var s = “abcd”;

但是会加重编译器负担。

2. 字符串api增强#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public static void main(String[] args) {

String str1 = "\t \n\r ";
//判断字符串是否是空白字符
System.out.println(str1.isBlank());
System.out.println("*****===========");
str1 = "\t abc \r \n ";
//去除字符串首尾中的空白,包括英文和其它所有语言的空白字符
String strip = str1.strip();
//去除字符串首部的空白,包括汉字
String stripLeading = strip.stripLeading();
//去除字符串尾部的空白,包括汉字
String stripTrailing = strip.stripTrailing();
System.out.println(strip+"<====>"+strip.length());

str1 = "java";
str1.repeat(3);//把str1 重复三次,输出 javajavajava

//去除字符串首尾中的空白,只能去除码值小于32的(如果是中文全角的空白是去除不了的)
String trim = str1.trim();
System.out.println(trim+"<====>"+trim.length());
}

3. stream增强#

  • 支持Stream.ofNullable(null)生成一个值的流
  • 支持while循环,符合一个条件就take结束或者开始计算
1
2
3
4
5
// 从第一个开始计算,当 n < 3 时就截止
Stream.of(1, 2, 3, 2, 1).takeWhile(n -> n < 3) .collect(Collectors.toList()); // [1, 2]

// 一开始不计算,当n<3不成立时开始计算
Stream.of(1, 2, 3, 2, 1).dropWhile(n -> n < 3) .collect(Collectors.toList()); // [3, 2, 1]

4. Optional增强#

支持orElseThrow等语法。

5. 正式引入HTTP Client API#

和Apache 的 HttpClient的区别?
8d27930b8e75caf9a2a4c131ad4d6380131d2630

6.支持jshell,类似py shell#

可以节省验证demo的时间
83c6d5568870f9f8ac73fe489efa93832ab7d598

7.默认使用G1收集器,且jvm和垃圾收集有关的参数设置进行了很多调整#

[toc]

Connection.setAutoCommit(boolean)#

用于事务提交。
setAutoCommit(true), 则执行的所有sql执行都会作为单个事务直接提交并运行
setAutoCommit(false), 则必须等调用conn.commit()才会提交运行

Q: setAutoCommit默认是true还是false#

A: 默认是true。

Q: setAutoCommit(true)的缺点是什么?
A: 如果一次性执行多个sql语句, 中间sql出错时,就会造成脏数据。

Q: setAutoCommit(false)后,如果出了错却没有在catch中进行Connection的rollBack操作,会发生什么?#

A; 操作的表就会被锁住,造成数据库死锁

fetchSize#

fetchSize 是设定JDBC的Statement读取数据的时候每次从数据库中取出的记录条数#

  • fetchSize越 , 客户端内存占用越 ,读取数据库次数越 ,速度越

Q: Oracle和Mysql中的fetchSize有什么区别?#

A: Oracle会每次网络传输fetchSize条数据到客户端, MYSQL则会一次性全部传送到客户端,因此Mysql中的fetchSize是一种模拟游标。

PreparedStatement#

Q:使用PreparedStatement相比Statement的好处?#

A:

  1. PreparedStatement是预编译的,比Statement速度快,执行效率高,因此即使sql中不带参数也最好使用PreparedStatement
  2. 代码的可读性和可维护性更好(相比于sql拼接)
  3. PreparedStatement可以防止SQL注入攻击,而Statement却不能

Q:prepareStatement是statement接口的实现吗?#

A:
prepareStatement不是实现,而是继承的接口
0a20a5f49a20cca0aa7b8e2b3bf04a98ad855e40

CallableStatement#

  • CallableStatement继承自PreparedStatement
  • CallableStatement接口添加了 调用存储过程 核函数以及处理输出参数(INOUT)的方法。
  • 即存储过程就用CallableStatement

Connection Pool#

连接池优点:

  1. 减少连接创建次数
  2. 更快的系统整体响应速度
  3. 统一连接管理,减少失误性的连接未关闭。

ResultSet#

作用: 缓存数据结果集

1
2
Statement st = conn. createStatement (int resultSetType, int resultSetConcurrency)
ResultSet rs = st.executeQuery(sqlStr)

滚动,就是指调用.next()或者.previous()或者移动到对应行


resultSetType 是设置 ResultSet 对象的类型可滚动,或者是不可滚动。取值如下(见单词知意):

  • ResultSet.TYPE_FORWARD_ONLY 只能向前滚动
  • ResultSet.TYPE_SCROLL_INSENSITIVE, 支持前后滚动,对修改不敏感
  • ResultSet.TYPE_SCROLL_SENSITIVE 支持前后滚动,对修改敏感

resultSetConcurency 是设置 ResultSet 对象能够修改的,取值如下:
  • ResultSet.CONCUR_READ_ONLY 设置为只读类型的参数。
  • ResultSet.CONCUR_UPDATABLE 设置为可修改类型的参数。

Q:Connection、statement、ResultSet的关闭顺序是?#

A:
先ResultSet、再Statement、最后再connection。
因为这种操作很麻烦,最好使用jdbc连接池,或者try-with-resource


相关原理#

Q: JDBC 的spi机制有了解吗?#

A:
SPI机制(Service Provider Interface)其实源自服务提供者框架(Service Provider Framework,参考【EffectiveJava】page6),是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔

关键在于serviceLoader去加载某路径下相关的driver包,获取出一个列表。

  1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
  2. 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。
    00f3c667eed44817134e87e4bb8ec08084a89177
    dd2a60f8ad344663f1502b1604b1db5519659a73
    后面根据url去查找匹配的driver,每个deriver都有一个match方法

深入理解java SPI机制


Q: jdbc driver是用什么classloader加载的? 和线程上下文又有什么关系?#

A:

  • Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,复习双亲委派加载机制请看:java类加载器不完整分析 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

  • ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

  • 我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。
    真正理解线程上下文类加载器(多案例分析)

[toc]


什么是JNI?#

JNI即Java Native Interface(Java本地接口)
是一个协议,主要作用为:实现Java调用c/c代码(类库),或者C/C调用Java代码


JNI应用步骤#

  1. 首先定义java类
  • 类里需要有一个native关键字的方法
  • 静态代码块加载一个C++生成的dll
1
2
3
4
5
6
7
8
9
//JNI.java文件
public class JNI{
//创建一个native接口方法,此方法在C++代码中实现
public native int call();
//静态代码块,加载由C++代码生成的.dll动态链接文件(.dll相当于Java中的jar包吧...)
static{
System.loadLibrary("JNIdll");
}
}
  1. 使用javac 编译JNI.java生成字节码文件JNI.class
    2e3b00fcc7db910ee0cc332308f69b08448d0333

  2. 使用javah 编译JNI.class, 将class文件转成头文件
    c5a38ef4c9ca5f2aaf60ee8a01699958fe0f0ece
    头文件中包含了call方法的声明
    ef461a898064377ae54e50e2c75daac5ad35b3a3

  3. 编写cpp文件,实现头文件中的call方法

1
2
3
4
5
6
7
8
9
10
11
12
//JNIdll.cpp文件

#include<stdio.h>
#include<jni.h>
#include "JNI.h"

JNIEXPORT jint JNICALL Java_JNI_call
(JNIEnv *, jobject){
//实现代码
int i = 777;
return i;
}
  1. 将java目录下的include目录下的两个文件jni.h和jni_md.h(jni_md.h在include目录下的win32目录中)拷贝到vc的include目录下, 好让C++环境支持jni

  2. 用C/C++编译命令编译cpp和h文件,生成dll动态链接库
    7da23cd5f25cc82ce66c07e3ee06f7e09717a5f9


JNI的底层原理#

  1. jvm调用native方法时, 其方法指针nativeFunc默认先指向resolveNativeMethod方法

  2. resolveNativeMethod方法中, 是一段通用的实现代码
    首先,判断这个方法如果是静态方法,首先确保所属的class已经初始化,否则会报错
    c1b4d05e0a5342c8159fbc8187caf6c2fb514775

  3. 接着先在内部本地方法表中查询。
    内部本地方法表是一个集合,这个集合里包含了java语言和虚拟机本身用到的所有native方法
    根据类描述符的hash值从集合中寻找对应的方法
    如果找到了,就修改nativeFunc的指针,指向对应方法

  4. 如果内部本地方法表里没找到, 才会通过System.loadLibrary加载的so,虚拟机会将所有的so存入一个表中
    gDvm.nativeLibs是一个HashTable,其中存的是动态链接库的信息。其中的数据是通过System.loadLibrary添加进去的。

① 检查是否已经加载过该so,如果是还需要判断加载so的classLoader是否相同,不同的话也会报错,
②如果没有加载过,则使用dlopen打开so,然后创建一个ShareLib加入到gDvm.nativeLibs哈希表中。
③如果so中有JNI_OnLoad方法,则执行该方法。我们可以在该方法中做一些初始化工作,还可以手动建立java类中的native方法和so中的native方法的对应关系。

【源码解读】JNI的实现原理

[toc]


以一问一答的形式学习java工具

Q:检查内存泄露的工具有?#

A: jmap生成dump转储文件,jhat可视化查看。


Q:某进程CPU使用率一直占满,用什么工具可以排查?#

A:
top -Hp pid找到最占CPU的线程
然后jstack来查找那个线程此时所处的堆栈,确定问题发生位置。


各工具详细介绍#

jstack#

全称: JVM Stack Trance
作用: 查看某个java进程的堆栈情况, 可用于确认死锁、IO等待、死循环等问题。
命令用法:

  • jstack pid

  • 查看死锁例子如下图,找到wait的lock和已被锁的lock

  • 查看等待IO例子:

jstat#

全称:
作用:
查看进程中内存使用情况,但只能给出一些简单统计数据

  • 统计加载了多少类以及占用空间 jstat -class pid

  • 统计编译了多少文件 jstat -compiler 10


Q: jstat -gcutil {pid} 2000 可以每隔2秒,对pid进程打印内存使用统计信息。
gcutil的输出如下
bf7e8bc9d6c5e91598baa006685e943e60df272e
里面哪个百分比如果长期处于99%-100%会有OMM风险?(OutOfMemoryError)
A:
如果E和O即新生代、老年代内存区一直都处于满的状态,则很有可能会引发OMM风险。就像下面这个。
5d59d85bd0c23c151b5985f4aa98f0df64717b1e


jmap#

全称: JVM Memory Map
作用:生成进程的内存堆快照
当需要看一下进程里是什么东西占用了过多内存时, 可以用jmap打印一下堆快照。
命令用法:

  • 打印堆快照: jmap -dump:file=./dumpfile.dump 进程pid

  • 查看特定类所占用的情况: jmap -histo:live 进程pid | grep 类名


Q: 用jmap检查内存泄漏实例#

我分了3次,每个1h调用jmap查看进程的内存实例,查看该时刻各对象数量
第一次查看情况如下:
424ece2d981a6aa09227e85b59c4e40e62bc3879
第二次隔了2各小时,查看情况如下:

648c5a30bede19cb2c9848dde177167b9cee2dda
请问哪个对象可能有内存泄漏风险?
A:
ObjectA可能有泄露, 因为他的对象数量增加最快,说明对象一直在增加且很多无法进行gc释放。

jhat#

全称: JVM Heap Analysis Tool
和jmap配合, 可以解析jmap生成的堆快照, 支持生成1个web进程供我们分析和查看。
命令用法:

  • jhat -J-Xmx515M dumpfile.dump
    此时就会启动1个webServer,然后我们去访问就行了
    0361a76d6700b236053c852d2dc02a08dfaa9db8

jdb#

全称:Java Debugger
作用:用来对core文件和正在运行的Java进程进行实时地调试,类似于c++里的gdb
常见用法:

  • 启动进程并调试: jdb -classpath . Test

  • 至二级调试某进程: jdb -attach 8000 -sourcepath /Users/wefit/Development/study/java/jtest/src/

jcmd#

作用:多功能的工具,可以用它来导出堆、查看Java进程、导出线程信息、执行GC、还可以进行采样分析,可以理解为1个性能调优时用的工具。
常见命令:

  • 查看 当前机器上所有的 jvm 进程信息: jcmd -l

  • 查看指定进程的性能统计信息: jcmd pid PerfCounter.print

  • 列出当前运行的 java 进程可以执行的操作: jcmd PID help

  • 查看线程堆栈信息: jcmd PID Thread.print

  • 查看堆内存信息: jcmd PID GC.heap_dump FILE_NAME

jps#

简单记法: JVM process status
全名:Java Virtual Machine Process Status Tool
作用: 显示?当前系统用户?的?所有?Java进程情况及其进程号
常用命令:

  • 查看进程jvm参数: jps -v

  • 输出程序main class的完整package名或程序的jar文件完整路径名: jps -l

  • 输出传递给main方法的参数: jps -m

jinfo#

jvm infomation
作用:和jps功能类似, 但是支持根据指定pis查看指定进程

  • 可以查看JVM参数、系统参数、调整jvm参数

  • 但不支持查看java程序的内存使用情况

javap#

把java字节码文件反汇编为Java源码文件。

javac#

javac是java编译工具
javac的执行过程:
8c24abf8a5ff633bad6222ad66eac944c715ed91

IDEA-DEBUG功能#

idea的debug功能是怎么实现的? 为什么可以通过Evaluate 进行条件判断的?按以下问题逐个解答
完整详解见IDEA的debug功能,背后的原理是怎样的?


Q: idea的debug中可以evaluate修改进程行为,是通过什么修改的?#

A:
ASM框架——动态修改类、方法,甚至可以重新定义类,连 CGLib 底层都是用 ASM 实现的。 访问者模式。需要实现visitMethod()/visitAnnotation()


Q: 实现了ASM框架,就会自动支持修改吗?#

A:
不会自动支持, 需要使用 instrument 的类修改功能,我们需要实现它的 ClassFileTransformer 接口定义一个类文件转换器。它唯一的一个 transform() 方法会在类文件被加载时调用,在 transform 方法里,我们可以对传入的二进制字节码进行改写或替换,生成新的字节码数组后返回,JVM 会使用 transform 方法返回的字节码数据进行类的加载。


Q: 怎么才能让 JVM 能够调用我们提供的类转换器呢#

A:

  • JVM TI 通过事件机制,通过接口注册各种事件勾子,在 JVM 事件触发时同时触发预定义的勾子,以实现对各个 JVM 事件的感知和反应。
  • Agent 是 JVM TI 实现的一种方式
  • Java 的调试体系 jdpa 组成,从高到低分别为 jdi->jdwp->jvmti,我们通过 JDI 接口发送调试指令,而 jdwp 就相当于一个通道,帮我们翻译 JDI 指令到 JVM TI,最底层的 JVM TI 最终实现对 JVM 的操作。
  • debug时要添加参数 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 选项就指定了我们要加载的 Java Agent,jdwp 是 agent 的名字

Q: 那你讲一下我要自己实现一个可修改代码的agent的话该怎么做?#

A:

  1. 实现一个Agent,实现入口类agentmain(String args, Instrumentation inst)
    在main里给inst添加转化方法addTransformer(自定义Transformer())
    再对需要的类进行转化inst.retransformClasses(TransformTarget.class)。
  2. 实现自定义Transformer, 里面需要实现一个ClassVisitor 访问者模式,有类似visitMethod的方法实现
  3. 使用Attacher 将agent动态加载到目标jvm上。
1
2
VirtualMachine vm = VirtualMachine.attach("34242"); // 目标 JVM pid
vm.loadAgent("/path/to/agent.jar")

JDK性能监控和调优工具#

有阿里的Arthas、 商业化的jprofiler
能进行CPU分析、方法耗时分析、GC活动分析、实时堆内容分析、 采样时间内的方法火焰图等

Q: 阿里的Arthas、 商业化的jprofiler 他们能监控的原理是什么?#

  • 开发者可以在main函数执行之后再启动自己的Instrument应用,入口是agentmain函数。arthas就是通过这个实现的。利用-javaagent参数进行instrument的初始化,可以通过这个入口修改class
    之后就可以通过addTransformer,retransformClasses,redefineClasses等方式对字节码进行增强和热替换了。
  • 修改字节码也需要一套框架,不然自己重写太蛮烦了。ASM是一个Java字节码操作框架,用来动态生成class或者增强class,cglib的底层就是它,arthas也是通过它实现对class的增强的。
  • Arthas增强功能的核心是Enhancer和AdviceWeaver这两个类,对方法进行Aop织入,达到watch,trace等效果。
  • 还要提供一个启动端口连接的功能。JVMTI(JVM Tool Interface)是Java虚拟机所提供的native接口,提供了可用于debug和profiler的能力,是实现调试器和其他运行态分析工具的基础,Instrument就是对它的封装。
    通过JDI接口进行交互调试,attach端口建立连接。

Arthas运行原理

JDK可视化工具#

jconsole#

可监控jvm,即可监控本地jvm也可监控远程jvm,管理应用程序,可以检查死锁等诸多问题。

c2787e54e2a977602128d12af1e5c03d556d42b4

jvisualvm#

  • jvisualvm可以监控远程服务器的运行状态

  • 可以在java程序运行起来后再运行, 通过某些协议连接到java进程中。

  • 运行时, 可以不需要配置配置环境、虚拟机参数等。

[toc]

Q: NIO和标准IO有什么区别?#

A:

标准IO, 基于字节流和字符流进行操作,阻塞IO。
NIO基于通道channel和缓冲区Buffer进行操作,支持非阻塞IO,提供选择器


JavaNIO核心3组件:#

1. Channels 通道#

Q: 通道Channel对象能同时做读写操作吗?#

还是说需要像标准IO那样,需要同时创建input和output对象才能做读写操作?

A:
通道Channel是双向的, 既可以从channel中读数据,也可以写数据。
可以看到既能调用read也能调用write,且需要依赖缓冲区buffer。

1
2
3
4
FileChannel fileChannel = FileChannel.open(new File("a.txt").toPath());
ByteBuffer buf = ByteBuffer.allocate(1024);
fileChannel.read(buf);
fileChannel.write(buf);
  • 注意上图上,fileChannel.read(buf)是将a.txt里的数据读到buf, 即a.txt->buf
  • fileChannel.write(buf)是将buf里的数据写入到a.txt中, 即buf->a.txt,不要搞反啦!
  • 通道和缓冲区的关系如下图:
    5cc26d4b5a39246b56d13836560f8f46b428f838

Q: 通道支持异步读写吗#

A: 支持。


Q: 通道的读写是否必须要依赖缓冲区buffer?#

A: 一般都是依赖buffer的。 但也支持2个管道之间的传输,即管道之间直接读写。

1
2
3
4
5
6
String[] arr=new String[]{"a.txt","b.txt"};
FileChannel in=new FileInputStream(arr[0]).getChannel();
FileChannel out =new FileOutputStream(arr[1]).getChannel();

// 将a.txt中的数据直接写进b.txt中,相当于文件拷贝
in.transferTo(0, in.size(), out);

Q: 有哪几种常用的NIO Channel#

A:

  • FileChannel
    Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。
    FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
    创建方式
1
2
RandomAccessFile    file = new RandomAccessFile("D:/aa.txt");
FileChannel fileChannel = file.getChannel();
  • SocketChannel
    Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
    支持非阻塞模式socketChannel.configureBlocking(false)。
    可以通过以下2种方式创建SocketChannel:
    打开一个SocketChannel并连接到互联网上的某台服务器。
    一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
    创建方式:
1
2
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("192.168.1.100",80));
  • ServerSocketChannel
    Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。ServerSocketChannel类在 java.nio.channels包中。
    SocketChannel和ServerSocketChannel的区别: 前者用于客户端,后者用于服务端
    创建方式:
1
2
3
4
5
6
7
8
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket.bind(new InetSocketAddress(80));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannle != null)
doSomething...
}

Q: fileChannal为什么不支持非阻塞?#

A:

  • UNIX不支持文件的非阻塞I / O,请参见 Non-blocking I/O with regular files.由于Java应该(至少尝试)在所有平台上提供相同的行为,所以FileChannel不会实现SelectableChannel.
  • 阻塞与非阻塞发生在进程与进程之间的通信,一般文件的读写操作只会发生在进程内部,不存在外部进程的传输。
    即使存在进程间的传输,文件依旧需要通过网络通信,把内容转成数据流传输,这点的传输和Socket通信是相同的,所以没必要再造一个API出来

2. Buffer缓冲区#

buffer特点详解#

  • 我们真正要把数据拿到或者要写数据, 实际上都是通过buffer进行操作的。
    文件 <-> buffer <-> 数据
  • buffer是1个即可读也可写的缓冲区,拥有读写2种模式。
  • buffer的capacity属性限定了每个buffer的最大容量,下面的1024就是capacity。
    ByteBuffer buf = ByteBuffer.allocate(1024);
  • buffer拥有1个position属性,表示当前的读写位置。
  • 往buffer中写数据时,position就会增加。
  • position最大值为capacity-1
  • 把fileChannel对应文件里的数据 写入到buffer,叫做写模式
  • 写之后,调用flip,让buffer的postion置0,此时相当于准备读取buffer里的数据(即调用buffer.get()拿数据)
    (这个模式的叫法个人也觉得不太好,很容易绕,你可以就记忆成: flip就是从写模式转成读模式!)

Q: buffer调用flip()方法从写模式切换到读模式时,position会变成多少?#

A: 变为0。

1
2
3
4
5
6
7
8
9
ByteBuffer buf = ByteBuffer.allocate(1024);
// 数据读到buf中,并返回数量,每次最多读1024个
int byteRead = fileChannel.read(buf);
// 输出byteRead的数量,最多为1024
System.out.println("position=" + buf.position()+", byteRead=" + byteRead);

buf.flip();
// 切换到读模式了,输出0
System.out.println("position=" + buf.position());

Q: buffer拥有1个limit属性。知道是做什么的吗?#

A:
写模式下,buffer的limit就是buffer的capacity,即最大容量


Q: 当buffer从写模式切换到读模式时,limit为多少?#

A: 每次切换前都要调用flip(),切换后,limit为写模式中的position。

1
2
3
4
5
6
7
int byteRead = fileChannel.read(buf);
// 输出1024
System.out.println("limit=" + buf.limit() + ",postion=" + buf.position());
System.out.println("切换到读模式");
buf.flip();
// 输出byteRead数量
System.out.println("limit=" + buf.limit());

结果如下
7faf1b8a996ad366bd46621cbb6207c4afa1a375


Q: 向buf缓冲区写数据的方式有哪些?#

A:

  • int byteRead = fileChannel.read(buf);
    从通道中读数据到buf中, 即相当于向buf缓冲区中写数据。
  • buf.putChar(‘a’);
    手动向buf中写入字符a, postion加1。

Q: 手动修改当前缓冲区的postion的方法有哪些?#

A:
rewind() 将postion设置为0
mark() 可以标记1个特定的位置, 相当于打标记, 在一顿操作后,可通过reset()回到之前mark()的位置(就像你需要mark我的这几篇博文一样!)


Q:1个channel管道支持多个buffer吗?#

A: 支持。 通道的write和read方法都支持传入1个buffer数组,会按照顺序做读写操作。
27b39bd04044d506452c7404849d4ecee57fc3eb


Q: 讲一下buffer的种类
A:
Buffer的种类:
a881a6f6fe6a8395391678a5982d6620de0193b7

大部分都是java基础类型产生的buffer, 除了mappedByteBuffer比较特别。


Q: Buffer的Warp、slice、duplicate分别是做什么的?#

A:

  • warp:
    根据一个byte[]来生成一个固定的ByteBuffer时,使用ByteBuffer.wrap()非法的合适。他会直接基于byte[]数组生成一个新的buffer,值也保持一致。

  • slice:
    得到切片后的数组。

  • duplicate:
    调用duplicate方法返回的Buffer对象就是复制了一份原始缓冲区,复制了position、limit、capacity这些属性

注意!!!!!!
以上warp\slice\duplicte生成的缓冲区get和put所操作的数组还是与原始缓冲区一样的
所以对复制后的缓冲区进行修改也会修改原始的缓冲区,反之亦然
因此duplicte、slice一般是用于操作一下poistion\limit等处理,但是原内容不会去变他,否则就会引起原缓冲器的修改。

Selector#

selector可用来在线程中关联多个通道,并进行事件监听。
ec184550662e4d8604d9538a21bab5ea0c04bf2c


Q: 在NIO中Selector的好处是什么?#

A:

可以用更少的线程来管理各个通道。
减少线程上下文切换的资源开销。


Q: Selector支持注册哪种类型的通道?#

A:
支持非阻塞的通道。
通道要在注册前调用 channel.configureBlocking(false) 设置为非阻塞。
例如FileChannel就没办法注册,他注定是阻塞的。
而socketChannel就可以支持非阻塞。


Q: Selector注册时,支持监听哪几种事件,对应的常量是什么?(啊最不喜欢记忆这种东西了…)#

A:
共有4种可监听事件

  • Connect 成功连接到1个服务器,对应常量SelectionKey.OP_CONNECT
  • Accept 准备好接收新进入的连接, 对应常量SelectionKey.OP_ACCEPT
  • Read, 有数据可读,对应常量SelectionKey.OP_READ
  • Write 接收到往里写的数据, 对应常量SelectionKey.OP_WRITE
    如果希望对该通道监听多种事件,可以用"|"位或操作符把常量连接起来。
1
2
 int interestingSet = Selectionkey.OP_READ | Selectionkey.OP_WRITE;
Selectionkey key = channel.register(selector,interestingSet)

SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系


Q: Selector维护的SelectionKey集合共有哪几种?#

A:
共有三种。

  1. 已注册的所有键的集合(Registered key set)
    所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

  2. 已选择的键的集合(Selected key set)
    已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(并有可能是空的)。
    不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。

  3. 已取消的键的集合(Cancelled key set)
    已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。


Q:注册之后, 如何使用selector对准备就绪的通道做处理:#

A:

  1. 调用select()方法获取已就绪的通道,返回的int值表示有多少通道已经就绪
  2. 从selector中获取selectedkeys
  3. 遍历selectedkeys
  4. 查看各SelectionKey中 是否有事件就绪了。
  5. 如果有事件就绪,从key中获取对应对应管道。做对应处理
  6. 类似如下,一般都会启1个线程来run这个selector监听的处理

Q:select()方法其实是阻塞方法,即调用时会进入等待,直到把所有通道都轮询完毕。如果希望提前结束select(),有哪些方法?#

A:
有2个办法:
wakeup(), 调用后,select()方法立刻返回。
close(), 直接关闭selector。


Q:之前说NIO是非阻塞IO,但为什么上面却说select()方法是阻塞的?#

A:
其实NIO的非阻塞,指的是IO不阻塞,即我们不会卡在read()处,我们会用selector去查询就绪状态,如果状态ok就。
而查询操作是需要时间,因此select()必须要把所有通道都检查一遍才能告诉结果,因此select这个查询操作是阻塞的。


NIO进阶#

Q: 多线程读写同一文件时,如何加锁保证线程安全?#

A:
使用FileChannel的加锁功能。

1
2
3
4
5
6
7
8
9
RandomAccessFile randFile = new RandomAccessFile(target, "rw");
FileChannel channel = randFile.getChannel();
// pos和siz决定加锁区域, shared指定是否是共享锁
FileLock fileLock = channel.lock(pos , size , shared);
if (fileLock!=null) {
do();
// 这里简化了,实际上应该用try-catch
fileLock.release();
}

Q: 如果需要读1个特大文件,可以使用什么缓冲区?#

A:
使用MappedByteBuffer。
这个缓冲区可以把大文件理解成1个byte数组来访问(但实际上并没有加载这么大的byte数组,实际内容放在内存+虚存中)。
主要通过FileChannel.map(模式,起始位置,区域)来生成1个MappedByteBuffer。
然后可以用put和get去处理对应位置的byte。

1
2
3
4
5
6
7
8
9
10
11
12
int length = 0x8FFFFFF;//一个byte占1B,所以共向文件中存128M的数据
try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE);) {
MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);
for(int i=0;i<length;i++) {
mapBuffer.put((byte)0);
}
for(int i = length/2;i<length/2+4;i++) {
//像数组一样访问
System.out.println(mapBuffer.get(i));
}
}

MapMode的三种模式:

  • MapMode.READ_ONLY(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException。
  • MapMode.READ_WRITE(读/写): 对得到的缓冲区的更改会写入文件,需要调用fore()方法
  • MapMode.PRIVATE(专用): 可读可写,但是修改的内容不会写入文件,只是buffer自身的改变。

Q:NIO中ByteBuffer, 该如何根据正确的编码,转为对应的CharBuffer#

A:
利用Charset的decode功能。

1
2
3
4
ByteBuffer byteBuffer = ...;
Charset charset = Charset.forName("UTF-8");
CharBuffer charBuffer = charset.decode(byteBuffer);
如果是CharBuffer转ByteBuffer, 就用charset.encode。

Q: NIO背后的实现原理是什么?#

A: 使用了linux的epoll机制
NIO和epoll的对应关系见浅谈NIO和Epoll的实现原理
epoll原理见epoll原理


398cc1e5fdaa0abf90cd8e577fd826ade07c3ecf

[toc]

服务端#

服务端开启过程#

用accept开启一个Socket

1
2
ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();

调用后会进入阻塞。

  • 用socket.getInputStream(), 得到一个socket输入流, 进行read时,就是从客户端那边读取发来的数据
1
2
3
4
5
InputStream inputStream = socket.getInputStream();
while(len = inputStream.read(bytes))!=-1) {
//注意指定编码格式,发送方和接收方需要统一
sb.append(new String(bytes, 0, len, "UTF-8"));
}
  • 如果server端要返回数据,就再从socket对象里get输出流,往输出流写东西,就会传到客户端那边了。
1
2
OuputStream outputStream = socket.getOuputStream();
outputStream.write("hahah, this is response".getBytes("UTF-8"))

Q:server端如何有效判断client消息传输完毕?#

A:

  • 客户端不再发送时,那边会调用socket.shutdownOutput(), 对方的read()方法就会返回-1.
    *建议使用长度+类型+数据的协议传输方式, 这样服务端可以明确直到自己这一次这要读多少字节以及是什么样的消息, 减少做消息结束判断导致的异常问题。

Q:如何避免上面用while循环处理一个请求处理时用时较久,影响了其他请求的处理?#

A:
每当accept 返回1个socket之后, 把这个socket放进一个线程中,交给线程池去处理。


ServerSocket选项#

  1. SO_TIMEOUT accept的超时时间
  2. SO_REUSEADDR 是否支持复用端口
    即另一个serverSocket还没有close的时候, 新的serverSocket能否重用那个端口。
  3. SO_RCVBUF 缓冲区大小
    收到到特定缓存的大小才会从inputStream中返回。
  • 设置方式:serverScoket.setSoXXX(…)

客户端#

Q: 客户端进行输出流输出时,如果执行了outputStream.close()后, 还需要执行socket.close()吗?#

A:
不需要。对于同一个socket, 如果关闭了输出流比如pw.close(), 则与该输出流关联的socket也会关闭, 所以一般不需要关闭输出流。 当关闭socket的时候, 输出流也会同时关闭。
因此如果执行了以下代码:

1
2
3
4
5
// 从socket中拿到inputStream和outputStream
...
outputStream.close()
inputStream.read();
inputStream.close();

这时候inputStream.read()的就会报错, 因为socket已经被关闭了。

UDP#

Q:如果要做UDP通信, 那么要用什么socket?#

A: DatagramSocket


Q: DatagramSocket和java中普通ServerSocket或者 Socket客户端的区别?#

A:

  • TCP的socket建立时, 需要先指定对端口的op和端口, 并用输出流去输出数据
  • 而java中的UDP客户端建立时, 不需要在socket里指定ip和端口, 而是将ip+端口放在datagramPacket包里去指定。(所以一个socket可以用来发送好几份不同目的组的UDP包)
  • 另一个区别是, tcp两边用流来传输数据, 而UDP用datagramPacket来传递数据

Q: MulticastSocket是什么?#

A:
多点广播的socket实现, 也是用DatagramPacket来传输数据。
类似于一个客户端可以把数据包发给号借给目的端。
multicastSocket.joinGroup(多播ip)
则这个socket会加入到这个多播组中。
举例:

假设环境中有4个MulticastSocket, 分别为socketA、socketB、socketC、socketD。
socketA、socketB、socketC都通过joinGroup方法,加入到了224.0.0.1这个多播组中
那么当socketA发送了某个DatagramPacket时,会只发送给B和C, 不会发送给D。


Q: 上面的socketA会发送给自己吗?#

A:
默认会的。

发送给自己的话, 那就是设置了环回。
如果执行过 multicastSocket.setLoopbackMode(true),那就是禁止环回机制。
注意:默认是开启的。


参考资料:
https://blog.csdn.net/a78270528/article/details/80318571(这篇写得很棒)
https://blog.csdn.net/woshisap/article/details/6597413
https://blog.csdn.net/weixin_44618862/article/details/98480120

[toc]

Java安全类库常见问题#

Q:有几种方式可以启动安全管理器?#

A:两种方式
隐式启动: 启动命令里加-Djava.security.manager, 即可开启安全管理器。
显示启动: 代码里自己new一个SecurityManager或者继承一个新子类, 然后用System.setSecurityManager(xxxSecurityManager)来设置


看以下代码:

1
2
3
4
5
public class SecurityManagerTest {    
public static void main(String[] args) throws FileNotFoundException {
System.out.println(System.getProperty("file.encoding"));
}
}

Q:如果我不设置安全管理器,直接跑System.getProperty,会报权限错误吗?#

A:
不会报错误。 如果隐式和显示启动都没做,那么System里的SecurityManager就是null,那么就不会进行检查。
可以看下System.getProperty里的源码:
492830d481c84ff56c99dec832d0939d6616b824


Q: 隐式启动中,如果只配了-Djava.security.manager ,但没有配-Djava.security.policy, 那么上面的文件以及系统配置是都允许读,还是都不允许读?#

A:
都不允许读。 即开启后,不在policy允许范围内的,都默认不允许读。


  • 如果要允许读文件以及读file.encoding那个系统配置, 需要加上-Djava.security.policy=xxx.policy , 指明policy文件。

  • 文件里大概长这样
    9fe292ac9397f17154eb131f2b7ac65ae6bbfa7a
    注意policy是拼在-Djava.security.manager的后面
    5ef1de92e4ef3956fc00c8830f82479b77213629


显示启动,就是自己new一个SecurityManager,并set进去。

Q:如果我之前有隐式启动,此时new了一个新的SecurityManager放进去,此时是否还有该属性的read权限?#

eb6b0119d97e6e30fd93f610fde5a8e5dc4ecd6b

A:没有了,之前由启动参数配进去的安全管理器已经被你覆盖掉了

对于隐式启动和显示启动, 都是默认没有任何可用权限!

都是白名单机制, 无黑名单机制。


常见的java 安全权限#

都是能从名字就知道什么作用的,瞄一眼有个印象就行,大概知道java里这些功能可能都会有权限。

  • java.security.AllPermission 所有权限的集合

  • java.util.PropertyPermission 系统/环境属性权限
    就是System.getProperty(xxx.xxx.xxx)的权限

  • java.lang.RuntimePermission 运行时权限
    这个安卓用的很多,

  • java.net.SocketPermission Socket权限

  • java.io.FilePermission 文件权限,包括读写,删除,执行

  • java.io.SerializablePermission 序列化权限

  • java.lang.reflect.ReflectPermission 反射权限

  • java.security.UnresolvedPermission 未解析的权限

  • java.net.NetPermission 网络权限

  • java.awt.AWTPermission AWT权限

  • java.sql.SQLPermission 数据库sql权限

  • java.security.SecurityPermission 安全控制方面的权限

  • java.util.logging.LoggingPermission 日志控制权限

  • javax.net.ssl.SSLPermission 安全连接权限

  • javax.security.auth.AuthPermission 认证权限

  • javax.sound.sampled.AudioPermission 音频系统资源的访问权限


关于上面的permission权限,有一个重要的应用,就是在ClassLoader中。
如果JVM开启了SecurityManager, ClasserLoader就会在加载类的时候调用Policy.getPermissions来获取代码权限集, 并将代码来源和权限集封装到保护域。

1
2
3
4
5
6
7
8
9
public class MyClassLoader extends URLClassLoader {
@Override
protect PermissionCollection getPermissions(CodeSource cs) {
PermissionCollection pc = super.getPermissions(cs);
// 添加权限
pc.add(new XXXPersion());
return pc;
}
}

Q: 详细讲讲安全管理器的应用场景?#

A:

  • 如果你是某个平台开发者,提供了某个SDK给其他人使用, 这里面涉及了某个文件读取或者文件写入操作。
    但是你只希望他用这个sdk去操作平台下特定目录的文件,而不是去动其他的位置(比如根目录之类的)
    于是你就可以给sdk代码封装上一层权限, 在这个代码块中只能读特定目录(就像上面的filePermission)
  • 或者你通过的公开类中有个高危方法deleteXXX, 你希望拥有管理员权限的人才能调用,其他人不能调用, 于是可以在高危方法中加一层权限, 下面的例子来自安全编码规范, removeEncryt就是高危方法, 调用前会检查一下是否具有这个自定义的权限类型
1
2
3
4
5
6
7
8
9
10
11
12
public class SensitiveHash {
void removeEntry(Object key) {
check("removeKeyPerssion");
ht.remove(key);
}
private void check(String dire) {
SecurityManager sm = System.getSecurityManager();
if (sm!=null) {
sm.checkSecurityAccess(dire)
}
}
}

Q: 上面这个例子中, 如果恶意调用者自己在安全管理器中增加这种权限的处理怎么办?#

A: setSecurityManager方法API也是需要权限的。 作为客户端程序一般没有权限去设置SDK的安全管理器。 java默认的policy配置文件也没有放开这个权限。需要支持在policy里setSecurityManager才行。


Q: 假设某服务实现了一个自定义classLoader, 支持从网络获取信任客户传来的jar包并在自己的服务中执行, 那么应该如何防止加载不可信的jar包呢?#

A :

  1. 取出jar包中的证书资源
  2. 获取服务自身的密钥
  3. 校验证书是否是这些密钥的受信任签名(即我要识别你的这个证书是不是从我这里签发出去的)

注意,不要使用URLClassLoaer和java.util.jar里的自动签名校验机制,他只是检查jar包中的签名和公钥是否匹配,这仅仅是完整性校验, 这2者是可以被全部替换的


Q:不加盐值的哈希口令哈希有什么缺陷?#

A:

生日判定,可以快速找到一个口令

可以利用事先计算好的哈希列表几秒钟破解。


Q:对口令做哈希加盐值时有什么要求?#

A:

盐值至少应该包含8字节而且必须是由安全随机数产生。( 例如如果问你,sha256+4字节盐值,那肯定就是错误的!)

应使用强哈希函数,推荐使用SHA-256或者更加安全的哈希函数。

迭代次数默认推荐10000次,对于性能有特殊要求(比如嵌入式系统)的产品低可迭代1000次。

对于单向哈希时,其输出长度应该不小于256比特

[toc]

文件API#

Q: File类可以用来做目录操作吗?#

A:
可以。
File对象本身可以是目录。
调用file.mkdirs()即可创建目录。


Q:直接调用file.delete()可以删除目录吗?#

A:
如果是文件或者空目录,可以直接删除。
但如果目录中有文件或者子目录,则必须递归删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
       //递归删除目录中的子目录下
for (int i=0; i<children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}

Q: 有哪些方法判断给定路径下文件是否存在?#

A:

  1. File类的exists方法: ? file.exist(string)
1
2
File testFile = new File(testFilePath);
if(!testFile .exists()) {...}
  1. File类的静态exist方法, File.exist(Path path)
1
2
Path filePath = Paths.get(testFilePath);
if (Files.exists(filePath) {...}

注意静态方法和非静态方法的区别

字节输入流InputStream#

说一下以下这些特点对应哪些InputStream类#

  • 字节数组char[] 作为输入源的InputStream类是————ByteArrayInputStream
  • 用文件作为输入源的InputStream类是?————FileInputStream
  • 用字符串作为输入源的是?————StringBufferInputStream
  • 用于多线程之间管道通信的输入源是————PipeInputStream ?

Q: FilterInputStream是什么?#

A: 用于装饰上面这些输入流的,可以叠加,每装饰一层就相当于增加了1个功能。

1
InputStream inputStream = new FilterInputStream(InputStream)


以下这些特点分别对应哪些FilterInputStream?

  • 装饰后,不仅可读字符串,还可读取例如int、long等java基本类型的是————DataInputStream
    DataInputStream里面会支持readInt、readLong等方法。
  • 装饰后,支持分批缓冲读取读取的是————BufferedInputStream
    创建BufferedInputStream时,我们会通过它的构造函数指定某个输入流为参数。BufferedInputStream会将该输入流数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从输入流中读取下一部分的数据。
  • 其他:
    PushbackInputStream: 具有1个能回退上一个字节的缓冲区
    ObjectInputStream : 一般用于反序列化读入
    LineNumberInputStream: 可跟踪输入流中的行号

字节输出流OutputStream#

Q: OutputStream包含哪些实现?#

ByteArrayOutputStream ?输出到缓冲区?
FileOutputStream ? 写到文件
PipedOutputStream ?写入管道
FilterOutputStream ?

而FilterOutputStream 包含

  • DataOutputStream ?(可以out.writexxx各种类型的数据,writeDouble, writeUTF, ?reader也一样,可以读想要的数据类型)、
  • PringtStream (输出到文件用这个, 该类.println(str)即可写入文件)
  • BufferOutputString

FileOutputStream相关

Q:new FileOutputStream(name, true),这个构造里的true参数是做什么用的?#

A:
是否支持在文件末追加的意思。
image.png

默认是false,指的是覆盖整个文本。
如果设置成true,会在要写入的文件后面追加本次写入的内容。


Q:BufferOutputStream相关概念(其实是考缓冲区是否需要刷新之类的问题)#

  • BufferOutputStream里的flush()方法是做什么的?
  • BufferOutputStream调用close后,会触发flush()来刷新缓冲区吗?
  • BufferOutputStream调用close可能会丢数据吗?
  • BufferOutputStream多次调用close会报错吗?

A:

  • flush把缓冲区里的数据写入文件,并刷新缓冲区
    image.png

  • close关闭此输出流并释放与此相关联的任何系统资源, 会调用flush,除了flushBuffer,还会调用父类的flush。

  • 不会丢数据,因为上面这条原因。

  • 多次调用不会报错。
    *?
    image.png

Reader和Writer#

Q: Reader/Writer和InputStream/OutputStream的区别?#

A:

  • InputStream是表示 字节输入流 的所有类的超类
    Reader是用于读取 字符流 的抽象类
    InputStream提供的是字节流的读取,而非文本读取,这是和Reader类的根本区别。
    即用Reader读取出来的是char数组或者String ,使用InputStream读取出来的是byte数组。
  • Reader/Writer提供兼容Unicode、面向字符的IO功能,为了国际化

  • 用reader读取标准输入:
    BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
  • 用Writer进行标准输出:
    BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));

如何设置设置编码:#

1
2
InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8"); ?
BufferedReader read = new BufferedReader(isr); ?

序列化问题#

Q: java的序列化一般是做什么的?#

A:

  1. 永久的保存对象数据(将对象数据保存在文件当中,或者是磁盘中
  2. 通过序列化操作将对象数据在网络上进行传输(由于网络传输是以字节流的方式对数据进行传输的.因此序列化的目的是将对象数据转换成字节流的形式)
  3. 将对象数据在进程之间进行传递(Activity之间传递对象数据时,需要在当前的Activity中对对象数据进行序列化操作.在另一个Activity中需要进行反序列化操作讲数据取出)
  4. Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长(即每个对象都在JVM中)但在现实应用中,就可能要停止JVM运行,但有要保存某些指定的对象,并在将来重新读取被保存的对象。这是Java对象序列化就能够实现该功能。(可选择入数据库、或文件的形式保存)
  5. 序列化对象的时候只是针对变量进行序列化,不针对方法进行序列化.
  6. 在Intent之间,基本的数据类型直接进行相关传递即可,但是一旦数据类型比较复杂的时候,就需要进行序列化操作了.

Q: 对某对象进行序列化时, 如何让里面某个敏感成员不被序列化?#

A:?

  • 方法一:可使用transient关键字处理那个敏感成员
  • 方法二:可以通过覆盖Serializable接口的writeObject和readObject来实现序列化, ?但是方法签名必须是private void writeObject(ObjetOutputStream stream) throw IOException;
  • 方法三: 实现Externalizable接口,可自定义实现writeExternal以及readExternal方法

Q: Externalizable和Serializable哪个快? 原因是什么?#

A: Externalizable更快。
原因:

  • Java序列化写入不仅是完整的类名,也包含整个类的定义,包含所有被引用的类。
    类定义可以是相当大的,也许构成了性能和效率的问题,当然这是编写一个单一的对象。如果您正在编写了大量相同的类的对象,这时类定义的开销通常不是一个大问题。另一件事情是,如果你的对象有一类的引用(如元数据对象),那么Java序列化将写入整个类的定义,不只是类的名称,因此,使用Java序列化写出元数据(meta-data)是非常昂贵的

  • 通过实现Externalizable接口,这是可能优化Java序列化的。实现此接口,避免写出整个类定义,只是类名被写入


Q: Externalizable需要产生序列化ID吗?#

A: 采用Externalizable无需产生序列化ID(serialVersionUID)~而Serializable接口则需要

[toc]

Thread类基础#

Q: Thread的deprecated过期方法是哪3个?作用是啥#

A:

  • stop(), 终止线程的执行。
  • suspend(), 暂停线程执行。
  • resume(), 恢复线程执行。

Q: 废弃stop的原因是啥?#

A:
调用stop时,会直接终止线程并释放线程上已锁定的锁,线程内部无法感知,并且不会做线程内的catch操作
即线程内部不会处理stop后的烂摊子。如果其他线程等在等着上面的锁去取数据, 那么拿到的可能是1个半成品。
变成题目的话应该是下面这样,问会输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {

public static void main(String[] args) throws InterruptedException {

System.out.println("start");
Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.stop();
// thread.interrupt();

}
}

class MyThread extends Thread {
public void run() {
try {
System.out.println("run");
Thread.sleep(5000);
} catch (Exception e) {
//处理烂摊子,清理资源
System.out.println("clear resource!");
}
}
}

答案是输出 start和run,但是不会输出clear resource


Q: stop的替代方法是什么?#

A: interrupt()。
调用thread.interrupt()终止时, 不会直接释放锁,可通过调用interrupt()或者捕捉sleep产生的中断异常,来判断是否被终止,并处理烂摊子。
上题把thread.stop()改成thread.interrupt(),在Thread.sleep()过程中就会抛出interrupException(注意,InterrupExcetpion是sleep抛出的)
因此就会输出clear resource。
如果没有做sleep操作, 可以用isInterrupted()来判断自己这个线程是否被终止了,来做清理。
另外注意一下interrupt和isInterrupted的区别:
image.png


Q: suspend/resume的废弃原因是什么?#

A: :调用suspend不会释放锁。
如果线程A暂停后,他的resume是由线程B来调用的,但是线程B又依赖A里的某个锁,那么就死锁了。
例如下面这个例子,就要知道会引发死锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
public static Object lockObject = new Object();
public static void main(String[] args) throws InterruptedException {

System.out.println("start");
Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);

System.out.println("主线程试图占用lockObject锁资源");
synchronized (Test.lockObject) {
// 用Test.lockObject做一些事
System.out.println("做一些事");
}
System.out.println("恢复");
thread.resume();

}
}

class MyThread extends Thread {
public void run() {
try {
synchronized (Test.lockObject) {
System.out.println("占用Test.lockObject");
suspend();
}
System.out.println("MyThread释放TestlockObject锁资源");
}
catch (Exception e){}
}
}

答案输出
image.png

MyThread内部暂停后,外部的main因为没法拿到锁,所以无法执行后面的resume操作。


Q: 上题的suspend和resume可以怎么替换,来解决死锁问题?#

A: 可以用wait和noitfy来处理(不过尽量不要这样设计,一般都是用run内部带1个while循环的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test {
public static Object lockObject = new Object(); //拿来做临时锁对象
public static void main(String[] args) throws InterruptedException {

Thread thread = new MyThread();
thread.start();
Thread.sleep(1000);

System.out.println("主线程试图占用lockObject锁资源");
synchronized (Test.lockObject) {
// 用Test.lockObject做一些事
System.out.println("做一些事");
}
System.out.println("恢复");

synchronized (Test.lockObject) {
Test.lockObject.notify();
}

}
}

class MyThread extends Thread {
public void run() {
try {
synchronized (Test.lockObject) {
System.out.println("占用Test.lockObject");
Test.lockObject.wait();
}
System.out.println("MyThread释放TestlockObject锁资源");
}
catch (Exception e){}
}
}

如此执行,结果正常:
image.png


Q: 下面这例子为什么会运行异常,抛出IllegalMonitorStateException错误?#

1
2
3
4
5
public static void main(String[] args) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
thread.notify();
}

A: notify和wait的使用前提是必须持有这个对象的锁, 即main代码块 需要先持有thread对象的锁,才能使用notify去唤醒(wait同理)。
改成下面就行了

1
2
3
4
5
Thread thread = new MyThread();
thread.start();
synchronized (thread) {
thread.notify();
}

Q: 为什么wait必须持有锁的时候才能调用?#

A:
因为wait和notify是组合使用的。

  • 一般是到了一定条件例如缺少资源、缺乏某个前置动作时,才会进入wait。
  • 这时候生产资源的那个线程生产了新资源后,就会调用notify方法,告诉另一个线程,我做好了,你可以动身了。
  • 但如果我们不先加同步块, 就可能导致 wait之前的判断条件有问题,即先判断缺资源, 然后切到另一个线程 做了资源生产并notify, 这时候再wait已经没有意义了, 永远收不到notify。 ”即如果不在同步块中,则wait的判断条件或者wait时机可能是有问题的!“
    为什么WAIT必须在同步块中

Q: Thread.sleep()和Object.wait()的区别#

A:
sleep不会释放对象锁, 而wait会释放对象锁。


Q: 如果有3个线程同时抢占了这个锁且都在wait,我希望只notify唤醒某个线程,怎么办?#

A:

  1. 使用LockSupport, 可以unPark指定的线程。
    0904c96f81872d9d57deb7cc7cf5e9afb0601039
  2. 使用Lock + Condition 实现唤醒指定的部分线程。即锁是同一个,但是可以针对锁生成的特定condition做唤醒
    799b48eaf23750316d096f97237b0cca42e2332f
    3ad4a174c3b5b160dc51501502f57c2fe62cdcb5

Q: LockSupport相比notify/wait有什么优点?#

A:

  1. LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
  2. unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。

Q:Runnable接口和Callable的区别。#

A: Callable可以和Futrue配合,并且启动线程时用的时call,能够拿到线程结束后的返回值,call方法还能抛出异常。


Q:thread.alive()表示线程当前是否处于活跃/可用状态。thread.start()后,是否alive()一定返回true?#

活跃状态: 线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活的

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
TestThread tt = new TestThread();
System.out.println("Begin == " + tt.isAlive());
tt.start();
System.out.println("end == " + tt.isAlive());
}
}

A:
不一定,有可能在打印时,线程已经运行结束了,或者start后,还未真正启动起来(就是还没进入到run中)


Q: 线程A如下,把线程A作为构造参数,传给线程B,此时对B线程打印this.isAlive会显示什么?:#

1
2
3
4
5
6
public class A extends Thread {
@Override
public void run() {
System.out.println("this.isAlive()=" + this.isAlive());
}
}
1
2
3
A a = new A();
Thread b = new Thread(a);
b.start()

A:
此时会打印false!
image.png

因为把a作为构造参数传入b中, b执行start时, 实际上是在B线程中去调用了 A对象的run方法,而不是启用了A线程。
如果改成

1
2
A a = new A();
a.start()

那么就会打印true了


Q:把FutureTask放进Thread中,并start后,会正常执行callable里的内容吗?#

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws Exception {
Callable<Integer> callable = () -> {
System.out.println("call 100");
return 100;
};

FutureTask<Integer> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start();
}

A:
能正常打印

synchronized关键字#

  • 即可作为方法的修饰符,也可以作为代码块的修饰符
  • 注意修饰方法时,并不是这个方法上有锁, 而是调用该方法时,需要取该方法所在对象上的锁。
1
2
3
4
class A{
synchroized f(){
}
}

即调用这个f(), 并不是说f同一时刻只能进入一次,而是说进入f时,需要取到A上的锁。


Q: 调用下面的f()时,会出现死锁吗?#

1
2
3
4
5
6
7
8
class A{
synchroized f(){
t()
}

synchroized t(){
}
}

A:不会。
1个线程内, 可以重复进入1个对象的synchroized 块。

  • 原理:
    当线程请求自己的锁时。JVM会记下锁的持有者,并且给这个锁计数为1。
    如果该线程再次请求自己的锁,则可以再次进入,计数为2.
    退出时计数-1.
    直到全部退出时才会释放锁。

  • 目的是为了避免死锁。万一 1个对象在sync方法中调用另一个sync方法,如果是非重入的,就可能导致自己把自己锁住了。

sync和JUC-Lock都是可重入锁,原理类似。


Q:2个线程同时调用f1和f2会产生同步吗?#

1
2
3
4
class A{
private static synchronized void f1(){};
private synchronized void f2(){};
}

A:
不会产生同步。二者不是1个锁。
f1是类锁,等同于synchronized(A.class)
f2是对象锁。

其他的同步工具#

CountDownLatch#

1
final CountDownLatch latch = new CountDownLatch(2);

2是计数器初始值。
然后执行latch.await()时, 就会阻塞,直到其他线程中把这个latch进行latch.countDown(),并且计数器降低至0。

  • 和join的区别:
    join阻塞时,是只等待单个线程的完成
    而CountDownLatch可能是为了等待多个线程

Q: countDownLatch的内部计数值能被重置吗?#

A:
不能重置了。如果要重新计数必须重新new一个。毕竟他的类名就叫DownLatch

FutureTask#

可以理解为一个支持有返回值的线程
FutureTask<Integer> task = new FutureTask<>(runable);
当调用task.get()时,就能能达到线程里的返回值


Q:调用futrueTask.get()时,这个是阻塞方法吗?如果是阻塞,什么时候会结束?#

A:
是阻塞方法。

  1. 线程跑完并返回结果
  2. 阻塞时间达到futrueTask.get(xxx)里设定的xxx时间
  3. 线程出现异常InterruptedException或者ExecutionException
  4. 线程被取消,抛出CancellationException

Semaphore#

信号量概念#

就是操作系统里常见的那个概念,java实现,用于各线程间进行资源协调。
用Semaphore(permits)构造一个包含permits个资源的信号量
然后某线程做了消费动作, 则执行semaphore.acquire(),则会消费一个资源
如果某线程做了生产动作,则执行semaphore.release(),则会释放一个资源(即新增一个资源)
更详细的信号量方法说明:
https://blog.csdn.net/hanchao5272/article/details/79780045


Q: 信号量中,公平模式和非公平模式的区别?下面设成true就是公平模式#

1
2
//new Semaphore(permits,fair):初始化许可证数量和是否公平模式的构造函数
semaphore = new Semaphore(5, true);

A:
其实就是使用哪种公平锁还是非公平锁。
image.png

Java并发中的NonfairSync(非公平)和fairSync(公平)主要区别为:

  • 如果当前线程不是锁的占有者,则NonfairSync并不判断是否有等待队列,直接使用compareAndSwap去进行锁的占用,即谁正好抢到,就给谁用!
  • 如果当前线程不是锁的占有者,则FairSync则会判断当前是否有等待队列,如果有则将自己加到等待队列尾,即严格的先到先得!

CyclicBarrier (栅栏)#

栅栏,一般是在线程中去调用的
它的构造需要指定1个线程数量,和栅栏被破坏前要执行的操作
每当有1个线程调用barrier.await(),就会进入阻塞,同时barrier里的线程计数-1。
当线程计数为0时, 调用栅栏里指定的那个操作后,然后破坏栅栏, 所有被阻塞在await上的线程继续往下走。

Exchanger (交换栅栏)#

我理解为两方栅栏,用于交换数据。
简单说就是一个线程在完成一定的事务后,想与另一个线程交换数据
则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据

原子类AtomicXXX#

就是内部已实现了原子同步机制

Q:下面输出什么?(考察getAndAdd的用法)#

1
2
3
AtomicInteger num = new AtomicInteger(1);
System.out.println(num.getAndAdd(1));
System.out.println(num.get());

A:
输出1、2
顾名思义, getAndAdd(),那么就是先get,再加, 类似于num++。
如果是addAndGet(),那么就是++num


Q:AtomicReference和AtomicInteger的区别?#

A:
AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。
即可能会有多个线程修改atomicReference里包含的引用。

  • 经典用法:
    boolean exchanged = atomicStringReference.compareAndSet(initialReference, newReference)
    就是经典的CAS同步法
    compreAndSet它会将将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。类似于一个非负责的自旋锁。

  • AtomicReferenceArray是原子数组, 可以进行一些原子的数组操作例如 set(index, value),

java中已实现的全部原子类:#

image.png

注意,没有float,没有short和byte。


线程池#


Q: ThreadPoolExecutor线程池构造参数中,corePoolSize和maximumPoolSize有什么区别?#

A:
当提交新线程到池中时

  • 如果当前线程数 < corePoolSize,则会创建新线程
  • 如果当前线程数=corePoolSize,则新线程被塞进一个队列中等待。
  • 如果队列也被塞满了,那么又会开始新建线程来运行任务,避免任务阻塞或者丢弃
  • 如果队列满了的情况下, 线程总数超过了maxinumPoolSize,那么就抛异常或者阻塞(取决于队列性质)。

  • 调用prestartCoreThread()可提前开启一个空闲的核心线程
  • 调用prestartAllCoreThreads(),可提前创建corePoolSize个核心线程。

Q: 线程池的keepalive参数是干嘛的?#

A:当线程数量在corePoolSize到maxinumPoolSize之间时, 如果有线程已跑完,且空闲时间超过keepalive时,则会被清除(注意只限于corePoolSize到maxinumPoolsize之间的线程)


Q: 核心线程可以被回收吗?(线程池没有被回收的情况下)#

A:
ThreadPoolExecutor有个allowCoreThreadTimeOut(boolean value)方法,可以设置是否在超期后做回收


Q: 那这个线程数设置多少,你是怎么考虑的呢?#

A:
io密集型, 可以设置多一点, 因为多一个线程,他可能也没太占cpu,都是在等待IO。
如果是计算密集型,则要设置少一点,别把cpu搞满载了。

有超线程技术的话, 一般可以设置成2倍CPU数量的线程数

超线程技术把多线程处理器内部的两个逻辑内核模拟成两个物理芯片,让单个处理器就能使用线程级的并行计算,进而兼容多线程操作系统和软件。超线程技术充分利用空闲CPU资源,在相同时间内完成更多工作


Q: 线程池有哪三种队列策略?#

A:

  1. 握手队列
    相当于不排队的队列。可能造成线程数量无限增长直到超过maxinumPoolSize(相当于corePoolSize没什么用了,只以maxinumPoolSize做上限)
  2. 无界队列
    队列队长无限,即线程数量达到corePoolSize时,后面的线程只会在队列中等待。(相当于maxinumPoolSize没什么用了)
    缺陷: 可能造成队列无限增长以至于OOM
  3. 有界队列

Q: 线程池队列已满且maxinumPoolSize已满时,有哪些拒绝策略?#

A:

  • AbortPolicy 默认策略:直接抛出RejectedExecutionException异常
  • DiscardPolicy 丢弃策略: 直接丢了,什么错误也不报
  • DiscardOldestPolicy 丢弃队头策略: 即把最先入队的人从队头扔出去,再尝试让该任务进入队尾(队头任务内心:不公平。。。。)
  • CallerRunsPolicy 调用者处理策略: 交给调用者所在线程自己去跑任务(即谁调用的submit或者execute,他就自己去跑) 注意这个策略会用的比较多
  • 也可以用实现自定义新的RejectedExecutionHandler

Q: 线程池为什么需要阻塞队列?#

A:
线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。避免大量线程获取这个创建锁。


Q:有以下五种Executor提供的线程池,注意记忆一下他们的用途,就能理解内部的原理了。#

  • newCachedThreadPool: 缓存线程池
    corePoolSize=0, maxinumPoolSize=+∞,队列长度=0 ,
    因此线程数量会在corePoolSize到maxinumPoolSize之间一直灵活缓存和变动, 且不存在队列等待的情况,一来任务我就创建,用完了会释放。
    image.png

  • newFixedThreadPool :定长线程池
    corePoolSize= maxinumPoolSize=构造参数值, 队列长度=+∞。
    因此不存在线程不够时扩充的情况

  • newScheduledThreadPool :定时器线程池
    提交定时任务用的,构造参数里会带定时器的间隔和单位。 其他和FixedThreadPool相同,属于定长线程池。

  • newSingleThreadExecutor : 单线程池
    corePoolSize=maxinumPoolSize=1, 队列长度=+∞
    只会跑一个任务, 所以其他的任务都会在队列中等待,因此会严格按照FIFO执行

  • newWorkStealingPool(继承自ForkJoinPool ): 并行线程池
    如果你的任务执行时间很长,并且里面的任务运行并行跑的,那么他会把你的线程任务再细分到其他的线程来分治。
    ForkJoinPool介绍:https://blog.csdn.net/m0_37542889/article/details/92640903

A:


Q: submit和execute的区别是什么?#

A:

  • execute只能接收Runnable类型的任务,而submit除了Runnable,还能接收Callable(Callable类型任务支持返回值)
  • execute方法返回void, submit方法返回FutureTask。
  • 异常方面, submit方法因为返回了futureTask对象,而当进行future.get()时,会把线程中的异常抛出,因此调用者可以方便地处理异常。(如果是execute,只能用内部捕捉或者设置catchHandler)

Q:线程池中, shutdown、 shutdownNow、awaitTermination的区别?#

A:

  • shutdown: 停止接收新任务,等待所有池中已存在任务完成( 包括等待队列中的线程 )。异步方法,即调用后马上返回。
  • shutdownNow: 停止接收新任务,并 停止所有正执行的task,返回还在队列中的task列表 。
  • awaitTermination: 仅仅是一个判断方法,判断当前线程池任务是否全部结束。一般用在shutdown后面,因为shutdown是异步方法,你需要知道什么时候才真正结束。

Thread状态转换#

Q: 线程的6种状态是:#

A:

  • New: 新建了线程,但是还没调用start
  • RUNNABLE: 运行, 就绪状态包括在运行态中
  • BLOCKED: 阻塞,一般是因为想拿锁拿不到
  • WAITING: 等待,一般是wait或者join之后
  • TIMED_WAITING: 定时等待,即固定时间后可返回,一般是调用sleep或者wait(时间)的。
  • TERMINATED: 终止状态。

欣赏一幅好图,能了解调用哪些方法会进入哪些状态。
image.png

原图链接

Q: java线程什么时候会进入阻塞(可能按多选题考):#

A:

  • sleep
  • wati()挂起, 等待获得别的线程发送的Notify()消息
  • 等待IO
  • 等待锁

Volatile#

用volatile修饰成员变量时, 一旦有线程修改了变量,其他线程可立即看到改变。


Q: 不用volatile修饰成员变量时, 为什么其他线程会无法立即看到改变?#

A:
线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。
这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值。


Q: 用了volatile是不是就可以不用加锁啦?#

A: 不行。

  • 锁并不是只保证1个变量的互斥, 有时候是要保证几个成员在连续变化时,让其他线程无法干扰、读取。
  • 而volatile保证1个变量可变, 保证不了几个变量同时变化时的原子性。

Q:展示一段《Java并发编程实战》书里的一个经典例子,为什么下面这个例子可能会死循环,或者输出0?#

image.png

A:
首先理解一下java重排序,可以看一下这篇博文:
https://www.cnblogs.com/coshaho/p/8093944.html

然后分析后面那2个奇怪的情况是怎么发生的。

  • 永远不输出:
    经过程序的指令排序,出现了这种情况:
  1. ReaderThread在while里读取ready值, 此时是false, 于是存入了ReaderThread的寄存器。
  2. 主线程修改ready和number。
  3. ReaderThread没有感知到ready的修改(对于ReaderThread线程,感知不到相关的指令,来让他更新ready寄存器的值),因此进入死循环。
  • 输出0
    经过程序的指令排序,出现了这种情况:
    1)主线程设置ready为true
    2)ReaderThread在while里读取ready值,是true,于是退出while循环
  1. ReaderThread读取到number值, 此时number还是初始化的值为0,于是输出0
  2. 主线程这时候才修改number=42,此时ReaderThread已经结束了!

上面这个问题,可以用volatile或者加锁。当你加了锁时, 如果变量被写了,会有指令去更新另一个寄存器的值,因此就可见了。


Q: volatile变量如果定义的太多会发生什么?#

A:
volatile有嗅探机制,如果定义过多,可能会引发总线风暴,导致性能下降。


线程群组#

为了方便管理一批线程,我们使用ThreadGroup来表示线程组,通过它对一批线程进行分类管理
使用方法:

1
2
Thread group = new ThreadGroup("group");
Thread thread = new Thread(gourp, ()->{..});

即thread除了Thread(Runable)这个构造方法外,还有个Thread(ThreadGroup, Runnable)构造方法


Q:在线程A中创建线程B, 他们属于同一个线程组吗#

A:
是的


线程组的一大作用是对同一个组线程进行统一的异常捕捉处理,避免每次新建线程时都要重新去setUncaghtExceptionHandler。即线程组自身可以实现一个uncaughtException方法。

1
2
3
4
5
6
7
ThreadGroup group = new ThreadGroup("group") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println(thread.getName() + throwable.getMessage());
}
};
}

线程如果抛出异常,且没有在线程内部被捕捉,那么此时线程异常的处理顺序是什么?
相信很多人都看过下面这段话,好多讲线程组的博客里都这样写:
(1)首先看看当前线程组(ThreadGroup)有没有父类的线程组,如果有,则使用父类的UncaughtException()方法。
(2)如果没有,就看线程是不是调用setUncaughtExceptionHandler()方法建立Thread.setUncaughtExceptionHandler实例。如果建立,直接使用它的UncaughtException()方法处理异常。
(3)如果上述都不成立就看这个异常是不是ThreadDead实例,如果是,什么都不做,如果不是,输出堆栈追踪信息(printStackTrace)。

来源:
https://blog.csdn.net/qq_43073128/article/details/90597006
https://blog.csdn.net/qq_43073128/article/details/88280469


好,别急着记,先看一下下面的题目,问输出什么:
Q:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 父类线程组
static class GroupFather extends ThreadGroup {
public GroupFather(String name) {
super(name);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println("groupFather=" + throwable.getMessage());
}
}

public static void main(String[] args) {
// 子类线程组
GroupFather groupSon = new GroupFather("groupSon") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println("groupSon=" + throwable.getMessage());
}
};


Thread thread1 = new Thread(groupSon, ()->{
throw new RuntimeException("我异常了");
});
thread1.start();
}

A:
一看(1),那是不是应该输出groupFather?
错错错,输出的是groupSon这句话在很多地方能看到,但没有去实践过看过源码的人就会这句话被误导。
实际上父线程组不是指类继承关系上的线程组,而是指下面这样的:
image.png

即指的是构造关系的有父子关系。
如果子类的threadGroup没有去实现uncaughtException方法,那么就会去构造参数里指定的父线程组去调用方法。


Q: 那我改成构造关系上的父子关系,下面输出什么?#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
// 父线程组
ThreadGroup groupFather = new ThreadGroup("groupFather") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println("groupFather=" + throwable.getMessage());
}
};

// 子线程组,把groupFather作为parent参数
ThreadGroup groupSon = new ThreadGroup(groupFather, "groupSon") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println("groupSon=" + throwable.getMessage());
}
};

Thread thread1 = new Thread(groupSon, ()->{
throw new RuntimeException("我异常了");
});

thread1.start();
}

A:
答案输出
image.png

即只要子线程组有实现过,则会用子线程组里的方法,而不是直接去找的父线程组!


Q:如果我让自己做set捕捉器的操作呢?那下面这个输出什么?#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
// 父线程组
ThreadGroup group = new ThreadGroup("group") {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
System.out.println("group=" + throwable.getMessage());
}
};

// 建一个线程,在线程组内
Thread thread1 = new Thread(group, () -> {
throw new RuntimeException("我异常了");
});

// 自己设置setUncaughtExceptionHandler方法
thread1.setUncaughtExceptionHandler((t, e) -> {
System.out.println("no gourp:" + e.getMessage());
});

thread1.start();
}

A:
看之前的结论里,似乎是应该输出线程组的异常?
但是结果却输出的是:
image.png

也就是说,如果线程对自己特地执行过setUncaughtExceptionHandler,那么有优先对自己设置过的UncaughtExceptionHandler做处理。

那难道第(2)点这个是错的吗?确实错了,实际上第二点应该指的是全局Thread的默认捕捉器,注意是全局的
实际上那段话出自ThreadGroup里uncaughtException的源码:
image.png

这里就解释了之前的那三点,但是该代码中没考虑线程自身设置了捕捉器


修改一下之前的总结一下线程的实际异常抛出判断逻辑:#

  1. 如果线程自身有进行过setUncaughtExceptionHandler,则使用自己设置的按个。
  2. 如果没设置过,则看一下没有线程组。并按照以下逻辑判断:
    如果线程组有覆写过uncaughtException,则用覆写过的uncaughtException
    如果线程组没有覆写过,则去找父线程组(注意是构造体上的概念)的uncaughtException方法。
  3. 如果线程组以及父类都没覆写过uncaughtException, 则判断是否用Thread.setDefaultUncaughtExceptionHandler(xxx)去设置全局的默认捕捉器,有的话则用全局默认
  4. 如果不是ThreadDeath线程, 则只打印堆栈。
  5. 如果是ThreadDeath线程,那么就什么也不处理。

[toc]

反射#

Q: 调用类对象.class 和 forName(类名)的区别?#

1
Class<A> classA = A.class;
1
Class<A> classA = Class.forName("A");

A: 仅使用.class不能进行第一次静态初始化, forname函数则可以


例如B是A的基类,下面这段代码如何?
假设有父子2个类,如下:

1
2
3
static class Parent { }

static class Son extends Parent{}

Q: 用instanceof 可以和父类比较吗,且会返回true吗?#

1
2
3
4
Son son = new Son();
if (son instanceof Parent) {
System.out.println("a instanof B");
}

A: 可以比较,且返回true。


Q: 用getClass并用== 可以和父类比较吗,且会返回true吗#

比如下面这样, 注意A是B的子类。

1
2
3
4
Son son = new Son();
if (son.getClass() == Parent.class){
System.out.println("son class == Parent.class");
}

A: 不可以,编译就会报错了。和Class<泛型>的 ==号比较有关。
image.png

因为getClass返回的是<? extends Son>, .class返回的是Class<Parent>


Q: 用getClass并用.equals可以和父类比较吗,且会返回true吗,下面这样:#

1
2
3
4
    Son son = new Son();
if (son.getClass().equals(Parent.class)){
System.out.println("son class.equals(Parent.class)");
}

A: 可以比较,正常编译, 但是会返回false,即不相等!


Q: getDeclaredXXX 有哪几种?#

A: 5种:

  • 注解Annotation
  • 内部类Classed
  • 构造方法Construcotor
  • 字段Field
  • 方法Method
    image.png

Q:getMethods()返回哪些方法, getDeclaredMethods()会返回哪些方法?#

A:
getMethods()返回 本类、父类、父接口 的public方法
getDeclaredMethods()只 返回本类的 所有 方法

其他getXXX和getDeclaredXXX的区别同理。


拿到Filed、Method、Constructor之后咋用

  • Method可以invoke(object, args)
  • Constructor可以newInstance(Object…)来做构造调用。
  • Filed可以用get(object)、set(object)来设置属性值。

Q: 反射拿到Method对象后, 该对象.getModifiers() 是干嘛的?#

A: 返回该方法的修饰符,并且是1个整数。
image.png


Q:下面这样对一个无默认构造的类执行newInstance会发生什么?#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.huawei.test

public class A {
public A(int i ) {
System.out.printf("i=" +i);
}

public static void main(String[] args) {
try {
A a = (A)Class.forName("com.huawei.test.A").newInstance();
} catch (ClassNotFoundException e) {
System.out.printf("ClassNotFoundException");
} catch (InstantiationException e) {
System.out.printf("InstantiationException");
} catch (IllegalAccessException e) {
System.out.printf("IllegalAccessException");
}
}
}

A:
打印InstantiationException初始化错误。
因为A没有默认构造器了,所以不可以用newInstance来构造。
应该改成这样,通过获取正确的构造器来进行构造。

1
A a = (A)Class.forName("A").getConstructor(int.class).newInstance(123);

Q:如何提高反射的效率?#

A:

  • 使用高性能反射包,例如ReflectASM
  • 缓存反射的对象,避免每次都要重复去字节码中获取。(缓存!缓存!)
  • method反射可设置method.setAccessible(true)来关闭安全检查。
  • 尽量不要getMethods()后再遍历筛选,而直接用getMethod(methodName)来根据方法名获取方法
  • 利用hotspot虚拟机中的反射优化技术(jit技术)
    参考资料:
    https://segmentfault.com/q/1010000003004720
    https://www.cnblogs.com/coding-night/p/10772631.html


Q:用反射获取到的method对象, 是返回一个method引用,还是返回1个拷贝的method对象?#

A:
反射拿method对象时, 会做一次拷贝,而不是直接返回引用,因此最好对频繁使用的同一个method做缓存,而不是每次都去查找。
image.png


Q:getMethods()后自己做遍历获取方法和getMethod(methodName) 直接获取方法, 为什么性能会有差异?#

A:
getMethods() 返回method数组时,每个method都做了一次拷贝。
getMethod(methodName)只会返回那个方法的拷贝, 性能的差异就体现在拷贝上。
image.png


Q:获取方法时,jvm内部其实有缓存,但是返回给外部时依然会做拷贝。那么该method的缓存是持久存在的吗?#

A:
不是持久存在的,内存不足时会被回收。
源码如下:
image.png

1
2
3
4
5
6
7
private Class.ReflectionData<T> reflectionData() {
SoftReference<Class.ReflectionData<T>> reflectionData = this.reflectionData;
int classRedefinedCount = this.classRedefinedCount;
Class.ReflectionData rd;
return reflectionData != null && (rd = (Class.ReflectionData)reflectionData.get()) != null
&& rd.redefinedCount == classRedefinedCount ? rd : this.newReflectionData(reflectionData, classRedefinedCount);
}

image.png

可以看到这是一个软引用。
软引用的定义:
内存紧张时可能会被回收,不过也可以通过-XX:SoftRefLRUPolicyMSPerMB参数控制回收的时机,
只要发生GC就会将其回收
如果reflectionData被回收之后,又执行了反射方法,那只能通过newReflectionData方法重新创建一个这样的对象了


Q: 反射是线程安全的吗?#

A:
是线程安全的。 获取反射的数据时,通过cas去获取。 cas概念可以见多线程一节。
image.png


Q: 反射的具体性能差异在哪?#

a普通方法调用
b反射方法调用
c关闭安全检查的反射方法调用,性能差异如下:
image.png

b反射方法调用和c关闭安全检查的反射方法调用的性能差异在哪?
普通方法调用和关闭安全检查的反射方法调用的性能差异在哪?
A:

  • 安全检查的性能消耗在于
    ,SecurityManager.checkPermission(SecurityConstants.CHECK_MEMBER_ACCESS_PERMISSION); 这项检测需要运行时申请 RuntimePermission(“accessDeclaredMembers”)。
    所以如果不考虑安全检查, 对反射方法调用invoke时, 应当设置 Method#setAccessible(true)

  • 普通方法和反射方法的性能差异在于

  1. Method#invoke 方法会对参数做封装和解封操作
  2. 需要检查方法可见性
  3. 需要校验参数
  4. 反射方法难以内联
  5. JIT 无法优化

Q: 为什么反射没法做JIT优化呢?#

A:
我们都知道 Java 代码是需要编译才能在虚拟机里运行的,但其实 Java 的编译期是一段不确定的操作过程。因为它可能是一个前端编译器(如 Javac)把 *.java 文件编译成 *.class 文件的过程;也可能是程序运行期的即时编译器(JIT 编译器,Just In Time Compiler)把字节码文件编译成机器码的过程;还可能是静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器码的过程。
其中即时编译器(JIT)在运行期的优化过程对于程序运行来说更重要,Java虚拟机在编译阶段的代码优化就在这里进行,
由于反射涉及动态解析的类型,因此无法执行某些Java虚拟机优化。因此,反射操作的性能要比非反射操作慢,因此应该避免在对性能敏感的应用程序中频繁使用Java反射来创建对象

动态代理#


Q: 讲一下动态代理的作用 以及他和静态代理的区别#

A:

  • 作用

    先弄一个原始接口类Info,里面提供一些接口例如dealInfo()、getInfo()之类的

    然后可以有N个实现了这个接口的各种Info子类。

    我们希望这些Info字类做dealInfo的时候, 都能在调用前后打一下日志。

    但不希望每个子类里强制都添加这个过程。

    所以引入一个代理Proxy, 将Info类放进去代理种执行,无论你放什么info子类进去,调用dealInfo时,都会在调用前后打印日志。

  • 和静态代理的区别:

    静态代理也是给代理传1个对象,然后执行方法时,执行代理对象的方法

    但是有个执行问题:

    如果我们的类需要加100个方法,那么我们在代理中也要加100个方法,里面反复写“啊,我要调用代理对象的方法”, 这太蠢了

    所以引入动态代理, 这样代理和所代理类之间实现了解耦, 没有必要每次改实类时,也要改代理的内容,重复加方法之类的。

    动态代理可以直接根据method去调用,并且还能弄一个自己独有的处理。


Q: 讲一下动态代理怎么用的?#

A:
代理Proxy类必须implements InvocationHandler
要有一个代理成员,在构造器中去传参数绑定这个代理成员
并实现Object invoke(Object object, Method method, Object[] args)
在invoke方法中调用method.invoke(subject, args)
然后真正执行时,
接口Info = (Info)Proxy.newProxyInstance(代理类对象.getClass().getClassLoader(),实际对象.handler.getInterfaces(), 代理类对象.)
那么当我们执行Info类的某些函数时,就是通过代理去执行了。
d190cf550b2ccb10577825cf86f8d3b2a24b99e2


Q: java动态代理的底层实现原理?#

A:
我们可以对InvocationHandler看做一个中介类,中介类持有一个被代理对象
在invoke方法中调用了被代理对象的相应方法。
通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。
代理类调用自己方法时,通过自身持有的中介类对象来调用中介类对象的invoke方法
从而达到代理执行被代理对象的方法。
也就是说,动态代理通过中介类实现了具体的代理功能。
d2cee51a51bbbd8be3c5315d2b39b1c664497474