[TOC]
第1章 Java基础
1.1 序列化
Java序列化就是将一个对象转化为一个二进制表示的字节数组,通过保存或者转移这些二进制数组达到持久化的目的。要实现序列化,需要实现java.io.Serializable接口。
反序列化是和序列化相反的过程,就是把二进制数组转化为对象的过程。在反序列化的时候,必须有原始类的模板才能将对象还原。
- transient 关键字阻止变量序列化
1.2 反射机制
1.2.1 定义
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
1.2.2 反射机制相关类
类名 | 用途 |
---|---|
Class类 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类 | 代表类的成员变量(成员变量也称为类的属性) |
Method类 | 代表类的方法 |
Constructor类 | 代表类的构造方法 |
1.3 泛型
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
1.3.1 泛型方法
public static <E> printArray(E[] array){
for(E element:array){
System.out.printf("%s",element);
}
}
1.3.2 泛型类
public class Box<T>{
privat T t;
public void set(T t){
this.t = t;
}
public T get(){
return t;
}
}
1.3.3 类型通配符
- 类型通配符一般使用?代替具体的类型参数。
- <? extends T>表示该通配符所代表的类型是 T 类型的子类。
- <? super T>表示该通配符所代表的类型是 T 类型的父类。
1.3.4 类型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的 List<Object>和 List<String>等类型,在编译之后都会变成 List。 JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。
1.4 自动装箱和拆箱
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
1.5 static关键字
- 静态变量
对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。 - 静态方法
- 静态代码块
- 静态内部类
静态的内部类可以直接作为一个普通类来使用,而不需实例化外部类。 - 静态引入包
然后在这个类中,就可以直接用方法名调用静态方法,而不必用ClassName.方法名 的方式来调用。
第2章 Java集合
2.1 面试题
2.1.1 HashMap和TreeMap的区别
- HashMap:基于哈希表实现。数组方式存储key/value,线程非安全,允许null作为key和value,key不可以重复,value允许重复,不保证元素顺序,要求key必须重写equals和hashcode方法,可以调优初始容量和负载因子,需要扩容。
- TreeMap:基于红黑二叉树的NavigableMap的实现,线程非安全,不允许null,key不可以重复,value允许重复,存入TreeMap的元素应当实现Comparable接口或者实现Comparator接口,会按照排序后的顺序迭代元素,两个相比较的key不得抛出classCastException。主要用于存入元素的时候对元素进行自动排序,迭代输出的时候就按排序顺序输出
2.1.2 HashSet和TreeSet的区别
- HashSet保存的数据是无序的,TreeSet保存的数据是有序的。
- TreeSet 依靠的是Comparable来区分重复数据;HashSet依靠的是hashCode()、equals()来区分重复数据
- TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值。
HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复。
2.1.3 队列和链表的区别
- 队列:先进先出;队尾插入,队头删除
- 链表:有一个指针,该指针指向下一个元素的地址。任意位置插入、删除
2.1.4 HashMap线程不安全表现
- put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。 - resize死循环
使用头插法,链表的顺序会翻转。
第3章 JVM
3.1 类加载机制
3.1.1 类加载过程
加载-->连接--初始化
加载-->验证-->准备-->解析-->初始化
- 加载
Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。 - 验证
确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 - 准备
为类的静态成员分配内存,并设置默认初始值。 - 解析
将常量池中的符号引用替换为直接引用 - 初始化
初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
3.1.2 类加载器
- 启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的, 或通过-Xbootclasspath 参数指定路径中的, 且被虚拟机认可(按文件名识别, 如 rt.jar) 的类。 - 扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。 - 应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。
3.1.3 双亲委派
image当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父
类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候, 子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
- 避免重复加载;保证安全,java核心api中定义类型不会被随意替换
3.2 JVM内存结构
imageJVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区
域【JAVA 堆、方法区】、直接内存。
3.2.1 程序计数器
是当前线程所执行的字节码的行号指示器
3.2.2 虚拟机栈
是描述 java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
3.2.3 本地方法栈
同虚拟机栈,Native方法
3.2.4 堆
对象和数组
3.2.5 方法区/永久代
存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。运行时常量池是方法区的一部分。
3.2.6 MetaSpace Java8元数据区
3.2.7 直接内存
NIO编程
3.3 运行时堆内存划分
- 新生代(Eden 区、 From Survivor 区和 To Survivor 区),默认占堆的1/3空间。
- 老年代
- 永久代
3.4 垃圾回收
3.4.1 如何确定垃圾
- 引用计数法
- 可达性分析
通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
3.4.2 GC算法
3.4.2.1 标记清除算法(Mark-Sweep)
分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重。
3.4.2.2 复制算法(copying)
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
3.4.2.3 标记整理算法(Mark-Compact)
标记阶段和 Mark-Sweep 算法相同, 标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
3.4.2.4 分代收集
- 新生代复制算法
- 老年代标记整理
当对象在 Survivor 区躲过一次 GC 后,其GC年龄就会+1。 默认情况下年龄到达 15 的对象会被移到老生代中。
3.4.3 垃圾收集器
3.4.3.1 Serial 垃圾收集器(单线程、 复制算法)
Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工
作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。
3.4.3.2 ParNew 垃圾收集器(Serial+多线程)
Serial 收集器的多线程版本
3.4.3.3 Parallel Scavenge 收集器(多线程复制算法、高效)
Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器, 它重点关注的是程序达到一个可控制的吞吐量(Thoughput, CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
3.4.4 Serial Old 收集器(单线程标记整理算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,
这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
在 Server 模式下,主要有两个用途:
- 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。
-
作为年老代中使用 CMS 收集器的后备垃圾收集方案。
image
3.4.5 Parallel Old 收集器(多线程标记整理算法)
image3.4.6 CMS 收集器(多线程标记清除算法)
一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
过程
image1.初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
2.并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
3.重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对4.象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
3.4.7 G1收集器
Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器, G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。
3.5 JAVA 四中引用类型
3.5.1. 强引用
在 Java 中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
3.5.2. 软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
3.5.3. 弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
3.5.4. 虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态。
3.6 面试题
3.6.1 什么情况出发MinorGC
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间。
1、如果大于的话,直接执行minorGC
2、如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
3、如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
4、如果大于的话,执行minorGC
3.6.2 触发FullGC
- 老年代空间不足
如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。 - 持久代空间不足
如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC - YGC出现promotion failure
promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC. - 统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
在发生YGC是会判断,是否安全,这里的安全指的是,当前老年代空间可以容纳YGC晋升的对象的平均大小,如果不安全,就不会执行YGC,转而执行Full GC。 - 显示调用System.gc
3.6.3 内存泄漏
无用对象内存无法回收。
- 如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
因此,解决内存泄漏的一个方法,就是尽量降低变量的作用域,以及及时把对象复制为可清理对象(null) - 容器使用时的内存泄漏
- 各种连接
3.6.4 内存溢出
通俗的讲就是内存不够。
- 堆内存不足
大对象分配;可能存在内存泄漏 - 永久代/元空间溢出
在Java7之前,频繁的错误使用String.intern方法;;生成了大量的代理类,导致方法区被撑爆,无法卸载;应用长时间运行,没有重启 - GC overhead limit exceeded
垃圾回收超时内存溢出,超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。 - 方法栈溢出
创建了大量线程 - 分配超大数组
- swap区溢出
第4章 JAVA多线程并发
4.1 线程
4.1.1 什么是线程
操作系统调度的最小单元,拥有计数器、堆栈和局部变量等属性。
4.1.2 线程的状态
- 新建(NEW) new 创建
- 就绪(RUNNABLE)调用start()方法后
- 运行(RUNNING) 就绪状态的线程获取CPU时间,执行run()方法
- 阻塞(BLOCKED) 阻塞于锁
- 等待(WAITING)
- 超时等待(TIME_WAITING) 可以在指定的时间自行返回
-
终止(TERMINATED)线程执行完毕
Java线程状态转换
4.1.3 线程的基本方法
4.1.3.1. 线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后, 会释放对象的锁。因此, wait 方法一般用在同步方法或同步代码块中。
4.1.3.2. 线程睡眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致
线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
4.1.13.3. 线程让步(yield)
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,
优先级高的线程有更大的可能性成功竞争得到 CPU 时间片, 但这又不是绝对的,有的操作系统对
线程优先级并不敏感。
4.1.10.4. 线程中断(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。 这
个线程本身并不会因此而改变状态(如阻塞,终止等)。
- 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线
程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。 - 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出
InterruptedException,从而使线程提前结束 TIMED-WATING 状态。 - 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异
常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。 - 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止
一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以
根据 thread.isInterrupted()的值来优雅的终止线程。
4.1.3.5. Join 等待其他线程终止
join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞
状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
4.1.10.6. 线程唤醒(notify)
Object 类中的 notify() 方法, 唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象
上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调
用其中一个 wait() 方法,在对象的监视器上等待, 直到当前的线程放弃此对象上的锁定,才能继
续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞
争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
4.2 wait和sleep的区别
- wait()是Object类方法,sleep()属于Thread类
- wait()线程会释放对象锁,wait()不会。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。调用wait()的线程需要其他线程调用notify()或notifyAll()唤醒。
- wait()方法必须放在同步控制方法和同步代码块中使用,sleep()方法则可以放在任何地方使用。
4.3 Java内存模型(JMM)
多线程通信方式有:共享内存和消息传递。Java并发采用的是共享内存模型。
围绕着在并发过程中如何处理原子性、可见性和有序性建立。
Java内存模型
4.3.1 指令重排序
4.3.2 顺序一致性
4.3.3 happens-before
4.3.4 内存屏障
4.4 volatile
Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。
- 变量可见性
- 原子性
- 禁止指令重排序
- 当写一个volatile变量时,JMM会把线程对应的本地内存的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程从主内存读取共享变量。
4.5 Java锁
4.5.1 乐观锁和悲观锁
- 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的, CAS 是一种更新的原子操作, 比较当前值跟传入
值是否一样,一样则更新,否则失败。 - 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是 Synchronized,AQS框架下的锁则是先尝试 cas乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock。
4.5.2 自旋锁
自旋锁原理非常简单, 如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
4.5.3 公平锁和非公平锁
- 公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 - 非公平锁(Nonfair)
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- Java 中的 synchronized 是非公平锁, ReentrantLock 默认的 lock()方法采用的是非公平锁。
4.5.4 共享锁和独占锁
java 并发包提供的加锁模式分为独占锁和共享锁。
- 独占锁
独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线
程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。 - 共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如: ReadWriteLock。 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
- java 的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。
4.5.5 可重入锁
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁。
4.5.6 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
死锁产生的四个必要条件。
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
死锁的预防
1、有序资源访问
2、超时机制
4.6 synchronized
4.6.1 synchronized 作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是 Class实例
- 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
4.6.2 对象头
synchronized用的锁存在Java对象头中。mark word被分成两部分,lock word和标志位。
4.6.3 锁的升级
四种状态:无锁、偏向锁、轻量级锁、重量级锁。
- 偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只判断对象头的Mark Word里是否存储着指向当前线程的偏向锁。 - 轻量级锁
轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。 - 重量级锁
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么
Synchronized 效率低的原因。因此, 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁” 。
4.7 ReentantLock
第5章 MySQL
1.悲观锁和乐观锁
2.表级锁和行级锁
网友评论