记录JAVA一些容易疏忽基础问题
推荐工具库
GUAVA
https://github.com/google/guava
Overview (Guava: Google Core Libraries for Java HEAD-jre-SNAPSHOT API)
HUTOOL
基础
Q 对象在内存的结构
原文:https://www.cnblogs.com/jiangyang/p/11422732.html
Q 方法调用栈
竖版
横版
Q 查看对象的占字节数(byte)
jdk8: long i = ObjectSizeCalculator.getObjectSize(obj);
8bit(位)=1Byte(字节)
1024Byte(字节)=1KB
1024KB=1MB
1024MB=1GB
Q 基本变量声明注意
eg1: float f=3.4,错误。双精度转单精度,下转型,需强转。float f =(float)3.4; 或者写成float f =3.4F;
eg2: short s1 = 1; s1 = s1 + 1。错误,理由同上。short s1 = 1; s1 += 1。正确,因为s1+= 1;
相当于s1 = (short)(s1 + 1),其中有隐含的强制类型转换。
eg3: long w=12345678901。错误。超出了int类型的取值范围。正确: long w=12345678901L
Q 类型转换与实例化
推荐使用类的valueOf方法实例化,而不是直接调用构造函数。 例如:Integer.valueOf(),BigDecimal.valueOf(),String.valueOf(),包装类等
Q 如果int x=20, y=5,则语句System.out.println(x+y +""+(x+y)+y); 的输出结果是
答案:25255
Q 如果int x=20,则语句System.out.println(x++ + ++x +x); 的输出结果是
答案:20+22+22=64,前取值在运算/先运算在取值
Q char可以保存中文?
答案:占2字节,只能保存一个中文,char c = '我'; 注意不能用""
Q.以下代码的输出结果是?
public class Test{
static{
int x=5;
}
static int x,y;
public static void main(String args[]){
x--;
myMethod( );
System.out.println(x+y+ ++x);
}
public static void myMethod( ){
y=x++ + ++x;
}
}
答案:3 ,此题坑是静态代码块中是局部变量x,不是静态变量x
Q.下面这三条语句
1 2 3 |
|
的输出结果分别是? is 1005, 105 is, is 105
Q 输出结果
public class Inc {
public static void main(String[] args) {
Inc inc = new Inc();
int i = 0;
inc.fermin(i);
i= i ++;
System.out.println(i);
}
void fermin(int i){
i++;
}
}
答案:0,此题考值传递,坑 i= i ++; 先取值在运算
Q 以下代码输出的是
public class SendValue{
public String str="6";
public static void main(String[] args) {
SendValue sv=new SendValue();
sv.change(sv.str);
System.out.println(sv.str);
}
public void change(String str) {
str="10";
}
}
答案:"6" 值传递
Q 输出结果
public class StringDemo{
private static final String MESSAGE="taobao";
public static void main(String [] args) {
String a ="tao"+"bao";
String b="tao";
String c="bao";
System.out.println(a==MESSAGE);
System.out.println((b+c)==MESSAGE);
}
}
答案:true false; 编译时"tao"+"bao"将直接变成"taobao",b+c则不会优化,因为不知道在之前的步骤中bc会不会发生改变,而针对b+c则是用语法糖,新建一个StringBuilder来处理
Q.hashcode问题
两个对象相等equals
(),必须有相同的hashcode 值,反之不一定。同一对象的 hashcode 值不变
两个不相等!equals
()的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。
所以其get方法是先判断hashcode相同,若相同还要判断equals。
Q 取模Hash分片,一致性Hash分片
这两种算法都是通过数据的hash来给数据分片,从而达到分治、分段、分库分表的效果。
但是一致性Hash算法,后期的扩容成本和数据迁移成本要更好。其扩容后只需要迁移少量的数据。
而取模算法扩容后,则会影响到所有的数据分片,需要大量的数据迁移,才能最终完善。
通过取模法 实现最简单的分表
index = hash(sharding字段) % 分表数量 ;
select xx from 'busy_'+index where sharding字段 = xxx;
Q.哪些是不合法的定义?
public class Demo{
float func0()
{
byte i=1;
return i;
}
float func1()
{
int i=1;
return;
}
float func2()
{
short i=2;
return i;
}
float func3()
{
long i=3;
return i;
}
float func4()
{
double i=4;
return i;
}
}
答案:func1、func4,重点是类型转换图。哪些可以自动转换,哪些需要强制转换
Q 位运算更高效
<< n 乘2的N次方 >> n 除2的N次方
Q JAVA引用类型
依次减弱
Q final关键字
其可以作用在类、方法、成员变量之上。
final成员变量是不可变的:基本变量不可变,引用变量其引用不可变,但是引用的对象内部状态是不受限制的。
final变量不可变所以其是绝对的线程安全的,也是可以保证安全发布的。
final变量必须保证最晚在构造函数中赋值/初始化,不要忘记无参构造函数也要初始化final变量
public class SimLockInfo{
private final Long sn;
private final AtomicInteger retry;
public SimLockInfo() {
//初始化final变量
retry = null;
sn = null;
}
public SimLockInfo(Integer dhhm, Long sn, String id, Boolean get, Long begintime) {
this.sn = sn;
this.retry=new AtomicInteger(0);
}
}
Q exception.printStackTrace(),System.out.println()的隐患
当高并发出现大量异常,且方法栈非常深时,需要字符串常量池所在的内存块有足够的空间,可能会耗尽内存。造成GC、等待内存释放假死、OOM等问题。推荐使用log记录部分异常信息到文件。
System.out.println底层源码是持锁执行,性能有影响。
Q url或请求体参数中带有%22等符号
url中的20%、22%、26%、28%、29%怎么解析还原成真实的字符_%26-CSDN博客
Q 判断文件相同
上传后保存文件的原文件名、服务器分配名、真实路径、URL、MD5、SHA1、大小等信息
1、通过MD5、SHA1对比两个文件。
单独的MD5存在很小概率的错判,尤其是大文件。可以结合MD5、SHA1、大小因素等一起对比。
2、统一管理业务对文件的引用URL
防止http请求数据被篡改
1 https
2 加签名:发送端把重要参数组合起来计算签名,服务端把同样的参数组合起来计算签名,匹配计算签名与收到签名是否匹配
优雅停机ShutdownHook
https://segmentfault.com/a/1190000020657101?utm_source=tag-newest
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("关闭应用,关闭线程池,释放资源");
}));
模式、结构与思想
提供出入API或读取方法,实现通过外部信息动态创建与修改JVM内存对象
在JVM中一切都是对象。所以外部输入、读取的数据需要转化为jvm对象类模型。数据转化为内存对象进入系统,对象的方法实现业务功能,对象也可以转化为数据进行传递或持久化。Object是相互转化的中间枢纽。
所以开发人员需要提供对外接口实现动态创建\修改JVM中Object的功能。以实现数据加载、动态配置等功能。
数据库数据(ORM 1 Row==1 Object)
配置信息 <----->开放API(输入、读取)<---->创建/修改 JVM[Object] ---->引用/容器保存
外部请求参数
|
|
前端展示(json)
以最熟悉的SpringMvc RestfulAPI为例 : 通过对外API,动态修改系统配置
//通过对外API,动态修改系统配置
@PostMapping(value = "/updateconfig")
@ResponseBody
public Boolean updateConfig( @RequestParam("id") String id, @RequestBody Info info){
//实例化配置对象
Config c=new Config(info);
c.setId(id);
//替换当前系统中配置
configMap.put(id,c);
//DB持久化配置信息。下次启动系统时从DB读取,创建配置对象存入configMap
configMapper.update(id,c);
return true;
}
例如:
1、下文中JDKSecKill的例子,系统提供API让操作人输入数据,API根据输入数据在JVM中创建商品对象和货仓对象。
2、SpringIOC环境中将每一个配置的bean信息解析后创建BeanDefinition对象保存在Map,创建bean时通过BeanDefinition创建。
3、SpringGateWay中将路由配置解析后创建RouteDefinition对象保存在Map。
4、Mybatis将所有xml配置sql语句创建成MappedStatement。
5、Tomcat将http实例化成HttpRequest对象
性能优化思想
同步转异步、串行转并行、空间换时间、分而治之、横向扩容消除单点瓶颈
分治和异步思想
无论是算法、数据结构、系统架构都是非常核心重要的思想。将整体拆分成单元、将同步串行分解成异步并行。
比如:Fork/Join框架、快速\归并排序、树\hashmap结构、分段锁、分库分表、MQ、集群、分布式等等。
充分利用内存
提高响应速度的关键,减少IO操作。本地缓存、Redis。
单例和享元模式
节省内存开销尽量使用单例模式创建功能类对象,配合享元模式使其共享。注意线程安全。
比如:spring-bean默认都是单例
实现链式调用
方法1:数组/链表/栈+循环/递归([i++]/next) ,链上所有对象都实现同一个接口和方法 。
例如:SpringMvc-HandlerExecutionChain、Spring-BeanPostProcess、SpringAOP生成的代理对象、Dubbo-ProtocolFilterWrapper#buildInvokerChain等。
每次执行前组装执行链,有序的装配元素进入数组和index=0游标。链中每个元素实现同一个接口和方法,接口方法入参中有chain,代码在入栈/出栈是执行取决于代码写在调用下层接口方法前/后。
@Override
public void doFilter(Request request, Response response,FilterChain chain) {
i++; //前置执行
chain.next(request,response,chain); //执行下一个Filter
i--; //后置执行
}
比如:拦截过滤器模式、责任链模式、过滤器链模式(如下例1)、Dubbo中执行链(如下例2)
例1:执行链实现
public class FilterChain implements Filter{
//当前链的所有过滤器
List<Filter> fs = new ArrayList<Filter>();
//当前游标
int index = 0;
public FilterChain addFilter(Filter f) {
//添加过滤器
fs.add(f);
return this;
}
@Override
public void doFilter(Request request, Response response,FilterChain chain) {
//chain就是this
if(index == fs.size())
return;
//取出过滤器
Filter nextf = fs.get(index);
index ++;
//向下执行
nextf.doFilter(request, response, chain);
}
public static void main(String[] args) {
String msg = "测试,<script>,被就业,敏感信息";
//为本次调用 实例化过滤器链
FilterChain fc = new FilterChain();
//装配所需的过滤器
fc.addFilter(new HTMLFilter()).addFilter(new SensitiveFilter());
//模拟生成request 、response
Request request = new Request();
Response response = new Response();
.....
//开始执行过滤器链(传入过滤器链fc本身)
fc.doFilter(request, response,fc);
}
}
例2:倒序遍历filters,实例化Invoker匿名类对象,每个Invoker中方法中包含一个filters[i]。倒序先实例化Invoker,作为下一个实例化Invoker的next变量。利用闭包性质为Invoker匿名类对象的方法提供filter、next变量。
private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
//invoker为最后一个
Invoker<T> last = invoker;
//根据URL获取List<Filter>
List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
if (!filters.isEmpty()) {
//倒序遍历filters,实例化Invoker匿名类对象,每个Invoker中包含一个filter[i]
//先实例化最后一个Invoker,作为下一个实例化的Invoker的next
for (int i = filters.size() - 1; i >= 0; i--) {
final Filter filter = filters.get(i);
//记录上次实例化的Invoker,作为本次的next
final Invoker<T> next = last;
last = new Invoker<T>() {
//next为上次实例化的Invoker匿名类对象,其invoke中调用filters[i++].invoke
@Override
public Result invoke(Invocation invocation) throws RpcException {
//filters[i].invoke
return filter.invoke(next, invocation);
}
};
}
}
return last;
}
方法2:为目标方法添加包装器 ->依靠方法栈向下调用至目标方法。例如:AOP、包装模式、代理模式
实现插拔式、开放式、可拓展程序设计,满足开闭原则
基本思想:通过 过滤器链/拦截器/代理/包装器/模板 等方式实现。这样可以将各个子功能独立出来,主流程只关心核心业务代码。方便拓展、维护、复用。
例如:实现鉴权、LOG、统计、限流等功能过滤器,可以按需求为功能添加不同过滤器。且每个过滤器功能更加独立,容易维护、复用方便。
方法1:通过统一接口定义+数组/链表,把同一接口的不同实现类注册到数组/链表中。
利用模板方法模式,在指定流程位置遍历数组/链表,执行不同接口实现。使得程序有更高的拓展。实现功能的插拔式拓展。
接口中可以定义是否支持当前情况的方法,决定当前实现是否需要执行。
例如:过滤器链、SpringMvc-选择HandlerMapping、SpringMvc-选择HandlerMethodArgumentResolver、SpringMvc-HandlerExecutionChain、Spring-BeanPostProcess、Tomcat-valve 等设计
方法2:面相切面设计AOP
惰性思想
避免对定时任务依赖,节省整体资源消耗。但是会增加操作复杂度和用时。
例如:
Redis的TTL部分在get时判断有效性、
令牌桶限流发放令牌在请求令牌同时向桶中装填、
Guava包装式过滤器:不直接修改容器而是包装容器方法在获取元素时判断元素是否合法、
单例懒加载
包装/代理思想
很常用的思路,为原对象增加/强化功能。
例如:用Map实现一个缓存。可以把所有要缓存的对象包装在自定义的Node类中,Map<String,Node> cache。
可以通过Node增加例如判断过期之类的功能...
class Node<T>{
private T value;
private String key;
private DateTime createtime;
//添加属性和功能方法。增强value的作用。
......
}
例如:
为对象生成代理对象,增加对象的功能。
AOP给bean生成动态代理对象,把切面保存在代理对象中的数组中。
Dubbo消费者通过动态代理为服务接口生成动态代理对象调用invoker。生产者端通过javasisst为服务提供者bean生成静态代理对象。
复用、缓存和原型模式
除了最常用map\redis这些做数据缓存。很多框架中还会缓存拼装比较复杂的对象。
比如过滤器链中的过滤器可能一直不变,所以只要第一次拼装完成后就可以缓存起来重复利用。
方法抽取
将一个复杂的方法抽取成多个子方法组合,增加可读性和复用性
第三方功能组件配合适配器模式使用
第三方功能最好通过一个适配器类在转化,项目中其他类只依赖这个适配器。
当要替换此底层组件时,只需要修改或重写适配器即可
String
Q String类为什么是final类。
1.线程安全 :不可变对象,任何线程拿到String引用后都不能改变其值、除非改变其引用。
2.支持字符串常量池数据共享,节省资源,提高效率(因为如果已经存在这个常量便不会再创建,直接拿来用)
因为是不可变对象,所以hashcode不会改变,String对象缓存了自己的hash值。这样其作为Map的Key时会有更高的效率。
Q String怎么实现不可变?
Java内存管理-探索Java中字符串String(十二)_java string 内存管理-CSDN博客
字符串是常量,它们的值在创建之后不能更改。可以看到字符串的更改相关方法是返回新的字符串实例,而不是更改当前字符串的value[], String是不可变的关键都在底层的实现,而不是只是一个final。
//部分源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//拼接
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
// 重新创建一个新的字符串
return new String(buf, true);
}
}
Q String不可变测试。
@Test
public void str(){
getstr("s",1);
}
private void getstr(String s, int i){
if (i>2)
return ;
System.out.println(i+"层递归进入前:"+s);
getstr(s+"s",i+1); //s不可变,+拼接返回new String。下层引用的是new String
System.out.println("递归返回到"+i+"层:"+s);
}
---输出结果
1层递归进入前:s
2层递归进入前:ss
递归返回到2层:ss
递归返回到1层:s
Q String常量池的意义。
减少字符串的创建减少内存开销。假如没有字符串常量池,下面这个方法每次创建方法栈帧都要实例化两个字符串,但是通过常量池就可以避免这种问题。
public void getName(){
String s="123";
String name=getInfo("NAME",s)
}
Q String.intern()
根据JDK1.8源码中英文说明,此方法返回常量池中与当前字符串equals()的字符串引用。若池中没有则添加后再返回。
String s="123"; //编译器优化到常量池
String s1=new String("123"); //堆中新建实例
System.out.println(s.equals(s1)); //true
System.out.println(s==s1.intern()); //true intern返回s1常量池中equals()的引用
System.out.println(s==s1); //false 引用不同
s1=s1.intern();
System.out.println(s==s1); //true 引用相同
HotSpot VM 在JDK 8 以前版本方法区(包含运行时常量池,其中就有字符串常量池)都是用的堆中的永久代(permanent generation)来实现,JDK 8及以后开始使用元空间(MetaSpace)来实现.
字符串常量池中保存的是多个相同的String实例首个被intern的那个实例的引用,比如有多条语句声明了String实例"a",调用了intern()方法时,返回的是常量池中保存的是首次出现的"a"的引用.
//JDK1.8
String s1 = new StringBuilder("12").append("ab").toString();
System.out.println(s1.intern() == s1); //true ,"12ab"首次intern(),所以常量池记录s1引用
String s3 = new StringBuilder().append("12ab").toString();
System.out.println(s3.intern() == s3); //false,常量池中已存在s1引用,返回s1引用
数据类型
Q 判断integer
int h=9;
double j=9.0;
Integer a=new Integer(9);
Integer b=new Integer(9);
Integer c = 9;
Integer d = 9;
Double i= 9.0;
Integer e =Integer.valueOf(9);
Integer k =Integer.valueOf("9"); //类型转换 用包装类完成
int l=Integer.valueOf(9).intValue();
Integer f = 128;
Integer g = 128;
答案:
h==j;//true
a==h;//true 自动拆箱
a==b;//false
a==c;//false
c==d;//true
c==e;//true
f==g;//false
i.equals(h)//false
i.equals(j)//true
1、基本型和基本型封装型进行“==”运算符的比较,基本型封装型将会自动拆箱变为基本型后再进行比较,因此Integer(0)会自动拆箱为int类型再进行比较,显然返回true;
int a = 220;
Integer b = 220;
System.out.println(a==b);//true
2、两个Integer类型进行“==”比较, 如果其值在-128至127 ,那么返回true,否则返回false, 这跟Integer.valueOf()的缓冲对象有关,这里不进行赘述。
Integer c=3;
Integer h=3;
Integer e=321;
Integer f=321;
System.out.println(c==h);//true
System.out.println(e==f);//false
3、两个基本型的封装型进行equals()比较,首先equals()会比较类型,如果类型相同,则继续比较值,如果值也相同,返回true。
Integer a=1;
Integer b=2;
Integer c=3;
System.out.println(c.equals(a+b));//true
4、基本型封装类型调用equals(),但是参数是基本类型,这时候,先会进行自动装箱,基本型转换为其封装类型,再进行3中的比较。
int i=1;
int j = 2;
Integer c=3;
System.out.println(c.equals(i+j));//true
Q 设有下面两个赋值语句:a,b类型?
a = Integer.parseInt("1024");
b = Integer.valueOf("1024").intValue();
答案:a和b都是整数类型变量并且它们的值相等。
Q 有如下4条语句
Integer i01 = 59;
int i02 = 59;
Integer i03 =Integer.valueOf(59);
Integer i04 = new Integer(59);
System.out.println(i01 == i02); //true Integer 会自动拆箱成 int ,然后进行值的比较
System.out.println(i01 == i03); //true Integer内部缓存
System.out.println(i03 == i04); //false
System.out.println(i02 == i04); //true Integer 会自动拆箱成 int ,然后进行值的比较
Q 敏感的小数不要使用double和float
这两种类型容易引发精度丢失,或出现比较误差,例如:1.0-0.9=0.09999999....
方案一:所有金额以分为单位,用long记录。
若是在遇到除不尽问题,则最后做补偿处理。
方案二:使用BigDecimal类型做小数计算。必须由String转化
若BigDecimal与Double等其他类型比较,请将Double转化为BigDecimal,然后用BigDecimal.compareTo做比较。
但是以上方案还有可能出现0.1与0.10比较有误差的情况,或是Double的0.1转BigDecimal后出现0.100001的情况,此时可以用减法计算,若差值小于一个阈值时认定为相等。
推荐先将double转String在转BigDecimal
Java BigDecimal详解_bigdecimal负数相加-CSDN博客
============类型转换================
System.out.println(new BigDecimal(1.22));
//1.2199999999999999733546474089962430298328399658203125
System.out.println(new BigDecimal(String.valueOf(1.22)));
//1.22
============计算2/3=================
float f1= 2/3;
System.out.println(f1);
//0.0 计算错误
float f= (float) (new Double(2)/3);
System.out.println(f);
//0.6666667
BigDecimal s = new BigDecimal("2");
System.out.println(s.divide(new BigDecimal("3"), 5 ,BigDecimal.ROUND_HALF_UP));
//0.66667 注意除不尽时一定要设置divide方法的保留位数和四舍五入策略,否则抛出异常
以上方案同样适用于数据库。
BigDecimal
Java BigDecimal详解_bigdecimal负数相加-CSDN博客
Java中的BigDecimal类使用_import bigdecimal-CSDN博客
BigDecimal加减乘除计算_bigdecimal加减乘除运算顺序-CSDN博客
(1)商业计算使用BigDecimal。
(2)尽量使用参数类型为String的构造函数。
推荐使用BigDecimal.valueOf(),其底层源码实现先转换String。在实例化BigDecimal。
(3)2.0 not equals 2.00,2.0 compareTo 2.00 == 0.。请看源码注释,比较相等请用compareTo。
(4) BigInteger与BigDecimal都是不可变的(immutable),在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(5)所有加减乘除运算,都不是修改当前 BigDecimal对象 而是将结果作为新对象返回,与(4)相关
BigDecimal a= new BigDecimal("1.22");
a.divide(new BigDecimal(2));
System.out.println(a);
//1.22
a=a.divide(new BigDecimal(2));
System.out.println(a);
//0.61
常用API
add(BigDecimal) BigDecimal对象中的值相加,然后返回这个对象。
subtract(BigDecimal) BigDecimal对象中的值相减,然后返回这个对象。
multiply(BigDecimal) BigDecimal对象中的值相乘,然后返回这个对象。
divide(BigDecimal) BigDecimal对象中的值相除,然后返回这个对象。
toString() 将BigDecimal对象的数值转换成字符串。
doubleValue() 将BigDecimal对象中的值以双精度数返回。
floatValue() 将BigDecimal对象中的值以单精度数返回。
longValue() 将BigDecimal对象中的值以长整数返回。
intValue() 将BigDecimal对象中的值以整数返回。
类初始化
Q.以下代码的错误在哪里?
public class Test {
public static int i=1;
static {
i=2;
j=2;
System.out.println(i);
System.out.println(j);
}
public static int j=1;
}
答案: 只有System.out.println(j)会编译失败。 static代码在类加载的过程中执行, 且类加载是按照书写顺序加载代码。
"j" 在静态代码快中,只能赋值不能调用。
Q.以下代码的输出结果是?
public class B
{
public static B t1 = new B();
public static B t2 = new B();
{
System.out.println("构造块");
}
static
{
System.out.println("静态块");
}
public static void main(String[] args)
{
B t = new B();
}
}
答案:构造块 构造块 静态块 构造块
静态块只会在首次加载类时执行,在类第一次加载时已经确定首次,即使未执行完也是首次加载。
Q.运行下面代码,输出的结果是
class A {
public A() {System.out.println("class A");}
{ System.out.println("I'm A class"); }
static { System.out.println("class A static"); }
}
public class B extends A {
public B() { System.out.println("class B"); }
static { System.out.println("class B static"); }
{ System.out.println("I'm B class"); }
public static void main(String[] args) {
new B();
}
}
答案:只要说明父子类初始化顺序,父静-子静-父普-子普
父静态成员初始化 -- 父静态代码块 -- 子静态成员初始化 -- 子静态代码块--父成员变量初始化 -- 父代码块 --父构造器-- 子成员初始化 -- 子代码块--子构造器
class A static
class B static
I'm A class
class A
I'm B class
class B
Q.运行下面代码,输出的结果是
public class Cshsx {
private int i = 1;
public static int s = 2;
static {
System.out.println("静态代码块");
System.out.println(s);
}
{
System.out.println("代码块");
System.out.println(i);
System.out.println(s);
}
public static void main(String[] args) {
new Cshsx();
}
}
答案:主要是说明成员变量的初始化和赋值,在相应的代码块之前执行
静态代码块
2
代码块
1
2
Q.运行下面代码,输出的结果是
class X{
Y y=new Y();
public X(){
System.out.print("X");
}
}
class Y{
public Y(){
System.out.print("Y");
}
}
public class Z extends X{
Y y=new Y();
public Z(){
System.out.print("Z");
}
public static void main(String[] args) {
new Z();
}
}
答案:YXYZ
继承
方法中的变量x若没有特殊标志(super/this),默认找离本次调用最近的, 先找调用方法局部变量x,再找本类x(调用方法所在类),再找this.x,再找super.x。
Q.以下代码执行的结果显示是?
public class Demo {
class Super{
int flag=1;
Super(){
test();
}
void test(){
System.out.println("Super.test() flag="+flag);
}
}
class Sub extends Super{
Sub(int i){
flag=i;
System.out.println("Sub.Sub()flag="+flag);
}
void test(){
System.out.println("Sub.test()flag="+flag);
}
}
public static void main(String[] args) {
new Demo().new Sub(5);
}
}
答案:
Sub.test() flag=1
Sub.Sub() flag=5
Q.以下代码执行的结果显示是?
public class Son extends Father {
public int a = 1;
private int i = 1;
@Override
public String getMsg() {
return "son";
}
public int getSuperA() {
return super.a;
}
public static void main(String[] args) {
Son son = new Son();
Father son1=son;
System.out.println(son1.getMsg());
System.out.println(son.getSuperA());
System.out.println(son.getA());
System.out.println(son.getI());
son.setI(100);
System.out.println(son.getI());
}
}
public class Father {
public int a = 0;
private int i = 0;
public String getMsg() {
return "father";
}
public int getI() {
return this.i;
}
public void setI(int i) {
this.i = i;
}
public int getA() {
return this.a;
}
public void setA(int a) {
this.a = a;
}
}
结果:
son
0
0
0
100
Son没有重写的方法其调用时取父类方法
Q.以下代码执行的结果显示是?
public class Main
{
private String baseName = "base";
public Main()
{
callName();
}
public void callName()
{
System.out.println(baseName);
}
static class Sub extends Main
{
private String baseName = "sub";
public void callName()
{
System.out.println (baseName) ;
}
}
public static void main(String[] args)
{
Main b = new Sub();
}
}
答案:null,子类构造器执行super(),由于子类中存在变量baseName和方法callName,由于动态绑定和覆盖,super()调用的是子类方法,子类方法调用子类变量。但是此时子类非静态成员变量 baseName只完成初始化null,还没有赋值"sub"。
继承中:只有子类可见的同名方法会被子类覆盖,但是父类方法还是存在的,可以在子类中通过super调用,父类方法中会访问父类变量。所以所有的 变量+方法 都是父子相互独立共存。
静态内容根据当前对象的父/子引用变量类型,调用对应类的静态内容。非静态内容动态绑定。(static,final,private,构造器是静态绑定)
//即使覆盖了父类方法,父类方法依然存在
@Override
public String getMsg() {
//子类方法调用父类方法
return super.getMsg();
}
Q.判断对错。在java的多态调用中,new的是哪一个类就是调用的哪个类的方法。
答案:错,动态绑定/静态绑定
final,static,private,构造器 是静态绑定方法不能覆盖
子类可见的非静态方法可以覆盖(三同一大一小原则),运用的是动态单分配,是根据new的类型确定对象,从而确定调用的方法;
静态方法同名独立共存不覆盖,运用的是静态多分派,即根据静态类型确定对象(即引用变量类型,决定调用哪个类的静态方法),因此不是根据new的类型确定调用的方法
Q.以下代码执行的结果是多少
public class Demo {
public static void main(String[] args) {
Collection<?>[] collections = {new HashSet<String>(), new ArrayList<String>(), new HashMap<String, String>().values()};
Super subToSuper = new Sub();
for(Collection<?> collection: collections) {
System.out.println(subToSuper.getType(collection));
}
}
abstract static class Super {
public static String getType(Collection<?> collection) {
return “Super:collection”;
}
public static String getType(List<?> list) {
return “Super:list”;
}
public String getType(ArrayList<?> list) {
return “Super:arrayList”;
}
public static String getType(Set<?> set) {
return “Super:set”;
}
public String getType(HashSet<?> set) {
return “Super:hashSet”;
}
}
static class Sub extends Super {
public static String getType(Collection<?> collection) {
return "Sub"; }
}
}
答案:
Super:collection
Super:collection
Super:collection
考察点1:重载静态多分派——根据传入重载方法的参数类型,选择更加合适的一个重载方法
考察点2:static方法不能被子类覆写,在子类中定义了和父类完全相同的static方法,则父类的static方法被隐藏,Son.staticmethod()或new Son().staticmethod()都是调用的子类的static方法,如果是Father.staticmethod()或者Father f = new Son(); f.staticmethod()调用的都是父类的static方法。
考察点3:此题如果都不是static方法,则最终的结果是A. 调用子类的getType,输出collection
Q.对文件名为Test.java的java代码描述正确的是()
class Person {
String name = "No name";
public Person(String nm) {
name = nm;
}
}
class Employee extends Person {
String empID = "0000";
public Employee(String id) {
empID = id;
}
}
public class Test {
public static void main(String args[]) {
Employee e = new Employee("123");
System.out.println(e.empID);
}
}
答案:编译报错,因为子类构造器中隐式调用super(),但是父类没有无参构造器
子类构造器中会隐式/显示调用父类构造器,但其初始化执行在子类成员变量初始化之前。父类构造器的初始化执行顺序,请看父子类初始化顺序
Q.以下代码执行的结果是?
public class Father {
protected void doSomething() {
System.out.println ("Father doSomething " );
this.doSomething();
}
public static void main(String[] args) {
Father father= new Son();
father.doSomething() ;
}
class Son extends Father {
@Override
public void doSomething() {
System.out.println ("Son ’ s doSomething " );
super.doSomething();
}
}
答案:以下代码会出现栈溢出 StackOverflow Error ,因为doSomething方法会无线循环。
Try catch finally
释放 锁、链接、资源...等一定要放在finally代码
Q 在try的括号里面有return一个值,那在哪里执行finally里的代码?
答案:return前执行
Q下面代码运行结果是()
public class Test{
public int add(int a,int b){
try {
return a+b;
}
catch (Exception e) {
System.out.println("catch语句块");
}
finally{
System.out.println("finally语句块");
}
return 0;
}
public static void main(String argv[]){
Test test =new Test();
System.out.println("和是:"+test.add(9, 34));
}
}
答案:
finally语句块
和是:43
Q 最终输出?
public class Main {
public static int add(){
try {
return 5;
}
catch (Exception e) {
}
finally{
return 10;
}
}
public static void main(String[] args) {
System.out.println(add());
}
}
答案:10
Q 最终输出?
public class Main {
public static int add(){
int a=1;
try {
return a;
}
catch (Exception e) {
}
finally{
a=10;
}
return 0;
}
public static void main(String[] args) {
System.out.println(add());
}
}
答案:1,finally改变不了return过的基本变量
Q 最终输出?
public class Main {
public static String add(){
String a="123";
try {
return a;
}
catch (Exception e) {
}
finally{
a="456";
}
return "";
}
public static void main(String[] args) {
System.out.println(add());
}
}
答案:123 ,finally改变不了return过的引用变量
Q 利用标志变量记录执行进度
public static int p(){
int b=0;
try {
a=a*2;
b++; //更新执行进度
a=a*2;
b++;//更新执行进度
return a;
}
catch (Exception e) {
//根据标志b,回滚a的值
if (b==1){
a=a/2;
}
if (b==2){
a=a/4;
}
}
return 0;
}
异常
Q exception.printStackTrace(),System.out.println()的隐患
当高并发出现大量异常,且方法栈非常深时,需要字符串常量池所在的内存块有足够的空间,可能会耗尽内存。造成GC、等待内存释放假死、OOM等问题。推荐使用log记录部分异常信息到文件。
System.out.println底层源码是持锁执行,性能有影响。
Q.RuntimeException和Exception的区别
非RuntimeException即使throws上抛,最终也必需被catch处理。
RuntimeException允许不catch捕捉,方法也不用throws,其会导致代码运行中断。一般程序里自定义异常继承RuntimeException表示代码级别错误。RuntimeException一般会代表开发人员触发的错误。
public class BaseException extends RuntimeException {
private int status = 200;
public BaseException(String message,int status) {
super(message); //继承父类String message;
this.status = status;
}
getter()+setter();
}
如果代码块会抛出ERROR,请catch(Throwable)
控制台异常打印顺序
CauseBy倒序打印,最下边的CauseBy是最先触发E被打印出来的。每个CauseBy中的异常方法是顺序打印,先看第一个方法
容器(集合)
Q JAVA提供的集合都有哪些,特点是什么,使用场景?
Carson带你学Java:那些关于集合的知识都在这里了!_java集合有关的知识-CSDN博客
JAVA集合框架中的常用集合及其特点、适用场景、实现原理简介_10.说一下所有的集合以及他们的特点和使用场景。-CSDN博客
Q Collections工具类常用API
有序性:
static void sort(List<T> list, Comparator<? super T> c);
static T max(Collection<? extends T> coll, Comparator<? super T> comp);
static T min(Collection<? extends T> coll, Comparator<? super T> comp);
static void reverse(List<?> list); //翻转List
static void shuffle(List<?> list); //乱序List
查找:
static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c);//二分查找
static int frequency(Collection<?> c, Object o); //元素出现次数
static int indexOfSubList(List<?> source, List<?> target); //List子串首次位置
static int lastIndexOfSubList(List<?> source, List<?> target);
复制、替换
static <T> void copy(List<? super T> dest, List<? extends T> src);
static <T> boolean replaceAll(List<T> list, T oldVal, T newVal)
杂项(无交集?、List元素填充)
static boolean disjoint(Collection<?> c1, Collection<?> c2);
static <T> void fill(List<? super T> list, T obj);
包装:
同步包装(包含所有类型,只以Collection为例子)
static <T> Collection<T> synchronizedCollection(Collection<T> c);
类型检查(包含所有类型,只以List为例子)
static <E> List<E> checkedList(List<E> list, Class<E> type);
不可变包装(包含所有类型,只以Collection为例子)
static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
Q 若已知要产生一个要保存大量Object的容器,怎样提高代码执行效率?
答案:初始化容器时,指定初始容量,避免反复扩容。
所有以数组为基础的数据结构都涉及到扩容问题。Arrays.copyOf()
Q 数组结构充分利用下标进行快速位移/查询。
Array、ArrayList...。向左查找 [index-n],向右[index+n]。
Link链表同理。向左两个node.pre.pre,向右node.next
Q HashMap初始化与扩容
构造器可以传入初始化容量和负载因子。hashmap会将构造器传入的初始化容量设置为满足需求的最接近2的N次幂*负载因子。
实例化时只赋值容量和负载因子,在首次put时执行resize扩容创建数组table。
比如:new HashMap(1000);构造完成后实际容量阈值是1024*0.75f,1000转化为2的N次幂再乘以负载因子。
且会实例化table[1024]的数组。
Q HashMap负载因子
负载因子LoadFactor:决定填充比例,默认0.75。Threshold(阈值)=LoadFactor*Capacity(map的容量) ,Threshold为Map的真实容量。
LoadFactor越大利用率越高,越小分布越均匀查找速度越快。
Q HashMap 与 TreeMap 特点
HashMap:更适合作增、删、改、查的存储/缓存载体。(线程安全:ConcurrentHashMap)
TreeMap :利用红黑树有序性,更适合作统计\排序\范围查找等工作。(线程安全:ConcurrentSkipListMap)
有序TreeMap .get(Key)只通过比较器比较==0,不用key的hashcode和equals。
例如:subMap(K fromKey, K toKey)小于toKey大于等于fromKey区间;
headMap(K toKey) 小于toKey区间;
tailMap(K fromKey) 大于toKey区间;
firstKey、lastKey、firstEntry、lastEntry求最大元素和最小元素等;
values()返回以key升序的List。若有经常需要变化的有序List时也可以用TreeMap代替。减少排序的消耗。
Q List中 subList 只是父List的视图,并不是拷贝,修改父或子的内容,相当于全部修改。
且subList 是一种内部类List。可以用List接口的引用变量。
Q 在 subList 场景中,高度注意对父集合元素个数的修改,会导致子集合的遍历、增加、
删除均会产生 ConcurrentModificationException 异常。
Q Arrays.asList()把数组转换成集合,有什么问题?
1 他返回的Arrays中的内部类其实现List接口,可以用List接口的引用变量
2 没有实现List接口的修改方法, 使用add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
若想转化成ArrayList,可以new ArrayList(Arrays.asList());
3 其只是原数组的视图,不是拷贝,修改原数组,其值也改变。
str[0] = "gujin"; 那么 list.get(0)也会随之修改。
Q.List 转数组推荐不要直接用 toArray 无参方法?
因为其只能返回Object[]类,对泛型不友好
//推荐操作,保证数组类型
String[] array = new String[list.size()];
array = list.toArray(array);
//array的大小等于list时效率最高,小于则array所有为NULL,大于效率慢
Q List迭代中修改集合注意?
不要在 for/foreach 循环里进行元素的 remove/add 操作,可能会产生 ConcurrentModificationException 异常,或是难以预测的错误结果。推荐使用 Iterator/ListIterator 接口中的remove/add。注意:Iterator只能next+remove。
在Iterator遍历的过程中,一定要使用Iterator/ListIterator接口中的修改方法。集合自身的修改方法在Iterator外部使用。
Q 容器遍历 fail-fast 和 fail-safe
fail-fast:快速失败。当前线程记录遍历开始的容器修改次数modCount,遍历过程中检查有无变化。有变化说明容器被修改(可能是当前线程也可能是其他线程修改),抛出ConcurrentModificationException。
fail-safe:相当于先拷贝容器快照,遍历这个快照而不再关心容器本身的变化。缺点是可能出现弱一致性问题。JAVA并发包中的所有容器均采用此机制。
Q CopyOnWriteArrayList
适合读多写很少的情况,写操作时加锁顺序执行,复制集合,并在复制集合上添加/删除操作,然后用复制集合替换当前集合的引用。
COW注意:1.设置合理的初始容量;2.修改效率很低,最好能批量添加/删除(addAll或removeAll),可以先写到ArrayList中。
Q List 集合高效遍历?
if (list instanceof RandomAccess) {
//使用传统的for循环遍历。
} else {
//使用Iterator或者foreach。
}
Q.HashMap,自定义类型对象作为KEY,注意点?
答案:自定义类型,最好重写hashcode()和equels()。Map为提高效率会用hashcode()判断KEY是否相等。
如果不重写,默认使用Object类中hashcode()。判断的是KEY内存地址。HashMap允许KEY=NULL。
Q Map 集合高效遍历?
使用 map.entrySet +Iterator遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。JDK8可以使用 Map.foreach 方法。
values()返回的是 Collection<V> 值集合,是一个 List 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
Q Map中的视图keySet, values, entrySet。
Map中保存着 keySet, values, entrySet三个视图。这些返回的视图是支持清除操作(删除元素后,MAP也随之改变),也可以修改元素的状态, 但是增加元素会抛出异常。因为AbstractCollection 没有实现 add 操作 但是实现了 remove 、clear 等相关操作
Q Hashmap 对比 TreeMap
综合推荐 HashMap,其结合了红黑树的优点,且修改效率相对高一些
1.HashMap 使用 hashCode+equals 实现去重的。而 TreeMap 依靠 Comparable+Comparator 来实现 Key 的去重。
2.HashMap插入和删除效率高于TreeMap。因为TreeMap调整红黑树的成本更高。
3.TreeMap的查找效率高于HashMap。因为TreeMap是纯红黑树类型。HashMap是哈希表+红黑树
Q List 集合快速去重复?
//借助Set特性和hash表, 为List去重
hashSet.addAll(arrayList);
newList.addAll(hashSet);
Q ConcurrentMap原子性操作
put\putIfAbsent、compute\computeIfAbsent、remove(key, oldValue) 、replace(key, oldValue,newValue)
ConcurrentHashMap:通过 分段锁 保证复合操作原子性 :适合实现本地缓存、内存锁等功能
ConcurrentSkipListMap:通过 volitale+CAS 保证原子性 操作
Q JDK1.8 ConcurrentHashMap.compute\computeIfAbsent引发死循环的bug
ConcurrentHashMap computeIfAbsent的bug_concrunthashmap 的computeif-CSDN博客
ConcurrentHashMap中computeIfAbsent递归调用导致死循环_java.util的代码concurrenthashmap的computeifabsent中的nod-CSDN博客
不要在computeIfAbsent方法中再次修改map,会出现死循环cpu飙升。
原因1:compute持有Key1的分段锁,而修改map会持有key2的锁,可能出现死锁。
map.computeIfAbsent("a",key -> {
map.put("a","v2"); //修改map,引发bug的原因
return"v1";
});
如果非要在计算新值的过程中修改map,可以换一种方法来实现computeIfAbsent的功能:
V value = map.get(k);
if (value == null) {
V newValue = computeValue(k); // 这里对computeValue(k)的重复调用不敏感,高并发时可能同时出现多次调用。若想避免可以尝试加锁
value = map.putIfAbsent(k, newValue);
if (value == null) {
return newValue;
}
return value;
}
Q 集合交并差统计等高效操作思路与数据结构?(当内容数量不多时,也可以考虑暴力处理)
本人原文内容比较多,所以单开一页:
JAVA 容器集合+数据结构 模拟SQL实现交并差统计等操作_java数据结构 支持sql-CSDN博客
高效的找出两个List中的不同元素_比较两个list不同的元素-CSDN博客
Q Top K 、合并有序链表\数组、堆排序等问题?
其本质上是排序问题,常用思路:快速排序(求小 左递归,求大 右递归)、堆排序(适合大数据量) .。
常规排序缺点:仅仅为找出前几个元素,而将所有元素排序,浪费时间和资源
堆排序虽然本身速度没有快排和归并这种分治排序快,但是当统计外部大数据量时,求TOP K只需要建立容量为K的堆,非常节省内存开销。
JAVA中PriorityQueue 优先级队列是小根堆结构,但是可以通过comparator自定义大小,转换成大根堆模式。
PriorityQueue(int initialCapacity,Comparator<? super E> comparator)
Q 集合第三方JAR推荐?
google.Guava , Apache Commons Collections 包含很多高效的集合操作API和新类型的数据结构。
推荐Guava,经测试平均执行效率高于JDKAPI(Lists,Sets,Maps,Collections2等工具类中提供了包括集合交并差过滤等多种高效操作)。而且除了集合,还有很多高效简单的工具类,涉及String,Object,I/O等等
Q 实现LRU缓存
Map<Integer, String> map = new LinkedHashMap<Integer, String>(){
private static final long serialVersionUID = 1L;
private int maxSize = 10;
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<Integer, String> pEldest) {
return size() > maxSize;
}
};
Q 实现热点新闻列表缓存
需求:缓存1000个最新有序新闻。前端界面样式类似头条APP,假设一页10条新闻,页面向下滚动到底加载更早的10条新闻,向上滚动到顶加载更新的10条新闻。相当于有分页功能。
分析:TreeMap是有序红黑树结构(线程安全:读写锁、ConcurrentSkipListMap),API支持从某个KEY开始顺序向上/下查询和key1~key2的范围查询,假设越新的新闻其ID越大,以新闻ID(Long)为KEY将热点新闻存入TreeMap排序。
ConcurrentSkipListMap底层是有序链表,其lowerEntry、higherEntry方法,找到是节点然后Node.next实现范围查找。
实现:
用户首次打开页面,获得当时排序最高(新)的新闻id = map.lastKey()。循环执行10次map.lowerEntry(key)取十条更早的新闻保存到List返回前端。
用户向下滚动到底加载更早新闻,将当前页面中最下(小)新闻id发送给后端,后端根据新闻id,循环执行10次map.lowerEntry(key)取十条更早的新闻保存List返回前端。
用户向上滚动到顶加载更新新闻,将当前页面中最上(大)新闻id发送给后端,后端循环执行10次map.higherEntry(key)取十条更早的新闻保存List返回前端。
分页:根据某个KEY向前/后取N条
//伪代码
//取10条id更大的新闻
for(int i=0;i<10;i++){
e= map.higherEntry(id);
id=e.key;
list.add(e);
}
个性化新闻推荐:可以通过用户画像将用户分N类,创建N个不同缓存,缓存不同风格的新闻。
分页:根据页号取N条。注意toArray()底层是遍历subMap.values 装入新ArrayList。
测试3万字符串的subMap转Object[],耗时10ms左右。
//根据时间范围查找,subMap传入的key可以不存在map中【subMap\higherEntry等只通过其比较器比较范围】
ConcurrentNavigableMap<Node, String> subMap = concurrentSkipListMap.subMap(new Node(20200101), new Node(20200501));
Collection<String> values = subMap.values();
Object[] objects = values.toArray();
//System.out.println(objects);
//分页查询, 时间范围内的第2页,第20~40条
for (int i = 20; i < 40; i++) {
System.out.println(objects[i]);
}
Q ConcurrentSkipListMap
有序MAP.get(Key)只通过比较器比较==0,不用hashcode和equals
public class JdkTest {
public static void main(String[] args) {
testSkipListMap();
}
static void testSkipListMap(){
//有序map的key,只认比较器结果
ConcurrentSkipListMap<Node,String> concurrentSkipListMap = new ConcurrentSkipListMap();
Node order1 = new Node(1L, "order1", 2020010102);
Node order2 = new Node(2L, "order2", 2020010103);
Node order3 = new Node(3L, "order3", 2020010203);
//put时如果比较器结果为相等,则会认为Key相等,覆盖原key的value
concurrentSkipListMap.put(order1,order1.getOrderInfo());
concurrentSkipListMap.put(order2,order2.getOrderInfo());
concurrentSkipListMap.put(order3,order3.getOrderInfo());
//传入key不存在也可以实现范围查找【只通过比较器比较范围】
//时间范围查找
System.out.println(concurrentSkipListMap.subMap(new Node(2020010100),new Node(2020010105)));
//orderNo查询,SkipList查询只基于Comparable比较器,比较器的属性不准确是不能正确get的
//下面就会查到"order2",说明orderNo不影响get
System.out.println(concurrentSkipListMap.get(new Node(1L,"", 2020010103)));
}
static class Node implements Comparable<Node> {
private Long orderNo;
private String orderInfo;
private Integer onTime =0;
public Node(Long orderNo, String orderInfo, Integer onTime) {
this.orderNo = orderNo;
this.orderInfo = orderInfo;
this.onTime = onTime;
}
public Node(Integer onTime) {
this.onTime = onTime;
}
public String getOrderInfo() {
return orderInfo;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
return Objects.equals(orderNo, node.orderNo);
}
@Override
public int hashCode() {
return Objects.hash(orderNo);
}
@Override
public String toString() {
return "Node{" +
"orderNo=" + orderNo +
", orderInfo='" + orderInfo + '\'' +
", onTime=" + onTime +
'}';
}
@Override
public int compareTo(Node o) {
return this.onTime - o.onTime;
}
}
}
执行结果:
{Node{orderNo=1, orderInfo='order1', onTime=2020010102}=order1, Node{orderNo=2, orderInfo='order2', onTime=2020010103}=order2}
order2
Q ConcurrentSkipListMap跳跃表结构
其思路类似Mysql的B+树索引,底层是数据的有序链表,上层为链表建立N级索引,用空间换时间。
源码分析:基于跳跃表的 ConcurrentSkipListMap 内部实现(Java 8)_使用跳表实现map-CSDN博客
泛型
Q 集合泛型注意事项
1 List<T>泛型容器变量在赋值无泛型容器变量时,不检查泛型约束,但是在使用时会编译失败。
List<T> 在引用变量确定T类型后,就不能接收不同List<E>的对象引用。
List a1=new ArrayList(); //无泛型
a1.add("123");
a1.add(1);
List<Integer> a2=a1; //此处正常编译
Integer i=a2.get(0); //编译出错
假如T是E的父类,但是List<T> 和List<E> 没有父子关系。引用变量不能相互使用。
2 List<?>引用 :可以接受任意类型集合引用,但是其不能执行add元素操作。可以remove元素。
3 List<? extends T>引用 :只接受子类,上界为T(包含T)。不同于1,其引用赋值时会检查泛型约束。除了add(null)外无法执行add操作。get操作返回T类型对象。适合读
4 List<? super T>引用 :只接受父类,下界为T(包含T)。其引用赋值时会检查泛型约束。只能add添加T类或其子类。get操作返回Object类型对象。适合写
下面两段代码,可以看出
无泛型集合转换成有泛型集合,只要类型匹配就能正确的进行类型转换
有泛型集合转换另一种泛型集合,即使类型匹配,IDE也会检查出错误。
无泛型引用和任意泛型引用可以相互赋值(运行时自动转换类型,转换不匹配抛异常),有泛型引用只能与同泛型引用赋值。
@Test
public void testFx(){
List<SimCard> simCards=getsimlist();
//可以正确执行
for (SimCard s:simCards){
System.out.println(s.getDhhm());
}
}
//返回无泛型List
private List getsimlist(){
ArrayList<Object> simCards = new ArrayList(3);
simCards.add(new SimCard("1","1","1","1"));
simCards.add(new SimCard("2","1","1","1"));
return simCards;
}
@Test
public void testFx(){
//IDE提示错误
List<SimCard> simCards=getsimlist();
for (SimCard s:simCards){
System.out.println(s.getDhhm());
}
}
//返回有泛型List
private List<Object> getsimlist(){
ArrayList<Object> simCards = new ArrayList(3);
simCards.add(new SimCard("1","1","1","1"));
simCards.add(new SimCard("2","1","1","1"));
return simCards;
}
Q 泛型方法
泛型方法通过返回值引用,参数类型自动推断泛型,不需要调用时指定<T,E>。
使用时注意类型检测。
--调用
Pojo pojo =getObject("1");
--<T,E>自动推断<Pojo,String>
private <T,E> T getObject(E name){
try{
return (T) redisTemplate.opsForValue().get(name);
}catch (ClassCastException e){
//类型转换异常
return null;
}
}
Q 泛型测试
class TestType<T> {
private T arg;
public T getArg() {
return arg;
}
public void setArg(T arg) {
this.arg = arg;
}
public <T> T getAtgT(T name){
System.out.println(name);
return name;
}
public static void main(String[] args) {
TestType<Integer> integerTestType = new TestType<>();
integerTestType.setArg(1);
System.out.println(integerTestType.getArg());
String s= integerTestType.getAtgT("123");
Double d=integerTestType.getAtgT(new Double(1));
}
}
当integerTestType实例化后,其类泛型的属性和方法就已经绑定好。但是泛型方法getAtgT()仍然是独立的类型T。
泛型方法getAtgT()可以根据每次的参数类型,独立推断自己的泛型
Q 桥接方法bridgemethod与泛型擦除
IO
Q.关闭多个IO流
连续关闭两个流,在同一个finally快里,若第一个流close失败,出现异常时,会导致第二个流没有关闭。
finally {
try {
if (out != null) {
out.close();// 如果此处出现异常,则out2流也会被关闭
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (out2 != null) {
out2.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
1.7开始只要实现的自动关闭接口(Closeable)的类都可以在try()结构体上定义,java会自动帮我们关闭,及时在发生异常的情况下也会。
try (OutputStream out = new FileOutputStream(""); OutputStream out2 = new FileOutputStream(""))
{
// ...操作流代码
} catch (Exception e) {
throw e;
}
事务
Q.spring的PROPAGATION_REQUIRES_NEW事务,下面哪些说法是正确的?
答案:内部事务回滚了,外部事务仍然可以提交
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行
Q.spring的事务注解在加synchronized锁时有没有可能出现问题
答案:要保证synchronized包裹事务方法,先进入同步锁再启动事务就不会有问题。否则因为数据库隔离性的特点引起问题。下面是有问题的代码:
@Transactional
public synchronized void update() throws Exception {
//错误用法:线程不安全,各个线程先启动自身事务,再竞争锁
}
内部类
匿名类作为子类,可以通过类似闭包的特性引用外部变量。
补充:
1 局部内部类可以使用外部方法的不可变的局部变量的引用(final修饰 或 实际不变的)。这里的不可变指的是基本变量或引用变量不变,引用变量对应的Object的状态是可以改变的 。类似于闭包效果。
void put(){
final int i=0;//不可变
new Runnable(){
void run(){
print(i);
}
}
}
2 补充1,若在局部内部类中想要让外部局部变量可变。
void put(){
final int[] i = {5}; //解决不可变问题 或 封装成对象属性
new Runnable(){
void run(){
print(i[0]); //i[0] 是可变的
}
}
//可以将局部数组/对象通过Runnable暴露给另一个线程
//验证i的值,等待异步执行完毕
}
3 静态内部类:没有对应的外部类对象(其他内部类需要对应的外部类对象),适合只需要访问外部类静态资源或外部类静态方法中使用
4 匿名内部类是在编译时就生成class
多线程
线程安全、线程调度、各种线程池、并发容器
基础推荐入门:
马士兵多线程基础视频(2017斗鱼版):视频去哪了呢?_哔哩哔哩_bilibili
《JAVA并发编程实战》
java一切线程都是Thread对象
Q 线程安全三要素
JAVA 线程安全三要素_java安全认证三要素-CSDN博客
原子性,有序性,可见性
Q 下列哪个是原子操作?
A: j++ B: j = 1 C: j = j D: j = j+1
答案:B ,其他是有取值/计算/赋值 的复合操作,是线程不安全的
Q 线程1、2同时执行,有没有可能打印出“haha”?
//伪代码
x=1;
y=1;
public void a(){ //线程1 执行
x=5;
y=6;
}
public void b(){ //线程2 执行
if (y==6)&&(x<>5){
print("haha");
}
}
答案:有可能,因为jvm重排序问题。若有需要请遵守happenbefore原则,保证可见性和有序性
Q 死锁
避免:1、所有代码上锁时按照同样的固定顺序;2、trylock 3、lock(waittime)一定时间拿不到锁就解除阻塞。
Q 中断线程任务 interrupt
本质就是打标志+主动判断,通过异常或跳出/跳过代码块的方式结束线程的代码任务流程。JAVA禁用stop()终止线程,避免死锁、资源不释放等问题。FutureTask#cancel、ThreadPoolExecutor#shutdownNow 就是使用此方式中断线程任务。当对一个Thread线程对象调用 interrupt() ,设置其中断状态时:
① 如果线程处于被阻塞状态(例如处于sleep, wait, join,lockInterruptibly等状态,这些方法底层都判断了Thread.interrupted()),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。
JDK源码中只要看代码有没有对Thread.interrupted()做处理,即可知道可不可以不被中断。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。需要主动判断停止业务代码。
在执行较长时间业务代码时,需要判断中断状态。给外部调用代码/其他线程保留中断当前线程执行任务的可能。
for(;;){
//通过检测线程状态,抛出异常的手段终止线程执行当前任务。而不是真正停止当前线程
if(Thread.interrupted()) { //判断状态
//注意下次判断前,重置线程状态
throw new InterruptedException("任务被中断");
}
}
Q Synchronized关键字、锁升级、锁优化
Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_java synchronized 锁升级原理-CSDN博客
上锁的代码块保证原子性,有序性,可见性。一个线程持锁执行代码块时,其他要持锁的线程需要等待其放锁后才有机会持锁继续执行。可以简单理解为需要同一把锁的所有线程会排队串行执行此锁保护的所有代码块。
持锁:
1、Synchronized(object1){ 持有堆内存中object1对象的监视器锁 }
2、Synchronized在非静态方法上,持有this对象的监视器锁。
3、Synchronized(A.class){ }或在静态方法上,持有A类的class对象锁
4、阻塞期间不会被外界中断,如:Thread.interrupt()无效
释放锁:
1、退出被锁保护的代码块/方法;
2、持锁后抛出异常(catch异常不会放锁);
3、object1.wait();(执行object1.wait/notify的前提,要先持有object1对象的监视器锁,见源码中注释)。注意:object1.notify()不会释放锁。object1.wait()会释放已持有的的Synchronized(object1)锁。
锁升级:
偏向锁->轻量级锁(自旋锁)->重量级锁(OS级)。这是java 1.5以后jvm底层对synchronized做了性能优化。
当每次都是同一个线程访问synchronized时使用偏向锁,当有另一个线程来竞争锁后升级为轻量级锁(自旋锁),当某个线程自旋10次仍未获得锁,则此锁升级为重量级锁。重量级锁要与OS底层交互效率比较低。
java的lock接口实现类底层使用AQS在jvm层实现锁,无需与OS交互,比重量级锁性能好。
锁优化:
Jvm底层在运行和编译时会对锁有一些优化,例如:锁升级、锁粗话、锁擦除等
底层实现:
每个对象有一个监视器monitor。monitor中包含owner:持锁线程,recursions:记录重入次数。
synchronized代码块在字节码中为monitorenter.....monitorexit。monitorenter会检查对象monitor。
汇编语言中:lock cmpxchg 可以看出 Synchronized在1.8 中编译为 compare x change 比较交换。
Q synchronized+ReentrantLock异同
JAVA 重入锁 synchronized+ReentrantLock_java synchronized 嵌套 reentrantlock-CSDN博客
Q voliatile关键字的作用,以及happensbefore原则
JAVA volatile关键字_voliatile 对象分配在哪-CSDN博客
voliatile+cas 能够保证线程安全的三大要素。
ConcurrentLinkedQueue 是比较经典的只通过 【voliatile+cas+自旋】 实现无锁并发的队列。
Q ConcurrentLinkedQueue
通过 【voliatile+cas+自旋】 实现无锁非阻塞并发的队列。
ConcurrentLinkedQueue 源码解读:ConcurrentLinkedQueue 源码解读-腾讯云开发者社区-腾讯云
ConcurrentLinkedQueue 源码图解:https://my.oschina.net/mengyuankan/blog/1857573
注意:
1 ConcurrentLinkedQueue.size()会遍历所有元素非常慢,请使用isEmpty()
2 是无界队列注意OOM问题
Q ThreadLocal<T>
JAVA ThreadLocal<T>_java threadloacl<string>-CSDN博客
适合做线程级变量,单线程全局变量,起到线程隔离作用
Q ReentrantReadWriteLock可以锁升级吗
不支持锁升级,只要有一个线程读锁就不能有线程持有写锁,即使持有读锁的线程是自己
Q 控制多个线程顺序执行
方法1:CountDownLatch或其他AQS组件 方法2 :join() 方法3:通过一个volitiale/Atomic标志变量 方法4:object.wait或condition.wait。
Q 线程JOIN,程序输出?
Thread t1=new Thread(()->{try {
Thread.sleep(5000);
System.out.print(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}},"T1");
t1.start();
t1.join();
System.out.print(Thread.currentThread().getName());
答案:T1 main ,主线程等待join线程执行完后再执行,若t1.join()时t1已经结束,则主线程继续执行
Q 并发执行时,在无锁非同步情况下,不要以类似 i==0 的判断作为参照(除非i只有0/1两态)。最好改成i<=0 这种,因为高并发情况下,可能瞬间变为负值
Q 线程状态与相互转换
图中Resource blocked,Object waiting状态,线程会进入阻塞挂起状态,并放弃当前阻塞条件对应的已持有资源(比如锁),等待其他线程唤醒自己。
Runnable 是线程状态正常可以运行,但是当前未获得CPU时间片
Time waiting相当于sleep(),此时线程进入睡眠状态,并会主动醒来,且过程中不释放已持有资源。
Q 常见的线程状态控制
synchronized(obj)+obj.wait() / obj.notify;
AQS组件:底层用LockSupport实现挂起、唤醒; threadA.interrupted()会唤醒被LockSupports.park挂起的线程threadA。
lock+ lock.condition1.wait()、 lock.condition2.wait() ;
lock支持不同条件挂起:例如BlockQueue的put和poll可以用两个不同的condtion控制线程状态
Semaphore;
CyclicBarrier;
CountDownLatch.await;(一次性)
Future.get/set;(一次性)
thread.join();
Q 线程挂起和唤醒?
被挂起线程自身不能唤醒自己,唤醒线程A必须由其他线程完成。线程挂起后会放弃锁等资源。等被唤醒后再去尝试获取锁。
且其他线程要持有使线程A挂起的那个对象的引用。(可以通过传参或是公共变量、公共容器的方式,使其他线程获得唤醒因子)
简单理解就是哪个Object的方法挂起线程A,线程B就要用那个Object的唤醒方法来唤醒线程A。
使线程A挂起方式有很多,线程A可调用如下方法, 比如 :lock阻塞、 lock.conditionA.await()、objectA.wait()、CountDownLatch.await() 、Future.get()、AQS对象.acquire() 等等。(AQS可以自己实现同步组件,但是首先考虑使用JDK提供的已完成组件)
线程B必须获得使线程A挂起的对应对象的引用(objectA,conditionA,AQS对象等),并调用其唤醒方法例如 objectA.notify(),AQS对象.release()。
例外还可以强制唤醒。中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞或synchronized,无能为力。
Q 什么导致线程阻塞、放弃时间片
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
Thread类中定义的停止方法(sleep、yield、suspend)不会释放已持有资源。(资源比如同步监视器锁)
obj.wait()只会释放当前线程已经持有的此obj对象的镜像锁syn(obj)
lock.condition.await()释放condition对应的Lock。操作lock.condition必须先持有lock
方法 | 说明 |
---|---|
sleep() | sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止 |
suspend() 和 resume() | 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。 |
yield() | yield() 使当前线程放弃当前已经分得的CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。让同优先级的线程有执行的机会 |
wait() 和 notify() | 调用obj的wait(), notify()方法前,必须获得obj锁。两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。 wait, nofity和nofityAll这些方法不放在Thread类当中,原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。所以把他们定义在Object类中因为锁属于对象 |
Lock() | 等待锁的线程出现阻塞,线程释放锁后触发唤醒后续等待线程 |
AQS组件 | 线程状态控制器 |
Q AQS
Q.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
https://blog.csdn.net/JAVAziliao/article/details/90448820
这是JDK强制的,保证线程加入等待队列/唤醒队列是线程安全的,避免出现lost wake up问题,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的SYN锁。objcet.wait()相当于对应的此objcet镜像锁的条件Condition对象。
操作obj.wait()、obj.notify()必须先持有syn(obj)锁。
同理操作lock.condition.await()/signal()等也要先取得对应的lock,否则汇报IllegalMonitorStateException。
Q notify()/notifyAll()的选择
o1.notifyAll唤醒所有等待o1监视器锁线程,可能会触发惊群。
notify只会唤醒一个线程,但可能出现卡死状态。比如一个生产者执行notify后,没有唤醒消费者而又唤醒了一个生产者,这时候整个生产消费模式就换卡死。
推荐:在情况不确定时优先选用notifyAll
Q 线程wait()一定要保证在notify之前,不然线程会一直挂起。
以下代码存在什么隐患? 期望效果是main线程等待sub线程执行完任务后唤醒自己
//错误代码(有可能sub线程先执行notify,导致main一直wait)
Thread main= Thread.currentThread();
Thread sub=new Thread(()->{main.notify()}); //sub线程持有main对象
sub.start();
main.wait(); //main.wait();对应main Object对象的镜像锁
答案:可以用CountDownLatch代替,把同一个CountDownLatch交给sub线程用来唤醒main线程。因为CountDownLatch如果已减到0后,即使后执行CountDownLatch.wait()也不会挂起当前线程。 countDown()确保要被执行最好放在finally中,避免等待线程永不唤醒。线程不能唤醒自己
Q condition.await()线程被唤醒后怎样确保条件状态?
答案:最好在while循环中判断条件,保证线程醒来后,第一时间判断条件。
Dubbo中用此方式控制并发线程等待。org.apache.dubbo.rpc.filter.ActiveLimitFilter
volitiale int i;
lock.lock(); //必须先持有conditionA对应的lock,才能操作conditionA
while (i<0){ //也可以限制重试次数,超出抛出异常,避免死循环
conditionA.await(); //释放lock,等待其他线程获取lock并调用conditionA.signal()唤醒
}
lock.unlock();
Q 释放/唤醒 等操作在finally中执行
最好将释放/唤醒等操作放在finally中,避免因为异常无法执行,导致另一个线程永久挂起
--下面代码存在风险,若有一个线程put时抛出异常,end.await()永远不会被唤醒
CountDownLatch end = new CountDownLatch(100);
for (int i=0;i<100;i++){
int a=i;
threadPoolExecutor.execute(() -> {
Cache.put(a); //PUT方法可能抛出RuntimeException
end.countDown();
});
}
end.await();
Q 异步执行提高并行工作效率
多线程并行计算,减少总体消耗时间。
//异步并行计算
Future<Integer> f = threadPool.submit(()->{
Thread.sleep(1000);
return 1+1; })
//当前线程计算
int i=0
while(i<10){
i++;
}
//结果合并
return i + f.get();
Q Future中线程无锁加入/唤醒等待队列的实现。
加入队列:CAS自旋+双重检查。检查状态后CAS更新,继续for自旋执行二次检查代码。
Q 异步任务利用Future提前向多线程暴露结果,避免重复执行
例如:主线A将异步任务提交到线程池后返回Future,将Future放入共享缓存,线程A继续执行后续工作。其他线程先判断缓存中有无Future。已存在则直接future.get()等待主线A计算结果。
for{
lock/CAS操作取得资源
if 成功:
二次检查?是返回:否 执行操作 ->释放资源->唤醒wait
else
wait等待唤醒
}
Q 并发环境,避免重复执行更新缓存,防止缓存击穿
假设场景中有一个成员变量List result是DB查询结果作为本地缓存,result是懒加载当执行查询方法时判断null则查询DB结果赋值result,其他前程都直接访问缓存。怎么防止缓存击穿,保证只有一个线程完成DB查询,其他线程都访问缓存数据。
方法一:lock+双重检查
ConcurrentHashMap使用分段LOCK更新
分析:使用LOCK虽然也能防止击穿,让线程等待结果。但是所有执行到stop的线程都要串行化持锁,即使第一个持锁线程已经更新了缓存。造成后续线程排队读缓存,影响执行效率。
//result本地缓存
//下面代码虽然能够避免击穿,但是效率低
if (result==null){
//stop 停此处线程都需要串行化取锁,效率降低
//实际上只要一个线程更新缓存后,其他线程就可以并发读缓存
synchronized (this){
//二次检查
result=getCache();
if (result==null){
//查询DB更新缓存
}
return result;
}
}
return result;
方法二:CAS自旋+双重检查
其特点是非阻塞,不会像Lock一样将线程挂起。
比较经典的例子:ConcurrentHashMap中initTable()方法。Future中加入等待队列。
下面是一个非常简单的例子。AtomicLong 作为非阻塞锁,利用CAS的原子性抢锁,保证同时只有一个线程能进入CAS保护的代码块。但是线程抢成功进入CAS保护代码块后,要先做二次检查判断(result==null)。因为可能出现线程2在stop位置丢失cpu时间片,此时线程1成功进入CAS更新result后又把getlock设置为0。等线程2取得CPU再次运行时其仍然可以进入CAS块,这时若不做二次检查会造成result重复更新。
--下面最简易思路
for{
查询缓存
没有则AtomicLong.CAS操作取得DB查询权利?
获权:二次检查缓存 (存在)?是返回:否 ->查询DB->更新缓存->放权->CountDowbLatch唤醒
无权wait:CountDowbLatch等待唤醒->查缓存
}
List result;
AtomicLong getlock; //==1 当前有线程加载result
List getList(){
for(;;){
if (result!=null){
return result;
}
//stop
if (getlock.compareAndSet(0, 1)){//cas保证线程安全三要素
//取得更新权利
if (result==null){
//双重检查锁
try{
result=mapper.get();
return result;
}finally{
//释放更新权利
getlock.set(0);
}
}
}
//未获得权利的线程继续FOR循环 直到取得结果
//若不想让这些线程一直FOR循环,减少CPU消耗。可以使用countDowbLatch.await()将其阻塞。等待更新result的线程将其唤醒
Thread.yield();
}
}
方法三:computeIfAbsent()。会持有分段锁保证线程安全。
方法四: 在方法三基础上提前暴露结果Future到缓存Map中,其他线程get缓存存储的是Future则调用Future.get等待结果,避免忙自旋消耗CPU。相比较方法一Future.get使用AQS的共享模式。
若是单值则使用AtomicReference<Future<List>> result 提前暴露结果,用CAS判断result中引用是否为null,做为竞争更新权利的锁。
Future、CountDownLatch只能使用一次,所以每次需要实例化。
//伪代码
for(;;){
result = concurrentHashMap.get(key);
if(result==null){
//也可以使用new CountDownLatch(1)
实例化 future=new FutureTask();
if(concurrentHashMap.putIfAbsent(key,future)){
//第一个暴露成功的线程,执行
//future.run();
result =执行业务代码;
concurrentHashMap.put(key,result );
//future设置结果,唤醒等待线程
future.set(result );
return result ;
}
//暴露失败继续for循环
future = null; //gc
}
else if(result instanceof Future){
//线程挂起等待结果
return result.get();
}
else{
//剩余情况说明缓存中有值,直接返回
return result ;
}
}
Q Lock与Cas+for自旋锁
Lock会阻塞线程是线程挂起等待唤醒,Cas自旋锁不会。但是在超高并发锁的竞争十分激烈时Lock可能效果更好,因为挂起的线程不会占用CPU。所以锁的选择要在做充分的场景测试后再做取舍。
也可以CAS+CountDowbLatch使线程进入挂起状态减少CPU消耗。
业务分段锁
public updateInfo(String id,String info) {
Object lock=locks.get(id);
if (lock==null){
//每个ID一把锁
locksMap.putIfAbsent(id, new Object());
lock=locks.get(id);
}
synchronized (lock) {
.....
}
}
Q 下面哪个行为被中断不会导致InterruptedException?
Thread.join 、Thread.sleep、Object.wait、CyclicBarrier.await、Thread.suspend、reentrantLock.lockInterruptibly()
答案:Thread.suspend 。thread1.suspend()与thread1.resume()必须成对出现。
suspend()方法就是将一个线程挂起(暂停),resume()方法就是将一个挂起线程复活继续执。
行当执行thread1.interrupt()中断方法后,会将thread1设置为中断状态。但是并不是真的中断,需要在自己的业务代码中主动判断当前线程状态:终止代码或抛出异常。
若thread1阻塞在Thread.join 、Thread.sleep、Object.wait、CyclicBarrier.await、reentrantLock.lockInterruptibly()等情况下,thread1会从阻塞中唤醒并抛出InterruptedException,。
但是thread1此时处于IO阻塞、synchronized阻塞、reentrantLock.lock()阻塞、Thread.suspend()状态下时,interrupt()中断对其没有作用,thread1会一直阻塞、等待外界唤醒。
Q 同步转异步、异步转同步思想?
同步转异步:将同步串行化的代码,提交到线程池/Queue/MQ等等,实现线程切换。让同步串行化执行变成异步并行执行,可以缩短任务整体的用时。
异步转同步:例如线程A将一部分任务交给线程B执行,但是最终线程A需要等待B的执行结果。此时需要借助JDK的同步组件例如:AQS组件比如:Future、CountDownLatch,lock.condtion() 等使线程A挂起,线程B执行完成后,要通过当时使线程A挂起的同步组件对象引用释放资源,唤醒线程A。
Github项目NettyRpc 阅读(Netty+同/异步通讯+多线程+AQS+CAS+volatile)_netty 异步通讯代码-CSDN博客
RabbitMq 模拟RPC调用_rabbitmq countdownlatch-CSDN博客
Q 假设如下代码中,若t1线程在t2线程启动之前已经完成启动。代码的输出是
public static void main(String[]args)throws Exception {
final Object obj = new Object();
Thread t1 = new Thread() {
public void run() {
synchronized (obj) {
try {
obj.wait();
System.out.println("Thread 1 wake up.");
} catch (InterruptedException e) {
}
}
}
};
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread() {
public void run() {
synchronized (obj) {
obj.notifyAll();
System.out.println("Thread 2 sent notify.");
}
}
};
t2.start();
}
答案:
Thread 2 sent notify.
Thread 1 wake up
Q.SimpleDateFormat 是线程安全的?
答案:不是,可以使用 ThreadLocal<DateFormat>实现线程封闭 或 DateTimeFormatter 。
如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。
推荐第三方JAR包JODA-time
Q 并发生产随机数Math.random()的隐患?
答案: 会因竞争同一seed 导致的性能下降 , 推荐使用ThreadLocalRandom
Q 线程之间信息传递,线程上下文的实现和隐患?
1 通过公共对象/变量传递进行信息交互,注意线程安全的控制
2 有线程上下文的设计思想,可通过ThreadLocal实现,实现线程安全封闭+同线程各个方法间传递。
但是线程、进程之间切换、调动,注意上下文内容的传递。
例如:线程A将任务提交到线程池,线程池中的线程与线程A的线程上下文(ThreadLocal)是不同的。
或是微服务之间的调用,也涉及到线程切换的问题。可以将线程A的上下文作为参数或httpheader等手段传入消费线程。消费线程可以直接使用。或可以通过Aop/拦截器 在消费方法执行前,把上下文参数或httpheader同步到自己的同类型线程上下文中。ThreadLocal注意释放。
Q 怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj):boolean方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着”某条线程”指的是当前线程。
Q 主线程提交任务到线程池,此任务出现异常,主线程合适能检测到?
直到操作Future.get()时才会捕获任务抛出的异常,不操作Future.get()则主线程正常运行无异常。
public class Test1 {
@org.junit.Test
public void testThread() throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2));
Test1 this1=this;
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
this1.say(); //利用局部变量 使局部内部类中拿到外部对象引用
if(true)
throw new RuntimeException("错误");
return "ok";
}
} );
System.out.println("wait....");
Thread.sleep(1000);
System.out.println("wait get...");
System.out.println(future.isDone());
System.out.println(future.get(1000,TimeUnit.SECONDS));
System.out.println("get");
}
private void say(){
System.out.println("main say");
}
}
wait....
main say
wait get...
true
java.util.concurrent.ExecutionException: java.lang.RuntimeException: 错误
Q System.currentTimeMillis()性能问题
https://www.jianshu.com/p/3fbe607600a5
并发调用此函数效率非常低那有什么策略既可以获取系统时间又避免高频率调用呢。
- 策略一:如果对时间精确度要求不高的话可以使用独立线程缓存时间戳。
- 策略二:使用Linux的clock_gettime()方法。
Java VM可以使用这个调用并且提供更快的速度currentTimeMillis()。
如果绝对必要,可以使用JNI自己实现它.
- 策略三:使用System.nanoTime()。
策略一实现
class MillisecondClock {
public static final MillisecondClock CLOCK = new MillisecondClock(10);
private long rate = 0;// 频率
private volatile long now = 0;// 当前时间
private MillisecondClock(long rate) {
this.rate = rate;
this.now = System.currentTimeMillis();
start();
}
private void start() {
new Thread(new Runnable() {
@Override
public void run() {
now = System.currentTimeMillis();
try {
Thread.sleep(rate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"系统时间统一更新Thread").start();
}
public long now() {
return now;
}
}
Q 线程切换、异步执行
线程将任务/信息传递给其他线程异步执行。最常用的手段的通过Queue做线程切换(生产者-消费者模式)。同时可以配合共享变量/容器存储信息。注意ThreadLocal的信息不会传递给其他线程。
JAVA线程池就是这种设计思路。线程A把任务提交到线程池的QUEUE中,再由线程池中的工作线程从QUEUE中提取。线程池返回Future对象。
分布式中消息中间件MQ也是相同应用思路。可以配合Future控制调用线程等待结果。
Netty的Queue应用: IO线程池接收客户端请求通过队列把任务交给业务线程池处理。业务线程池处理完任务将结果放入另一个队列中,IO线程读取此队列中的结果返回给对应的客户端socket。整体实现异步化。
Q 实现简单的限流隔离、超时降级功能
类似hystrix功能。本质上是通过多线程异步实行实现, 主线程负责计时计数终止降级等操作,子线程负责执行业务。
ExecutorService executorService = Executors.newSingleThreadExecutor();
AtomicLong sum = new AtomicLong(); //可以换成信号量,但是Atomic无锁效率更高
@Test
public void testTimeOut(int timeout){
//限流计数
sum.incrementAndGet();
Runnable runnable = new Runnable(){
@Override
public void run() {
try {
//执行真实的业务代码
if (Thread.currentThread().isInterrupted()){
//也可用局部变量代替isInterrupted()
throw new InterruptedException ("任务已取消/终止");
}
} catch (InterruptedException e) {
e.printStackTrace();
//中断
}
}
};
//线程切换。任务提交到线程池异步执行,可以通过多个线程池实现业务的线程隔离
Future<?> future = executorService.submit(runnable);
try {
//AQS.tryAcquireNanos->LockSupport.parkNanos
Object o = future.get(timeout, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//中断
} catch (ExecutionException e) {
} catch (TimeoutException e) {
//超时 取消/终止任务,取消本质上是Interrupt
future.cancel(true);
//降级代码
}
//释放计数
sum.decrementAndGet();
}
Q JVM中简单实现商品秒杀
只是思路生产环境需要完善。ConcurrentHashMap(模拟商品列表) + ConcurrentLinkedQueue(模拟单个商品的仓储)+ThreadPool(异步执行消费)。 实现JVM级别商品秒杀,类似redis list + mq实现秒杀,原理大同小异。
通过对外接口addSecKillGood()在JVM中实现商品的入库。
通过对外接口SecKill()在JVM中实现商品的入库。
main()模拟一次从创建活动、投放商品到秒杀的全部过程。
package com.springalibabatest.demoprovider;
import java.util.Queue;
import java.util.concurrent.*;
/**
* JDK- 并发MAP+Queue+ 线程池 实现JVM商品秒杀
* 类似Redis-List + MQ异步 实现秒杀
*/
public class JDKSecKill {
/**
* 秒杀活动列表
* 静态容器:保存所有秒杀活动
* k=活动id
* v=活动对象
*/
private static final ConcurrentHashMap<Integer, JDKSecKill> SEC_KILLS = new ConcurrentHashMap<>(10);
/**
* 本次秒杀活动ID
*/
private final int id;
/**
* 本次秒杀活动描述
*/
private final String info;
/**
* 本次秒杀活动是否开始
*/
private volatile boolean begin;
/**
* 本次秒杀活动的商品列表
* key = 商品名称
* value = 商品仓库(并发Queue模拟)
* 推荐设计一个【商品仓库类】来包装ConcurrentLinkedQueue
*/
private final ConcurrentHashMap<String, ConcurrentLinkedQueue<Good>> secKillGoods = new ConcurrentHashMap<>(10);
/**
* 异步消费秒杀结果
*/
private final ThreadPoolExecutor executor= new ThreadPoolExecutor(20,100,
5L, TimeUnit.MINUTES,new ArrayBlockingQueue<>(1000));
public JDKSecKill(int id, String info) {
this.id = id;
this.info = info;
}
/**
* 模拟秒杀行为
* @param args
*/
public static void main(String[] args) throws InterruptedException {
//创建一个秒杀活动对象
final JDKSecKill jdkSecKill = new JDKSecKill(10,"201902秒杀活动");
//添加到活动列表
SEC_KILLS.putIfAbsent(jdkSecKill.getId(),jdkSecKill);
//为本次秒杀活动添加商品列表
jdkSecKill.addSecKillGood("goods-1",10);
jdkSecKill.addSecKillGood("goods-2",10);
//活动开始
jdkSecKill.setBegin(true);
//显示活动列表
System.out.println(SEC_KILLS);
//模拟并发抢购
final CountDownLatch begin=new CountDownLatch(1);
int user=1000;
ExecutorService executorService = Executors.newFixedThreadPool(user);
for (int i = 0; i <user ; i++) {
executorService.execute(()->{
try {
begin.await();
//秒杀goods-1商品
jdkSecKill.SecKill("goods-1");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//所有人开始抢购
begin.countDown();
System.out.println(executorService);
//需要等待executorService提交完所有抢购任务,再关闭jdkSecKill中线程池。否则任务会被拒绝。
Thread.sleep(3000);
//关闭所有线程池,jvm结束运行
executorService.shutdown();
jdkSecKill.close();
System.out.println("秒杀结束");
}
/**
* 设置活动开始结束
* @param begin
*/
public void setBegin(boolean begin) {
this.begin = begin;
}
/**
* 向秒杀的商品列表中添加秒杀商品的仓库
* @param name 商品名称
* @param num 数量
* @return
*/
public Boolean addSecKillGood(String name, int num){
if (name==null || name.equals("")){
return false;
}
//创建商品仓库Queue
ConcurrentLinkedQueue<Good> goods = new ConcurrentLinkedQueue<>();
Queue<Good> o = secKillGoods.putIfAbsent(name,goods);
if (o!=null){
//列表中已存在商品仓库
return false;
}
for (int i = 0; i <num ; i++) {
//商品放入仓库
goods.add(new Good(i+1,name));
}
return true;
}
/**
* 从仓库中取出一个商品
* @param name 商品名称
* @return
*/
private Good getFromMapQueue(String name){
//取得对应商品仓库
Queue<Good> goods = secKillGoods.get(name);
if (goods==null){
return null;
}
//从仓库中取出商品
return goods.poll();
}
/**
* 秒杀一个商品
* @param name 商品名称
* @return
*/
public boolean SecKill(String name){
if(!begin){
return false;
}
//从对应商品的仓库取货
Good good = getFromMapQueue(name);
if (good==null){
//已抢光
return false;
}
//异步消费秒杀结果
executor.submit(()->System.out.println("秒杀到商品"+name+",id="+good.getId()));
return true;
}
public void close(){
begin=false;
//关闭线程池
executor.shutdown();
}
public int getId() {
return id;
}
@Override
public String toString() {
return "JDKSecKill{" +
"id=" + id +
", info='" + info + '\'' +
", begin=" + begin +
", secKillGoods=" + secKillGoods +
'}';
}
/**
* 商品
*/
private class Good{
private int id;
private String name;
public Good() {
}
public Good(int id, String name) {
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Good{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
}
反射
Q 程序输出?
package test;
import java.util.Date;
public class SuperTest extends Date{
private static final long serialVersionUID = 1L;
private void test(){
System.out.println(super.getClass().getName());
}
public static void main(String[]args){
new SuperTest().test();
}
}
答案:test.SuperTest
1.首先 super.getClass() 是父类的getClass()方法,其父类是Date,它的getClass()方法是继承自Object类而且没有重写,
所以就是调用object的getClass()方法。返回当前运行时的类。
2.在调用getName()方法而getName()是:包名+类名
Q Class.forName和classloader的区别
class.forName()除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象
JVM
Q.堆栈常量池
public static int INT1 =1 ;
public static int INT2 =1 ;
public static int INT3 =1 ;
局部变量:
int a1 = 1;
int a2 = 1;
int a3 = 1;
Q 常见的内存泄漏 OOM
JAVA 常见内存泄露例子及详解_java常见的内存泄漏写法-CSDN博客
java内存泄漏原因、常见泄漏情况、分析和解决方法_内存泄露java-CSDN博客
Java中关于内存泄漏出现的原因汇总及如何避免内存泄漏(超详细版)_java 内存泄露-CSDN博客
静态变量,容器,ThreadLocal,链接释放,非静态内部类持有外部类引用,外部循环引用,单例(因为自身引用在本类中,自己包括成员变量都无法释放)。
及时释放引用、使用非强引用.....
排查:
Java内存泄漏的排查总结_java里面多少条数据会造成内存溢出-CSDN博客
SPI
实例:mysql驱动jar。其实现了java.sql.Driver接口,并通过spi的方式将自己的实现类注入到java项目中。
其SPI配置文件:mysql-connector-java\8.0.15\mysql-connector-java-8.0.15.jar!\META-INF\services\java.sql.Driver
文件内容:com.mysql.cj.jdbc.Driver