今日科技快讯
3月13日报道,自从3月11日下午港股收盘后美团发布2018年年报及四季度财报后,次日开盘美团点评大跌超过10%至52.35港元,今日开盘后再跌超过7%。美团去年上市市值超过4000亿港元,被称为中国排名前五的互联网公司,但如今市值2753.49亿,同比跌幅超过1000亿港币。
作者简介
大家早上好。本篇文章来自 ck276128749 的投稿,帮大家整理了一份Java面试总结,涵盖了面试中常问的各种Java知识,希望对大家有所帮助!
前言
俗话说金三银四,现在正是换工作的好时候,特此准备了一份面试总结!
计算机基础
TCP/IP
TCP(传输控制协议)是一种面向连接的通过失败重传机制确保数据在端到端之间可靠传输的协议。
IP是面向无连接无状态的么有额外的控制机制保证发送的包是否有序到达。
五层模型:应用层、传输层、网络层、链路层、物理层
总结一下:程序在发送消息时,应用层按既定的协议打包数据,随后由数据层加上双方端口号,网络层加上双方IP地址,链路层加上双方MAC地址,并且将数据拆分成数据帧,经过多个路由器和网关后到达目的机器。简而言之,就是按“端口--IP地址--MAC地址”这样的路径进行数据的封装和发送。
三次握手:SYN和ACK置0和1,seq 和 ack [序号和应答号] x和y
你听得见吗?
我听得见,你听得见吗?
我也听得见,我们说话吧。
四次握手:
我们分手吧
好等我收拾东西,收拾完我告诉你
我收拾完了
好再见
HTTPS
哈希算法:MD5、 SHA
对称加密:AES、DES、3DES
非对称加密:RSA
Https握手过程
客户端给出协议版本号、一个客户端随机数A(Client random)以及客户端支持的加密方式
服务端确认双方使用的加密方式,并给出数字证书、一个服务器生成的随机数B(Server random)
客户端确认数字证书有效,生成一个新的随机数C(Pre-master-secret),使用证书中的公钥对C加密,发送给服务端
服务端使用自己的私钥解密出C
客户端和服务器根据约定的加密方法,使用三个随机数ABC,生成对话秘钥,之后的通信都用这个对话秘钥进行加密。
面对对象
封装、继承、抽象、多态。
类
抽象类与接口的区别
语法维度 | 抽象类 | 接口 |
方法实现 | 可以有 | 不可以,在1.8口可以用 default 实现 |
方法访问控制符 | 无限制 | 有限制,默认是 public abstract |
属性 | 无限制 | 有限制,默认是 public static final |
本类型之间的扩展 | 单继承 | 多继承 |
静态方法 | 可以有 | 不能有 |
static{} 静态代码块 | 可以有 | 不能有 |
内部类
-
静态内部类
成员内部类
局部内部类 方法内部或者表达式内部
匿名内部类
类关系
继承(空心实线三角箭头)
实现(空心虚线三角箭头)
组合(空心实线菱形箭头)
聚合(空心虚线菱形箭头)
依赖(虚线箭头)
关联(实线箭头)
方法
方法签名包括(方法名称、参数列表)是JVM标识方法的唯一索引
不包括:返回值、访问权限控制符、异常类型等
无论对于基本类型还是引用变量,Java的参数传递都是值复制的过程。
对于引用变量,复制指向对象的首地址,双方都可以用过自己的引用变量修改对象的相关属性
静态代码块只会执行一次,第二次对象实例化时不会执行
复写(@Override)一大两小两同
一大:子类方法访问权限控制符只能相同或者变大
两小:抛出异常和返回值只能变小,能够转型成父类对象
两同:方法名和参数列表必须相同
复写只能是非静态,非 final,非构造
重载
@@@
泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。—泛型类,泛型方法
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
List<?>
问号在正则表达式中可以匹配任何字符,List<?>成为通配符集合,可以接受任何类型的集合引用赋值
不能添加
可以 remove 和 clear
一般作为参数接收外来集合,或者返回一盒不知道具体元素类型的集合
List<T>
最大的问题就是只能放置一种类型,如果随意转换的话就是破窗理论,泛型就是去了类型安全的意义
List<? extends T> Get First【Put操作会破坏类型安全】【协变】
适用于消费集合元素为主
可以接受任何 T 以及 T 的子类集合的赋值
取出来的类型有泛型限制,向上强转为 T
null可以表示任何类型,除 null 外不可以添加任何元素
List<? super T> Put First【Get操作不知道返回值到底是什么类型】【逆变】
适用于生产集合元素为主
与生活中投票选举类似,可以投,但是取出来的时候根本不知道是谁的票,相当于 泛型丢失
extends 是 put 功能受限
super 是 get 功能受限
Java 泛型与 Kotlin 泛型
Java中的泛型是不型变的,原因是类中的方法可能会“干坏事”,破坏了类型安全
对于 Kotlin 而言,可以这么说:Consumer in, Producer out,in 做方法参数,out 做返回值
子类型问题:【使用处型变和声明处型变】
Crate<Orange> 是 Crate<Fruit> 的子类型吗?直觉可能告诉你,Yes。但是,答案是 No。对于Java而言,两者没有关系。对于Kotlin而言,Crate<Orange> 可能是 Crate<Fruit> 的子类型,或者其超类型,或者两者没有关系,这取决于 Crate<T> 中的 T 在类 Crate 中是如何使用的。简单来说,型变就是指 Crate<Orange> 和 Crate<Fruit> 是什么关系这个问题,对于不同的答案,有如下几个术语。
invariance(不型变):也就是说,Crate<Orange> 和 Crate<Fruit> 之间没有关系。
covariance(协变):也就是说,Crate<Orange> 是 Crate<Fruit> 的子类型。
contravariance(逆变):也就是说,Crate<Fruit> 是 Crate\<Orange> 的子类型。
总体而言,Java 和 Kotlin 中的泛型还是比较相像的。对于使用处型变,两者几乎等价,只是表现形式不同,Kotlin 看上去更加简洁一些,它们都是通过编译器限制我们对一些方法的调用来实现的。Kotlin 相较于 Java 中的泛型,最主要的提升在于,声明处型变,即在泛型类定义时就可以把其声明为协变(out)的,或者逆变(in)的。总而言之,Kotlin 是以更加简洁、灵活而严格的方式实现了泛型。
5、数据类型
boolean(1)、byte(1)、char(2)、short(2)、int(4)、long(8)、float(4)、double(8)、refvar(4)
refvar 引用四个字节,占32位,最大寻址位2^32幂,也就是4G
refobj(堆区真正对象)基础大小12B(不包括实例数据),需要对其填充
refobj包括:
哈希吗、GC 标记、GC 次数。同步锁标记、偏向锁持有者
对象头
对象标记
类元信息
实例数据
对其填充
例如int 4字节,Integer 16字节=12+4字节
引用类型
强引用(StrongReference):具有强引用的对象不会被 GC;即便内存空间不足,JVM 宁愿抛出 OutOfMemoryError 使程序异常终止,也不会随意回收具有强引用的对象。
软引用(SoftReference):只具有软引用的对象,会在内存空间不足的时候被 GC;软引用常用来实现内存敏感的高速缓存。
弱引用(WeakReference):只被弱引用关联的对象,无论当前内存是否足够都会被 GC,即下一次 GC 时会被回收;强度比软引用更弱,常用于描述非必需对象;常用于解决内存泄漏的问题
虚引用(PhantomReference):仅持有虚引用的对象,无法通过该引用获取该对象,具有即时失效特性。在任何时候都可能被 GC;常用于跟踪对象被GC 回收的活动;必须和引用队列(ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
Integer 会缓存-128~127之间的数据,可以直接通过==比较,区间之外的数据在堆上产生,不会复用
字符串相关类包括 String、StringBuffer、StringBuilder
String 是只读字符串,典型的 immutable 对象,对他的任何改动其实都是创建一个新对象
String 对象赋值操作后,会在常量池缓存,如果下次申请创建对象时,缓存中已经存在,则直接返回相应引用给创建者
StringBuffer 则可以再原对象上进行修改,是线程安全的
StrungBuilder是1.5之后出现的类,与StringBuffer都是继承自AbstractStringBuilder,都是通过 super 调用父类的方法,都是通过字符数组的形式存储字符串,非线程安全
循环体中不推荐字符串直接相加,应该用 StringBuildr.append() 方法,如果直接相加的话,相当于每次都 new 一个 StringBuilder 对象再执行 append 操作,造成资源浪费
异常与日志
异常的分类
所有的异常都是 Throwable 的子类,分为 Error(致命异常)和 Exception(非致命异常),Exception 又分为 checked 异常和 unchecked 异常
Throwable
可预测:忘记带护照
需捕捉:去机场路上车子抛锚,必须处理,可以通过更换交通工具处理
可透出:票检机器异常,交给航空公司处理,无需关心
无能为力、引起注意型:堵车
力所能及、坦然处理型:飞机延误
Error:机场地震
Exception【是否可以提前预测】
checked(需要try cache代码)
unchecked(RuntimeException)
JVM
字节码
Java 为了实现跨平台,研发出了 JVM 即 Java 虚拟机,Java 字节码跑在 Java虚拟机上,由 JVM 生成机器可识别的机器码。
Java 也发明了类似汇编语言的指令集
解释执行:直接解释给 CPU
编译执行:先编译成 CPU 可执行的机器码
JIT 编译与解释混合执行
JVM 通过热点代码统计分析,识别高频的方法调用。循环日、公共模块等,基于强大而 JIT 动态编译可以将热点代码转换成机器码,直接交给 CPU 执行。
2、类加载过程
类加载时将.class字节码文件实例化成 Class 对象并进行相关的初始化的过程
加载、链接(验证、准备、解析)、初始化、使用、卸载
双亲委派模型:
低层次的当前类加载器,不能覆盖高层次的类加载器已经加载的类,如果低层次的加载器想要加载一个未知类,要非常礼貌的向上逐级询问,
请问:“这个类已经加载了吗?”
被询问的高级层次会自问:“我是否已经加载过此类?”,“如果没有,是否可以加在此类”。
如果以上两个问题都为否,才可以让当前类加载器加载这个未知类
加载(Loading):通过类的全限定名来获取定义此类的二进制字节流;将该二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构,该数据存储数据结构由虚拟机实现自行定义;在内存中生成一个代表这个类的java.lang.Class对象,它将作为程序访问方法区中的这些类型数据的外部接口
验证(Verification):确保Class文件的字节流中包含的信息符合当前虚拟机的要求,包括文件格式验证、元数据验证、字节码验证和符号引用验证。
准备(Preparation):为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆中;设置类变量初始值,通常情况下零值。
解析(Resolution):虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化(Initialization):是类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制。
内存布局
线程私有的:
程序计数器
虚拟机栈
本地方法栈
线程共享的:
堆
方法区(1.8后改为元数据区)
堆区:
堆区是OOM的主要发源地,它存储着几乎所有的实例对象,垃圾由垃圾回收器自动回收
堆分成两大块,新生代和老年代
新生代=1个Eden+2个Survivor(s0/s1)
Eden装满时会触发YGC
垃圾回收时,没有用到的对象直接清除,还存活的对象移送到Survivor区
每次YGC的时候,将Survivor中存活的对象复制到另一个区,将当前区清除,也就是每次只用一个区,当对象过大直接移到老年代。
每个对象都有一个计数器,最多来回交换14次就要进老年代
元空间:
JDK1.8使用元空间替换永久代
区别于永久代,元空间在本地内存中分配
永久代中字符串常量移到堆内存
其他:类元信息、字段、静态常量、方法、常量等都移动到元空间
虚拟机栈:
JVM是基于栈的运行结构
每个方法从开始到执行就是栈帧从入栈到出栈的过程
在活动栈中,只有位于栈顶的栈帧才是有效的,成为当前栈帧
栈帧结构:局部变量表、操作栈、动态链接、方法返回地址
局部变量表
存放方法参数和局部变量的区域
操作栈
在方法执行过程中,会有各种指令往栈中写入和提取信息
public int simpleMethod(){
int x=13;
int y=14;
int z=x+y;
return z;
}
///字节码顺序如下
BIPUSH 13 //常量13压入操作栈
ISTORE_1 //并保存到局部变量表的store1中
BIPUSH 14 //同上
ISTORE_2 //
ILOAD_1 //把局部变量表的store1变量压入操作栈
ILOAD_2 //同上
IADD //将栈顶两元素相加并压回操作栈
ISTORE_3 //将栈顶元素存储到局部变量表的istore3
ILOAD_3 //取至栈顶
IRETURN //返回栈顶元素
局部变量表就像一个中药柜,里面很多抽屉,某些指令可以直接在抽屉里进行
操作栈就像一个很深的桶,任何时候只能对桶口操作
常见的 i++与++i 区别?
a=i++
0:iload 1
1:iinc 1,1
4:istore_2
a=++i
0:iinc 1,1
3:iload 1
4:istore_2
iload从局部变量表的1号抽屉里取出一个数,压入操作栈,然后再抽屉里进行++操作,并不影响栈顶数值。
i++并非原子操作,计时通过volatile关键字修饰在,多个线程同时写的情况下也会产生数据覆盖的问题。
动态链接
每个栈帧总包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态链接
方法返回地址
程序有两种退出,正常退出和异常退出方法退出相当于弹出当前栈帧
返回值压入上层调用的栈帧
异常信息抛给能够处理的栈帧
PC计数器指向方法调用后的下一条指令
本地方法栈:
-
本地方法栈主外,为 Native 方法服务
程序计数器:
-
任何一个时刻,一个处理器的一个内核最后能执行一个线程中的一条指令,这样会导致经常的中断和恢复
每个线程创建后都会产生自己的程序计数器和栈帧,程序计数器存放执行指令的偏移量和行号指示器等,线程的执行或恢复都要依靠程序计数器
对象实例化
确认类元信息是否存在
分配对象内存
设定默认值
设置对象头
执行 init 方法
垃圾回收
引用计数器法,可达性分析法,GCRoot
静态属性中引用的对象、常量中引用的对象、虚拟机栈中引用的对象,本地方法栈中引用的对象可作为 GC Roots
垃圾回收方法
标记--清除 连续碎片,申请大对象容易出发 YGC
标记--整理 整理慢
标记--复制(主流) 浪费一半空间
垃圾回收器
Serial
串行单线程,会STW,影响性能
CMS (标记--清除)
初始标记、并发标记、重新标记、并发清除
1、3会触发STW
G1 (标记--复制)
G1具备压缩功能,能避免碎片问题
将堆分为若干大小的区域,四种类型
优先回收垃圾最多的区域
有良好的空间整合能力,不会产生大碎片
一大优势是可以预测回收时间
数据结构与集合
数据结构
线性结构、树结构、图结构
集合框架图
按照单个元素存储的Collection,在继承树中set、list都实现了Collection接口
第二类是按照key-value存储的map
以上两类集合无论是数据存取还是遍历,都存在较大差异
具体类图看《码出高效》152页
List 集合
ArrayList 内部实现是数组,扩容时时新建数组并内容转移,访问快,插入删除慢
LinkedList 内部实现是双向链表,与 ArrayList 相比,插入删除块,访问慢
LinkedList 还是先了 Deque 接口,具有栈和队列的性质
Map 集合
Map 集合是以 Key-Value 作为存储元素实现的哈希结构
Map 指向 Collection 的箭头仅仅代表两个类之间的依赖关系
HashTable 因为性能瓶颈已经被淘汰(全表锁)
ConcurrentHashMap 在1.8后进行了大幅度的优化,高并发推荐使用
TreeMap 是 key 有序的 Map 集合
Set 集合
不允许出现重复元素
最常用 HashSet、TreeSet、LinkedHashSet 三个集合类
源码分析 是使用 HashMap 实现的,Key 保证元素的唯一性,Value 是固定位一个静态对象
不保证集合元素的顺序
TreeSet 底层是 TreeMap,底层为树结构,保证插入后的集合仍然有序
LinkedHashSet 继承自 HashSet,底层使用链表维护了元素插入顺序
集合初始化
ArrayList
使用无参构造方法时,默认大小是10
扩容:oldCapacity+(oldCapacity >> 1) 扩容1.5倍
扩容后可能超过整数的最大范围,出现负数,最后容量可能越扩越小,所有出现这种情况就返回(size+1)
HashMap
Capacity:HashMap存储容量大小 默认16
LoadFactor:负载引自 默认0.75
threshold:表示HashMap中能放入的元素的个数 默认 = Capacity * LoadFactor = 12
HashMap 的容量并不会在new的时候分配,实在第一次 put 的时候完成创建的,调用 inflateTable(int toSize)方法
为了提高运行速度,设定HashMap的容量为2^n次幂,这样的方式计算落槽位置更快
每次扩容都是变为2倍
数组与集合
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针
Arrays 是针对数组对象进行操作的工具类,包括排序、查找、对比、拷贝等操作
Arrays.asList(数组)
可以通过 set 方法修改元素的值,原有数组对应位置的值也会同时被改变
但是不能进行修改元素个数的任何操作
asList 返回的事 Arrays 的一个内部类,这个内部类是个内鬼,实现十分简单,它并没有实现集合个数修改的相关方法
元素的比较
equals 与 hashCode
上面两个方法用来标识对象,协作可以更快判断对象是否相等
根据生成的哈性将数据散列开来,可以使元素的读取更快,但是不可避免会出现哈希值冲突,因此当hashCode相同时,还需要再调用equals进行一次值的比较,但是,若hashCode不同直接判定Object不同,跳过equals比较,加快了处理速度
如果两个对象的equals比较像等,那么两者的hashCode也必须相同
任何时候覆写equals必须要同时覆写hashCode
从代码角度分析也印证了hashCode是根据对象地址进行相关计算得到的int型的数值
如果用自定义对象作为Map的键,必须重写两个方法
hash()方法是对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
fail-fast机制
通常出现在对集合的遍历过程中,它是一种对集合遍历时的错误检测机制,在遍历中途出现意料之外的修改时,通过unchecked异常暴力反馈出来
点名,中途进人,同学起哄说点错了,需要重新点名
主列表的个数操作,均会导致sublist(子列表)的遍历、添加、删除、产生fail-fast异常
子列表无法序列化,子列表的修改也会导致主列表发生改变
List<String> list=new ArrayList<>();
list.add("ine");
list.add("two");
list.add("three");
for(String s:list){
if("two".equals(s)){
list.remove(s);
}
sout(list);
//最终也会输出one three
}
具体原因解释,在集合遍历时维护一个初始值为0的游标,从头到尾的进行扫描,在 cursor==size 时停止遍历,执行 remove 后 size=2;这时候 cursor 也==2,并没有机会执行到 next() 的第一行代码,所以不会产生异常
1.hashNext(){cursor!=size}
2.next()
3.remove()
4.System.arrayCopy()
使用Iterator遍历,并发时需要加锁
Iterator<String> iterator=list.iterator();
while(iterator.hashNext()){
synchronized(对象){
String item = iterator.next();
if(删除元素的条件){
iterator.remove();
}
}
}
或者使用并发容器CopyOnWriteArrayList代理ArrayList
主要的Map集合类
HashTab的抽象类是Dictionary,其余的事AbstractMap
红黑树
定义:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NILL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
有红必有黑,红红不相连
删除查找的最大时间复杂度是logn
树的左右子节点不存在,默认是黑色的
红黑树的任何旋转在三次之内均可完成
TreeMap
插入的Key必须实现Comparable或者提供额外的比较器Comparator,所以key不允许为null
TreeMap不同于HashMap,不一定要复写equals和hashCode方法去重,因为TreeMao依靠Comparable或者Comparator来实现Key的去重
一堆红黑树的旋转操作
HashMap
1.8之前出现的死链现象
1.7是先扩容,移动元素后进行增加元素操作
1.8是现增加元素后扩容,再移动元素
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//第一处:将e的next指向扩容后的位置i
e.next = newTable[i];
//第二处:将e赋给newTable[i]完成e的转移
newTable[i] = e;
//向后移动e
e = next;
}
}
}
1.7扩容后拉链上的数据顺序会变倒,因为是从头结点开始的转移
1.8采用从尾节点开始转移的操作,保证有序性
ConcurrentHashMap
JDK8 之前的 ConcurrentHashMap 采用分段锁的设计理念,相当于 HashMap和HashTabel的折中版
分段锁是由内部类 Segment 实现的,它继承与 ReetrantLock,用来管理它管辖的各个 HashEntity
JDK11 对 JDK7 的 ConcurrentHashMap 进行了改造
取消分段锁机制,进一步降低冲突
引入红黑树结构:
同一哈希槽上的元素个数超过8且table的容量大于等于64,则由链表转换为红黑树
当槽上的元素个数减小到6,由红黑树转回链表
使用了更加优化的方式统计集合内元素的数量
在转化过程中使用同步块锁住当前槽的首元素,防止其他线程对当前槽进行增删改操作,转换完成后利用 CAS 替换原有链表
六、并发与多线程
原子性、可见性、有序性
线程安全(核心理念:要么只读,要么加锁)
并行与并发:
并行:多个人同时使用一个话筒唱歌
并发:多个人轮流使用一个话筒唱歌
线程是 CPU 调度的基本单位,合适的线程数才能让 CPU 的资源被充分利用
线程的五种状态:新建、就绪、运行、阻塞、终止
NEW:
继承 Thread 类,不符合里氏替换原则
实现 Runnable 接口(推荐)可以使编程更加灵活
Callable 接口的 call()
v call() throw Exception;
可以通过 call 获取返回值
可以抛出异常
public class NewCallable implements Callable{
public Object call() throws Exception {
return null;
}
public static void main(String[] args) {
NewCallable callable=new NewCallable();
FutureTask task=new FutureTask(callable);
Thread thread=new Thread(task);
thread.start();
}
}
RUNNABLE:
就绪状态,即调用start()之后线程的状态
不可以多次调用start方法,否则会抛出异常
RUNNING:
线程可能会由于某些原因而退出RUNNING异常、锁、调度等
BLOCKED:
同步阻塞:
锁被其他线程占用
主动阻塞:
调用Thread的某些方法主动让出CPU执行权,如sleep()、join()等
等待阻塞:
执行了wait()[Object方法]
DEAD:
run()结束或者异常退出,此过程不可中转
死锁:
死锁是怎么导致的?如何定位死锁 某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间互相等待的连续循环,没有哪个线程能继续。这被称之为死锁。当以下四个条件同时满足时,就会产生死锁:
(1) 互斥条件。任务所使用的资源中至少有一个是不能共享的。
(2) 任务必须持有一个资源,同时等待获取另一个被别的任务占有的资源。
(3) 资源不能被强占。
(4) 必须有循环等待。一个任务正在等待另一个任务所持有的资源,后者又在等待别的任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。 要解决死锁问题,必须打破上面四个条件的其中之一。在程序中,最容易打破的往往是第四个条件。
什么是锁
锁主要提供了两种特性:互斥性、不可见性
锁也由最初的悲观锁发展到现在的乐观锁、偏向所、分段锁等
Lock是JUC包(Java并发包)的顶层接口,他的实现并没有用到synchronizd,而是用到了volatile的可见性
JVM底层通过监视锁来实现synchronized同步的,监视即monitor,是每个对象与生俱来的一个隐藏字段,使用synchronized时,JVM会找到兑现的monitor,再根据monitor的状态进行加锁,解锁的判断
字节码中:monitorenter、monitorexit
如果使用 monitorenter 进入时 monitor 为0,表示该线程可以持有 monitor 的后续代码,并将 monitor+1,如果当前线程已经持有了monitor,那么monitor 继续+1,如果 monitor 非0,其他线程就会进入等待状态
线程安全
不可变、绝对线程安全(不管运行时环境如何,都不需要额外的做同步措施)、相对线程安全
线程同步方式:
每一个线程的 Thread 对象都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashMap 为键,以本地线程变量为值的k-V值对,ThreadLocal 对象是当前线程 ThreadLocalMap 的访问入口,每个 ThreadLocal 对象包含了一个独一无二的 threadLocalHashCode值,使用这个值就可以在线程的K_V值对中找回对应的本地线程变量
ABA 问题
等待可中断:持有锁的线程长期不释放的时候,等待线程可以选择放弃
可实现公平锁:多个线程在同时等待锁时,必须按照申请锁的顺序获得锁
锁绑定多个条件:ReentrantLoack 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait(),notify(),方法可以实现一个隐含的条件,如果要和多个条件关联,就不得不额外添加一个锁
互斥同步
最基本的互斥手段是 synchronized 关键字,该关键字经过编译后,会在同步块的前后行成 monitorenter、monitorexit 两个字节码指令
这两个字节码都需要一个引用类型的参数来致命需要锁定和解锁的对象,如果明确指定了对象参数,那就是这个对象的 reference,如果没有明确的指定,那就根据 synchronized 修饰的事实例方法还是类方法,去取对应的对象实例或 Class 对象作为锁对象
执行 monitorenter 指令时,首先尝试获取对象锁,把锁的计数器+1,执行monitorexit 的时候锁计数-1,为0时,释放锁
synchronized 同步块对于同一线程来说是可重入的,不会出现自己锁死自己的问题
除了synchronized之外,我们看还可以使用JUC包中的可重入锁ReentrantLock来实现同步,在基本用法上,ReentrantLock与synchronized 很相似,他们具备一样的线程重入特性
ReentrantLock 增加了一些特点:
非阻塞同步
CAS:内存地址V,旧的预期值A,新的预期值B
AtomicInteger
无同步方案
可重入代码
线程本地存储
线程池
管理、复用线程,控制最大并发数
实现任务队列的缓存策略和拒绝机制
实现某些与时间相关的功能,如定时执行、周期执行
通过Executors.newXXX()创建新线程
使用注意点:
合理设置各类参数,应根据实际业务场景来设置合理的工作线程数
线程资源必须通过线程池提供,不能在程序中自行显式创建线程
线程池构造参数:略
5、ThreadLocal
每一个线程的 Thread 对象都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashMap 为键,以本地线程变量为值的k-V值对,ThreadLocal对象是当前线程ThreadLocalMap的访问入口,没有货ThreadLocal 对象包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程的K_V值对中找回对应的本地线程变量
ThreadLocal 有个静态内部类 ThreadLocalMap,它还有个静态内部类 Entry
ThreadLocal 与 ThreadLocalMap 有三组对应的方法:get(),set(),remove()
Entry 继承自 WeakReference,只有一个value属性值,它的key是ThreadLocal 对象
所有 Entry 对象都被 ThreadLocalMap 类的实例对象 threadLocals 持有,当线程对象执行完毕时,线程对象内的实例属性均会被垃圾回收,Entry 中ThreadLocal 的弱引用,即使在线程执行中,只要 ThreadLocal 对象引用被设置成 null,Entry 的 KEY 在下一次垃圾回收时被回收,而在使用 ThreadLocal 使用 set 和 get 时,又自动将那些 key==null 的 value 设置为 null,使 value能够被垃圾回收,避免内存泄漏,但是理想很丰满,现实很骨感
线程使用 ThreadLocal 有三个重要方法:
set(): 如果没有 set 操作的 ThreadLocal,容易引起脏数据问题
get(): 始终没有 get 操作的 ThreadLocal 对象是没有意义的
remove(): 如果没有 remove 操作,容易引起脏数据问题
副作用
脏数据
线程复用会产生脏数据,由于线程池会重用 Thread 对象,那么与 Thread绑定的类的静态属性 ThreadLocal 变量也会被重用
如果在实现线程的 run 方法中不显示的调用 remove(),来清理与线程有关的 ThreadLocal 信息,那么倘若下一个线程不调用 set 设置初始值,就很可能 get 到重用的线程信息,包括 ThreadLocal 对象所关联线程对象的 value值
内存泄漏
在源码中提示使用 static 关键字来修饰 ThreadLocal。
在此场景下,寄希望于 ThreadLocal 对象失去引用后,触发垃圾回收机制来回收 Entry 的 Value 就不显示了
如果不进行 remove() 操作,那么这个线程执行完成后,通过 ThreadLocal对象持有的 Value 对象是不会被释放的
使用的 Key 值是一个 WeakReference 类型的值(弱引用会在下一次 GC 时马上释放而不管是否被引用)。那么如果这个 Key 在 GC 时被释放了,就会导致 Value 永远都不会被调用到,但是如果线程不结束,又一直存在。
以上两个问题记得解决方法很简单,就是在每次调用 ThreadLocal 时,必要时及时调用 remove(),方法处理。
ThreadLocal 为什么会内存泄漏
ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。
其实,ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的 get(),set(),remove() 的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用 static 的 ThreadLocal,延长了 ThreadLocal 的生命周期,可能导致的内存泄漏(参考 ThreadLocal 内存泄露的实例分析)。
分配使用了 ThreadLocal 又不再调用 get(),set(),remove() 方法,那么就会导致内存泄漏。
为什么使用弱引用
key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是ThreadLocalMap 还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
key 使用弱引用:引用的ThreadLocal 的对象被回收了,由于 ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次 ThreadLocalMap调用 set,get,remove 的时候会被清除。
比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value在下一次ThreadLocalMap 调用 set,get,remove 的时候会被清除。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
总结
欢迎补充其他部分的总结!
推荐阅读:
欢迎关注我的公众号,学习技术或投稿
长按上图,识别图中二维码即可关注