剑指offer
[TOC]
剑指Offer
总览
- Java底层知识:JVM
- Java底层知识:GC相关
- Java多线程与并发
- Java多线程与并发-原理
- Java常用类库与技巧
- Java框架-Spring
- 计算机网络
- 数据库MySQL
- 缓存Redis
- 服务器Linux
Java底层知识:JVM
问题一:谈谈你对Java的理解?
- 平台无关性:一次编译,到处运行
- GC:不用像C++那样手动释放堆内存
- 语言特性:泛型、发射、Lamble表达式
- 面向对象:封装、继承、多态
- 类库:JUC并发库、网络库、I/O库
- 异常处理机制
问题二:Compile One,Run AnyWhere(平台无关性)如何实现?
- 编译时
- javac指令:将.java源码,编译为字节码
javap -c
:对代码(.class)进行反汇编
- 运行时
- jvm解释:转换为特定平台的执行指令
标准答案:Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java 语言在不同的平台上
运行时不需要进行重新编译,Java虛拟机在执行字节码的时候,把字节码转换成具体平台上
的机器指令。
问题三:JVM如何加载.class文件?
- Java虚拟机:屏蔽底层操作系统的不同,并且减少基于原生语言开发的复杂性。JVM是内存虚拟机,所有信息都存储在内存中。两大特性:JVM内存模型,GC。
Class Loader:依据特定格式,加载class文件到内存。
Execution Engine:对命令进行解释
Native Interface:融合不同开发语言的原生库为Java所用。
在Java API中,一个Native方法往往意味着这个方法没有使用或无法使用平台无关的手段来实现(Thread类)。(当然也可能是为了执行效率而使用Native方法,通常最高效的手段就是平台相关的手段)
Runtime Data Area:JVM内存空间结构模型
问题四:谈谈Java反射
标准答案:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,
都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
package javaOffer._1; |
问题五:类从编译到执行的过程
- 编译器将Robot.java源文件编译为Robot.class字节码文件
- ClassLoader将字节码转换为JVM中的Class < Robot>对象
- JVM利用Class < Robot>对象实例化为Robot对象
问题六:谈谈ClassLoader
ClassLoader在Java中有着非常重要的作用, 它主要工作在Class装载的加载阶段,其主要作用是从系统外
部获得Class二进制数据流。它是Java的核心组件,所有的Class 都是由ClassLoader 进行加载的,
ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始
化等操作。
- BootStrapClassLoader:C++编写,加载核心库java.*
- ExtClassLoader:Java编写,加载扩展库javax.*
- AppClassLoader:Java编写,加载程序所在目录
- Custom(自定义)ClassLoader:Java编写,定制化加载
- java.lang.ClassLoader#loadClass(java.lang.String, boolean)
- java.lang.ClassLoader#findClass
- java.lang.ClassLoader#defineClass(byte[], int, int)
问题七:谈谈类加载器的双亲委派机制
- 自底向上检查Class<?>是否已经加载了,自顶向下尝试加载类
- 委托机制:避免多份同样字节码的加载
问题八:谈谈类的加载方式
- 隐式加载:new
- 显示加载:loadClass,forName等
类的装载过程
- 加载:通过ClassLoader加载class文件字节码,生成class对象
- 链接:
- 校验:检查加载的class的正确性和安全性
- 准备:为类变量分配存储空间并设置类变量初始值
- 解析:JVM将常量池内的符号引用转换为直接引用
- 初始化:执行类变量赋值和静态代码块
LoadClass和forName的区别
- Class.forName:得到的class是已经初始化完成的
- ClassLoader.loadClass:得到的class是还没有链接
- 二者应用:forName(),加载初始化了静态资源(Mysql的Driver)。loadClass,spring延时加载。
问题九:Java内存空间结构模型
线程角度
- 线程私有:程序计数器(no OOM)、虚拟机栈(OOM)、本地方法栈(OOM)
- 线程共享:MetaSpace(类加载信息&&OOM)、堆(数组和类对象OOM。【常量池(字面量和符号引用量)OOM】)
存储角度
程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑)
- 改变计数器的值来选取下一 条需要执行的字节码指令
- 和线程是一对一的关系即“线程私有”
- 对Java方法计数,如果是Native方法则计数器值为Undefined
- 不会发生内存泄露
Java虚拟机栈(Stack)
Java方法执行的内存模型
包含多个栈帧(局部变量表、操作栈、动态链接、返回地址)
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、超声消费变量
Classfile /Users/codingprh/IdeaProjects/TeslaV2/src/javaOffer/_1/ByteCodeSample.class Last modified 2020-2-23; size 283 bytes MD5 checksum b490af96007ad2ac153fdd3c0ca00cc1 Compiled from "ByteCodeSample.java" public class javaOffer._1.ByteCodeSample minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #3.#12 // java/lang/Object."<init>":()V #2 = Class #13 // javaOffer/_1/ByteCodeSample #3 = Class #14 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 add #9 = Utf8 (II)I #10 = Utf8 SourceFile #11 = Utf8 ByteCodeSample.java #12 = NameAndType #4:#5 // "<init>":()V #13 = Utf8 javaOffer/_1/ByteCodeSample #14 = Utf8 java/lang/Object { public javaOffer._1.ByteCodeSample(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 10: 0 public static int add(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=2 0: iconst_0 (c=0,压入操作数栈) 1: istore_2 (c=0,操作数出栈,存储到局部变量表) 2: iload_0 (局部变量第一个元素入操作数栈) 3: iload_1 (局部变量第二个元素入操作数栈) 4: iadd (操作数栈顶两个元素相加) 5: istore_2 (操作数出栈,存储到局部变量表2位置) 6: iload_2 (局部变量第二个位置的元素压入操作数栈) 7: ireturn LineNumberTable: line 13: 0 line 14: 2 line 15: 6 } SourceFile: "ByteCodeSample.java" /** * 这是java源码,通过javap -v 反汇编的代码 */ public static int add(int a, int b) { int c = 0; c = a + b; return c; }
#### 本地方法栈
与虚拟机栈相似,主要作用于标记了native的方法
#### 堆(Heap)
- 对象实例的分配区域
- GC管理的主要区域
### 问题十:元空间(MetaSpace)与永久代(PermGen)的区别
- 元空间和永久代都是方法区的实现。实现方式不同,都是存储类信息、方法信息。
- 在jdk1.7之后字符串常量池从永久代移除,移动到堆中。在jdk1.8之后移除了永久代,而使用元空间来实现方法区。
- 元空间使用本地内存,而永久代使用的是jvm内存(OOM:PermGen space)
- 元空间比永久代的优势:
1. 字符串常量池存在永久代中,容易出现性能问题和内存溢出
2. 类和方法的信息大小难以确定,给永久代的大小指定带来困难。
3. 永久代会为GC带来不必要的复杂性
### 问题十一:JVM三大性能调优参数-Xms -Xmx -Xss的含义
- -Xss:每个线程虚拟机栈的大小
- -Xms:堆的初始化值
- -Xmx:堆大小的最大值
- -xms和-xmx的大小,一般都设置为相同,避免扩容的时候,带来的内存抖动问题。
### 问题十二:Java内存模型中堆和栈的区别——内存分配策略
- 静态存储:编译时确定每个数据目标在运行时的存储空间需求
- 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
- 堆式存储:编译时或运行时模块入口都无法确定,动态分配
### 问题十三:Java内存模型中堆和栈的区别
- 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址。
- 管理方式:栈自动释放,堆需要GC
- 空间大小:栈比堆小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
- 效率:栈的效率比堆高
### 问题十四:元空间、堆、线程独占部分空间的联系-内存角度
```java
package javaOffer._1;
/**
* 描述:
* java内存模型
*
* @author Noah
* @create 2020-02-23 15:50
*/
public class ModelSample {
private String name;
public void sayHello() {
System.out.println("hello," + name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
int a = 1;
ModelSample ms = new ModelSample();
ms.setName("aa");
ms.sayHello();
}
}元空间:Class:ModelSample,Method:sayHello/getName/setName/main,Field:name
Java堆:Object:String(“aa”),Object:ModelSample
线程独占:reference:ms,aa在堆中的引用。pc
问题十五:不同jdk版本之间的intern()方法的区别-JDK6 vs JDK6+
String aa=new String(“aa”);
aa.intern();
JDK6 :当调用intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,
将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。JDK6+:当调用intern 方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,
如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中
不存在,则在池中创建该字符串并返回其引用。
package javaOffer._1; |
Java多线程与并发
问题一:进程和线程的区别?
扩展问题:内核态和用户态的区别,怎么切换?为什么要转换?什么是系统中断?内核态多线程轻量级进程,如何实现的?
进程的由来:
- 串行:初期的计算机智能串行执行任务,并且需要长时间等待用户输入
- 批处理:预先将用户的指令集中成清单,批量串行处理用户指令, 仍然无法并发执行
- 进程:进程独占内存空间,保存各自运行状态,相互间不干扰且可以互相切换,为并发处理任务提供了可能
- 线程:共享进程的内存资源,相互间切换更快速 ,支持更细粒度的任务控制 ,使进程内的子任务得以并发执行
- 进程是资源分配的最小单位,线程是cpu调度的最小单位。
- 所有与进程相关的资源,都被记录到PCB(进程控制块)中。
- 描述信息
- 控制信息
- 资源信息(程序段、数据集)
- cpu线程
- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
- 线程只由堆栈寄存器、程序计数器和TCB组成。
- 总结:
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响, 线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程程序健壮
- 进程的切换比线程的切换开销大
问题二:Java进程和线程的关系
- Java对操作系统提供的功能进行封装, 包括进程和线程
- 运行一个程序会产生一个进程,进程包含至少一个线程
- 每个进程对应一个JVM实例,多个线程共享JVM里的堆
- Java采用单线程编程模型,程序会自动创建主线程
- 主线程可以创建子线程,原则上要后于子线程完成执行
问题三:Thread中的start和run方法的区别?
//路径:hotspot-29ef249e9953/src/share/vm/prims/jvm.cpp |
- 答案:java.lang.Thread#start——>JVM_StartThread——>thread_entry——>java.lang.Thread#run
- 调用start()方法会创建一个新的子线程并启动
- run()方法只是Thread的一个普通方法的调用
问题四:Thread和Runnable是什么关系?
- Thread是实现了Runnable接口的类,使得run支持多线程。
- 因为类是单一继承原则,推荐多使用Runnable接口
问题五:如何给run()方法传参?
- 构造器传参
- 成员变量传参
- 回调函数传参
问题六:如何实现处理线程的返回值
- 主线程等待法
- 使用Thread类的join()阻塞当前线程以等待子线程处理完毕
- 通过Callable接口实现:通过FutureTask or 线程池获取
问题七:线程的状态
public enum State { |
- 注意点:已经运行完的线程,再执行start()方法,异常:java.lang.IllegalThreadStateException
问题八:sleep和wait的区别?
- sleep是Thread类的方法,wait是Object类的方法,都是native方法
- sleep()方法可以在任何地方使用
- wait()方法只能在synchronized方法或synchronized块中使用
- 最本质的区别:
- java.lang.Object#wait()不仅让出cpu,还会释放已经占有的同步资源锁。
- java.lang.Thread#sleep(long)只会让出cpu,不会导致锁行为的改变
问题九:notify和notifyAll的区别
两个概念:锁池(EntryList)和等待池(WaitSet),java的每个对象都拥有这两个属性。
锁池:假设线程A已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个synchronized方法(或者块),由于B、C线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程A所占用,此时B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池
等待池:假设线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁,同时线程A就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。
- notifyAll:会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会。
- notify:只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
问题十:谈谈你对java.lang.Thread#yield的理解
/** |
- 当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
- 不会影响锁的资源
问题十一:如何中断线程
- 已经被抛弃的方法:
- 通过调用stop()方法停止线程
- 通过调用suspend(挂起)和resume(恢复)方法
- 目前使用的方法:
- 调用java.lang.Thread#interrupt,通知线程应该中断了,需要被调用的线程配合中断。(只是一个hint提醒)
- ①如果线程处于被阻塞状态, 那么线程将立即退出被阻塞状态,并
抛出一个InterruptedException异常。 - ②如果线程处于正常活动状态,那么会将该线程的中断标志设置为
true。被设置中断标志的线程将继续正常运行,不受影响。
- ①如果线程处于被阻塞状态, 那么线程将立即退出被阻塞状态,并
- 调用java.lang.Thread#interrupt,通知线程应该中断了,需要被调用的线程配合中断。(只是一个hint提醒)
问题十二:线程状态以及线程状态的切换
Java多线程与并发-原理
问题一:谈谈什么不是线程安全的?
- 存在共享数据(也称临界资源)
- 存在多条线程共同操作这些共享数据
- 解决方案(互斥锁):同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作
- 互斥性:即在同一时间只允许一个线程持有某个对象锁 ,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。
- 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值) ,否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
- sychronized能够解决原子(互斥)性和可见性。锁的不是代码,锁的都是对象。
- 对象锁
- 类锁
问题二:谈谈synchronized底层实现原理
- Java对象头:
- Monitor:每个Java对象天生自带了一把看不见的锁
对象在内存中的布局:对象头、实例数据、对齐填充
- 对象头结构
Mark Word:默认存储对象的hashCode,分代年龄,锁类型,锁标志位等信息
Class Metadata Address:类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据
Monitor实现:c++源码
ObjectMonitor() {
_header = NULL;
_count = 0; //信号量
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //持有这个锁的线程
_WaitSet = NULL; //等待池
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //锁池
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
//synchronized字节码
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String hello
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
什么是重入?
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。
谈谈synchronized优化?
- 在早期版本synchronized属于重量级锁,依赖于核心态实现。用户态转与核心态的切换开销较大。
- 自适应自旋(Adaptive Spinning)
- 锁消除(Lock Eliminate)
- 锁粗化(Lock Coarsening)
- 轻量级锁(Lightweight Locking)
- 偏向锁(Biased Locking)
问题三:自旋锁和自适应自旋锁
- 很多情况下,共享数据的锁定状态持续时间较短,切换线程不值得(挂起和唤醒【用户态切换到内核态】)。
- 自旋:在多核时代,让等待的线程,占据cpu使用时间。
- 缺点:若锁被其他线程长时间占用,会带来很多性能的开销。
- JVM调优参数:-XX:PerBlockSpin,默认值为10
- 自适应自旋锁:
- 自旋次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
问题四:锁消除和锁粗化
- 锁消除:JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
- 锁粗化:通过扩大加锁的范围,避免反复加锁和解锁
问题五:synchronized的四种状态
- 无锁:
- 偏向锁:减少同一线程获取锁的代价。
- 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。
- 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结
构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,
即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线
程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的
操作。 - 不适用于锁竞争比较激烈的多线程场合
- 轻量级锁:
- 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情
况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。 - 适应的场景:线程交替执行同步块
- 若存在同一时间,多线程访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
- 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情
- 重量级锁:sychronized
四种状态的转换:无锁->偏向锁->轻量级锁->重量级锁
轻量级别锁CAS解释:在当前线程的栈帧中建立一个锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。CAS操作尝试将对象的Mark Word更新指向Lock Record的指针,如果更新动作成功了,那么这个线程就拥有了该对象的锁。并且对象Mark Word的锁标志位转变为”00”。
轻量级别锁释放锁:那就用CAS操作把对象当前的Mark Word和线程中的Lock Record替换回来。
锁的内存语义:
- 当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
- 而当线程获取锁时, Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争会带来二外的锁撤销的消耗 | 只有一个线程访问同步块或者同步方法的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 若线程长时间抢不到锁,自旋会消耗cpu性能 | 线程交替执行同步块或者同步方法的场景 |
重量级锁 | 线程竞争不使用自旋,不会消耗cpu | 线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗 | 追求吞吐量,同步块或者同步方法执行时间较长的场景 |
问题六:synchronized和ReentrantLock的区别?
java.util.concurrent.locks.ReentrantLock介绍
- 位于juc包下
- 和java.util.concurrent.CountDownLatch、java.util.concurrent.FutureTask、java.util.concurrent.Semaphore一样基于AQS(队列同步器)实现
- 能够实现比synchronized更细粒度的控制,如公平锁
- 调用lock()之后,必须调用unlock()释放锁
- 性能未必比synchronized高,并且也是可重入的。
- 经典实现:java.util.concurrent.ArrayBlockingQueue(数组实现,有界,线程安全的队列)
ReentrantLock将锁对象化
- 判断是否有线程,或者某个特定线程,在排队等待获取锁
- 带超时的获取锁的尝试
- 感知有没有成功获取锁
java.util.concurrent.locks.Condition将wait/notify/notifyAll对象化
- 总结:
- synchronized是关键字, ReentrantLock是类
- ReentrantL ock可以对获取锁的等待时间进行设置,避免死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantL ock可以灵活地实现多路通知
- 机制: sync操作Mark Word , lock调用Unsafe类的park()方法
问题七:sun.misc.Unsafe的理解
- Unsafe是一个后门工具,可以用来在任意内存位置读取数据+CAS操作
问题八:什么是Java内存模型中的happens-before
Java内存模型JMM
- Java内存模型(即Java Memory Model ,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM中的主内存:
- 存储Java实例对象
- 包括成员变量、类信息、常量、静态变量等
- 属于数据共享的区域,多线程并发操作时会引发线程安全问题
JMM中的工作内存
- 存储当前方法中的所有本地变量信息,本地变量对其他线程不可见。
- 字节码行号指示器、Native方法信息
- 属于线程私有数据区域,不存在线程安全问题
JMM和Java内存区域划分是不同的概念层次
- JMM描述的是一组规范,围绕可见性、原子性、有序性展开
- 相似点:存在共享区域和私有区域
指令重排序需要满足的条件
- 在单线程环境下不能改变程序运行的结果
- 存在数据依赖关系的不允许重排序
- 总结:无法通过happens-before原则推导出来的,才能进行指令的重排序
问题九:JMM如何解决可见性问题
- 本质是:内存屏障,Happens-before的八大原则
- 程序次序规则: 一个线程内,按照代码顺序,书写在前面的操作先行发生于书
写在后面的操作; - 锁定规则: 一个unLock操作先行发生于后面对同一个锁的lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操
作; - 传递规则:如果操作A先行发生于操作B ,而操作B又先行发生于操作C ,则可
以得出操作A先行发生于操作C ; - 线程启动规则: Thread对象的start()方法先行发生于此线程的每一个动作 ;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
利用happens-before规则判方法是否是线程安全的?
public class ObjectC { |
上面两个方法是否是线程安全的?
显示的是一组再普通不过的get/set方法,假设存在线程A和B,线程A先(时间上的先后)调用了“setValue(1)”, 然后线程B调用了同一个对象的“getValue()”,那么线程B收到的返回值是什么?
我们依次分析一下先行发生原则中的各项规则,由于两个方法分别由线程A和线程B调用,不在一个线程中,所以程序次序规则在这里不适用:由于没有同步块,自然就不会发生lock和unlock操作,所以管程锁定规则不适用:由于valuc变量没有被volatile关键字修饰,所以volatile变量规则不适用:后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起.
因此我们可以判定尽管线程A在操作时间上先于线程B,但是无法确定线程B中“getValue0”方法的返回结果,换句话说,这里面的操作不是线程安全的。
那怎么修复这个问题呢?我们至少有两种比较简单的方案可以选择:要么把getter/setter方法都定义为synchronized方法,这样就可以套用管程锁定规则:要么把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile 关键字使用
场景,这样就可以套用volatile变量规则来实现先行发生关系。
问题十:谈谈Volatile的认识
- JVM提供的轻量级同步机制,不保证原子性
- 保证被volatile修饰的共享变量对所有线程总是可见的
- 禁止指令的重排序优化(内存屏障)
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
单例双重锁检测实现
问题十一:volatile和synchronized的区别
- volatile本质是在告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住直到该线程完成变量操作为止
- volatile仅能使用在变量级别; synchronized则可以使用在变量、方法和类级别
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量修改的可见性和原子性
- volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化
问题十二:谈谈CAS
- 一种高效实现线程安全性的方法:
- 支持原子更新操作,适用于计数器,序列发生器等场景
- 属于乐观锁机制,号称lock-free。
- 而synchronized和reentrantlock,悲观锁机制
- CAS操作失败时由开发者决定是继续尝试,还是执行别的操作
- CAS思想:
- 新值,预期原值,旧值(内存)
- CAS多数情况下对开发者来说是透明的:
- J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选
- Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患
- Java9以后,可以使用Variable Handle API来替代Unsafe
- 缺点:
- 若循环时间长,则开销很大
- 只能保证一个共享变量的原子操作
- ABA问题,解决:AtomicStampedReference,使用互斥锁可能更加高效
i++操作的字节码
/** |
问题十三:谈谈线程池
利用Executors创建不同的线程池满足不同场景的需求:
- newFixedThreadPool(int nThreads)指定工作线程数量的线程池
- newCachedThreadPool()处理大量短时间工作任务的线程池,
(1)试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
(2)如果线程闲置的时间超过阈值,则会被终止并移出缓存;
(3)系统长时间闲置的时候,不会消耗什么资源 - newSingleThreadExecutor()创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它
- newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程
- newWorkStealingPool()内部会构建ForkJoinPool ,利用working-stealing算法, 并行地处理任务,不保证处理顺序
Fork/Join框架:
- 把大任务分割成若干个小任务并发执行,最终汇总每个小任务结果后得到任务结果的框架
- work-stealing算法:某个线程从其他队列里窃取任务来执行
为什么要使用线程池:
- 降低资源消耗
- 提高线程的可管理型
Executor的框架
架构图:
J.U.C下的三个Executor接口
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦
- ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善
- ScheduledExecutorService:支持Future和定期执行任务
- Executors:静态工厂方法
ThreadPoolExecutor:
架构图
java.util.concurrent.ThreadPoolExecutor.Worker源码
/**
* Class Worker mainly maintains interrupt control state for
* threads running tasks, along with other minor bookkeeping.
* This class opportunistically extends AbstractQueuedSynchronizer
* to simplify acquiring and releasing a lock surrounding each
* task execution. This protects against interrupts that are
* intended to wake up a worker thread waiting for a task from
* instead interrupting a task being run. We implement a simple
* non-reentrant mutual exclusion lock rather than use
* ReentrantLock because we do not want worker tasks to be able to
* reacquire the lock when they invoke pool control methods like
* setCorePoolSize. Additionally, to suppress interrupts until
* the thread actually starts running tasks, we initialize lock
* state to a negative value, and clear it upon start (in
* runWorker).
*/
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
ThreadPoolExecutor的构造函数
- corePoolSize :核心线程数量
- maximumPoolSize :线程不够用时能够创建的最大线程数
- workQueue :任务等待队列
- keepAliveTime :抢占的顺序不- -定,看运气
- threadFactory :创建新线程, Executors.defaultThreadFactory()
- Handler:线程池的饱和策略
- AbortPolicy :直接抛出异常,这是默认策略
- CallerRunsPolicy :用调用者所在的线程来执行任务
- DiscardOldestPolicy :丟弃队列中靠最前的任务,并执行当前任务
- DiscardPolicy :直接丢弃任务
- 实现RejectedExecutionHandler接口的自定义handler
线程池新任务提交execute执行后的流程
- 如果运行的线程少于corePoolSize ,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
- 如果线程池中的线程数量大于等于corePoolSize且小于
- maximumPoolSize ,则只有当workQueue满时才创建新的线程去处理任务;
- 如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
- 如果运行的线程数量大于等于maximumPoolSize ,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
优秀:源码分享:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;这是在ThreadPoolExecutor下,管理线程生命周期,采用ctl变量通过位运算来得到当前线程池【线程的状态】和【可用线程的总数】。
线程池的状态
- RUNNING :能接受新提交的任务, 并且也能处理阻塞队列中的任务
- SHUTDOWN :不再接受新提交的任务,但可以处理存量任务
- STOP :不再接受新提交的任务,也不处理存量任务
- TIDYING :所有的任务都已终止
- TERMINATED : terminated()方法执行完后进入该状态
Java常用类库与技巧
TODO
- todo:学习org.springframework.core.NestedRuntimeException源码
- todo:try-catch-finally的字节码,为什么finally比catch先运行
- Todo: comparable重写的equal/hashcode/compareTo方法
- TreeSet:自然排序(comparable)和客户化排序(Comparator)的优先级,后者更高
问题一:Java异常处理机制
- What:异常类型回答了什么被抛出
- where:异常堆栈跟踪回答了在哪抛出
- why:异常信息回答了为什么抛出
Java异常的处理原则
- 具体明确:抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因;
- 提早抛出:应尽可能早的发现并抛出异常,便于精确定位问题;
- 延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常。
Java异常处理消耗性能的地方
- Try-catch块影响jvm的优化。
- 异常对象实例需要保存栈快照等信息,开销比较达。
问题二:Error和Exception的区别
- 结构图
- 从概念角度解析Java的异常处理机制:
- Error :程序无法处理的系统错误(OOM),编译器不做检查
- 1.NoClassDefFoundError -找不到class定义的异常
- 2.StackOverflowError -深递归导致栈被耗尽而抛出的异常
- 3.OutOfMemoryError -内存溢出异常
- Exception :程序可以处理的异常,捕获后可能恢复
- RuntimeException(非受检异常):不可预知的,程序应当自行避免(空指针、数组越界等)
- 1.NullPointerException -空指针引用异常
- 2.ClassCastException -类型强制转换异常
- 3.IlegalArgumentException -传递非法参数异常
- 4.IndexOutOfBoundsException -下标越界异常
- 5.NumberFormatException -数字格式异常
- 非RuntimeException(受检异常):可预知的,从编译器校验的异常(IOException、ClassNotFoundException),主动去try-catch。一般业务异常
- RuntimeException(非受检异常):不可预知的,程序应当自行避免(空指针、数组越界等)
- 总结:前者是程序无法处理的错误,后者是可以处理的异常
- Error :程序无法处理的系统错误(OOM),编译器不做检查
- 从责任角度看:
- Error属于JVM需要负担的责任;
- RuntimeException是程序应该负担的责任;
- Checked Exception可检查异常是Java编译器应该负担的责任。
问题三:设计高效主流的异常处理框架
- 在用户看来,应用系统发生的所有异常都是用用系统内部的异常
- 设计一个通用的继承自RuntimeException的异常来统一处理
- 其余异常都统一转译为上述异常AppException
- 在catch之后,抛出上述异常的子类,并提供足以定位的信息
- 由前端接受AppException做统一处理
- 结构图
问题四:数据结构和算法
数据结构常见考点:
- 数组和链表的区别;
- 链表的操作,如反转,链表环路检测,双向链表,循环链表相关操作;
- 队列,栈的应用;
- 二叉树的遍历方式及其递归和非递归的实现; .
- 红黑树的旋转;
算法常见考点
- 内部排序:如递归排序、交换排序(冒泡、快排)、选择排序、 插入排序;
- 外部排序:应掌握如何利用有限的内存配合海量的外部存储来处理超大的数据集,写不出来也要有相关的思路。
- 扩展点:
- 哪些排序是不稳定的, 稳定意味着什么
- 不同数据集,各种排序最好或最差的情况
- 如何优化算法
问题五:Java集合框架
- 结构图
- List和Set讲解
问题六:Map讲解
- 结构图: