Java基础
@
- StringBuilder 和 String
- JVM
- 静态static的使用场景 以及什么时候要用到static?
- final java常量在程序运行时是不能被修改的, 类似于 c++ #define定义常量
- Java中的方法覆盖(Overriding 重写)和方法重载(Overload)是什么意思
- java只有值传递, 没有引用传递
- 深拷贝和浅拷贝
- 接口implement
- 多态
- Lambda表达式
- 常用API
- 异常
- 集合
- 杂项知识点
- List集合
- set集合
- Map
- Stream流 就如同一个迭代器(Iterator)
- IO流
- 字节流与字符流
- 多线程
- JUC包(Package java.util.concurrent包)
- 线程安全如何解决
- 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- java中各种锁
- 异步回调: 被调用者执行完了自己的线程任务, 然后通过异步的方式把执行结果通知并返回给调用者
- 守护线程 void setDaemon(boolean on)
- 线程同步
- ReentrantLock 是可重入锁, 公平锁
- synchronized 和 ReentrantLock 的区别
- 死锁
- 阻塞队列
- 线程状态 新建 就绪 运行
- volatile两个作用
- 原子性
- ThreadLocal
- AQS AbstractQueuedSynchronizer 抽象队列同步器
- 面试题汇总
- 线程池
- 垃圾回收机制
- 垃圾回收算法
- 类加载机制
- 反射
- java8新增
- 杂项知识点
- & && | || 区别
- 可变参数
字节码文件(.class)就是二进制流
-
Java跨平台原理
Java程序并非是直接运行的,Java编译器将Java源程序编译成与平台无关的字节码文件(class文件),然后由Java虚拟机(JVM)对字节码文件解释执行。所以在不同的操作系统下,只需安装不同的Java虚拟机即可实现java程序的跨平台。

JVM直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作。

-
JDK




- 源程序通过 javac xxx.java文件然后编译生成xxx.class字节码文件,然后通过java xxx运行整个 xxx.java文件

- 小的到大的范围为隐式类型转换,大的到小的需要进行显式(强制)类型转换
StringBuilder 和 String

- StringBuilder
传统的String在进行 字符串的加号拼接时, 会创建多个对象(因为string是不可变的, 所以会先创建StringBuilder, 然后调用StringBuilder的append函数, 然后调用 toString()方法转化为String对象, 一个加号, 堆内存创建了两个对象)

StringBuilder只会创建一个对象, StringBuffer同理
使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。所以多数情况下推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。参考文献
String常见面试题
- 第一种题目

- 第二种
public class StringDemo {
public static void main(String[] args) {
String str1 = "ab";
String str2 = new String("ab");
String str3 = "a" + "b";
String str4 = new String("a") + new String("b");
System.out.println(str1 == str3); // true
System.out.println(str1 == str4); // false
System.out.println(str2 == str3); // false
System.out.println(str2 == str4); // false
}
}

分析
这里str1、str2、str3和上述情况一致。主要解释一下str4
String str4 = new String("a") + new String("b")可分解为new String("a")和new String("b"),在执行这两步操作时,都会先去字符串常量池中查找是否存在“a”和“b”,若没有,则在字符串常量池中创建“a”和“b”两个对象,然后再在Heap中创建new String("a")和new String("b")两个对象。否则直接在Heap中直接创建new String("a")和new String("b")两个对象。之后会将两个字符串进行拼接,拼接后会再在Heap中创建一个new String("a"+"b")对象。
因此若原本字符串常量池中没有“a”“b”,执行String str4 = new String("a") + new String("b")会创建五个对象,若字符串常量池中已经存在了"a"和"b"则只在Heap中创建三个对象。
参考

StringBuffer线程安全, 相比于StringBuilder
StringBuffer是字符串变量, 是可变的字符串, 并且是Synchronized线程安全的
相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。而在现实的模块化编程中,负责某一模块的程序员不一定能清晰地判断该模块是否会放入多线程的环境中运行,因此:除非确定系统的瓶颈是在 StringBuffer 上,并且确定你的模块不会运行在多线程模式下,才可以采用StringBuilder;否则还是用StringBuffer。
JVM
JVM组成结构
类加载器
运行时数据区(JVM内存模型)
执行引擎
本地方法接口 参考


简述一下JVM运行时数据区(也叫JVM内存模型)

- 在多线程的情况下,程序计数器用于记录当前线程执行的位置
介绍下JVM内存区域以及每个区域的作用。类信息, 常量, 字节码存储在哪个区域?(方法区)

- 方法区: 类加载器编译后的java文件放到方法区, 方法区用于存储已被虚拟机加载的类信息、常量 , 线程共享
- 堆: 存放对象实例,几乎所有的对象实例以及数组都在这里分配内存, 线程共享
- 虚拟机栈: 代码运行空间。我们编写的每一个方法都会放到 栈 里面运行。栈可以存着对象引用, 对象引用的值是具体对象在堆中的地址
- 程序计数器: 类似于一个指针一样的,指向下一行我们需要执行的代码。
- 本地方法栈: 本地方法存在的区域, 本地方法就是用native修饰的C语言的方法
对象分配内存图
局部方法里面的基本数据类型放在栈里面, 随着方法的结束, 在栈中销毁该变量, 这就是局部变量只能在方法中有效的原因。
堆 是所有线程共享的内存区域,栈 是每个线程独享的,如果你将一个实例变量放在栈内,那么就不存在多个线程访问同一个对象资源了,这显然是不对的,所以实例变量要在堆上创建,也不是线程安全的。


字节数:
| byte_boolean | short_char | int_float | double_long |
|---|---|---|---|
| 1 | 2 | 4 | 8 |
| float 类型的数值有一个后缀 F(例如:3.14F) 没有后缀 F 的浮点数值(例如:3.14)默认为 double |
内存溢出和泄露
内存泄露: 不再会被使用的对象的占用的内存空间不能被回收,就是内存泄露。
- 解决办法: 在对象不再被使用的时候立即把对象的引用赋值为 null, 这样就可以让 jvm 知道这是个可以回收的对象(因为JVM会根据可达性分析法GCRoot找到可以回收的对象, 直接赋值为null就省去了这一步骤)
内存溢出:程序运行时,需要的内存大于当前可用的内存, 内存不足,程序无法继续执行(有四种内存溢出)
内存泄漏也会导致内存溢出
-
解决: 检查App中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过10万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。
如果是堆溢出, 手动设置JVM Heap(堆)的大小。可以利用JVM提供的Xms(堆的最小值),-Xmx(堆的最大值)等选项可进行设置
静态static的使用场景 以及什么时候要用到static?
使用场景
- 修饰类的成员变量和成员方法:
- 静态内部类 : 实现 多线程安全和懒加载的单例模式
- 静态代码块:
- 普通类是不允许声明为静态的,只有内部类才可以。静态内部类, 实现单例模式
静态变量是随着类加载时被完成初始化的,它在内存中仅有一个,且JVM也只会为它分配一次内存,
由于static方法在装载class时首先完成,比构造方法早,此时非static属性和方法还没有完成初始化, 所以,在static方法内部无法直接调用非static方法
什么时候要用到static?:
- 工具类里面的工具方法使用static修饰, 不需要new一个对象就可以使用该工具方法
- 当类的某些内容不属于对象,而由对象共享, 对象不需要修改,只需要使用
- 方便在没有创建对象的情况下来进行调用类的(方法/变量)。
一个类中的成员变量和静态成员变量的区别
1、两个变量的生命周期不同
成员变量随着对象的创建而存在,随着对象被回收而释放。
静态变量随着类的加载而存在,随着类的消失而消失。
2、调用方式不同
成员变量只能被对象调用。
静态变量可以被对象调用,还可以被类名调用。
jdk8 静态成员变量在堆中
总结: 成员变量属于对象,对象放在堆上,那么成员变量也就放在堆上

参考
final java常量在程序运行时是不能被修改的, 类似于 c++ #define定义常量

例子: final double PI = 3.1415927;
Java中的方法覆盖(Overriding 重写)和方法重载(Overload)是什么意思
- 重载:
在同一个类中,方法名相同,但 参数的类型,个数,或 顺序不同;
相同参数不同返回值不可以重载, 比如一个返回int,一个返回boolean - 重写:
即指子类重写父类的方法,返回值类型、方法名、参数列表 都必须一样;
子类重写方法的 抛出异常的范围 必须小于等于 父类中被重写的方法;(如果子类抛出的异常更大,那么 父类就无法 catch 子类的异常。
所以禁止子类抛出的异常更大 参考)
子类重写方法的 权限修饰符范围 必须大于等于 父类中被重写的方法;
java只有值传递, 没有引用传递
值传递,顾名思义传递的是其值,也可以理解为副本给你,任意修改,原本不变。 引用传递,顾名思义传递本身,或者说传递的是地址,修改的是本身或者是原本地址里的内容。
案例参考

如果行参是引用类型,传递给形参的 就是实参所引用的对象在堆中地址值的拷贝,并不会影响到实参。
深拷贝和浅拷贝
- 浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),拷贝对象和原对象共用同一个内部对象。
- 深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

接口implement
JDK8的优化 项目添加新功能的时候,给InterA使用default添加新方法时, 实现类Impl不需要重写该方法
- 用default声明方法和具体的方法实现

多态
简介: 同一个方法发生在不同的子类上会产生不同的结果。一般通过抽象类的继承或者通过接口实现, 需要有继承和重写方法
让父类的对象引用指向不同的子类对象, 或者让接口的对象引用指向不同的接口实现类来实现多态
就是允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
接口和抽象类
- 抽象类设计理念为用于同类的事物, 强调子类父类所属关系, 比如说给 藏獒, 贵宾犬, 哈士奇写抽象类, 那么抽象类就是 狗狗类, 如果一个类继承了某个抽象类,则子类必定是抽象类的种类 , 用处②: 为了实现代码复用.
- 接口实现的设计理念是具备不具备的关系, 比如一个接口含有跑步这个方法, 能则如果狗狗类能跑步, 那么就可以实现跑步这个接口,不能跑步就不可以实现这个跑步接口。
- 除了Java8之后, 接口中不能含有
静态代码块以及静态方法,而抽象类可以 - 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
- 接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
- 接口成员变量默认为
public static final,必须赋初值,不能被修改, 抽象类可以包含非final的变量,可在子类中被重新定义,也可被重新赋值 - jdk8之前接口只有定义,不能有方法的实现细节,java 1.8中可以定义default方法体来实现方法,而抽象类中的方法可以有定义与实现细节
- 接口是公开的,是用于让别人使用的,里面的方法默认是修饰符是public 而抽象类是可以有私有方法或私有变量的,
- 实现接口的一定要实现接口里定义的所有方法,而实现抽象类可以有选择地重写需要用到的方法
public interface Person{
public static final int a=10; //接口成员变量
//JDK1.8新增
default void sayHello(){
System.out.println("Hello World");
}
}
- 抽象类性质 : 抽象类并不一定都是抽象方法,甚至可以没有,但有抽象方法的类一定是抽象类
- 其他性质:
A.抽象类可以在不提供接口方法实现的情况下实现接口: 也就是说不用抽象类implements接口时, 不用去override接口的所有方法
B.抽象类则可以同时包含抽象和非抽象的方法
C.Java接口中声明的变量默认都是final的。抽象类可以包含非final的变量
D.接口和抽象类也不可以被实例化
Lambda表达式
example
Lambda表达式的使用前提
- 有一个接口
- 接口中有且仅有一个抽象方法
// Java 8之前:
new Thread( new Runnable() {
@Override
public void run() {
System.out.println("Before Java8, too much code for too little to do");
}
}).start();
//Java 8方式:
new Thread( () -> System.out.println("In Java8, Lambda expression rocks !!") ).start();
和匿名内部类对比
- 所需类型不同
- 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类
- Lambda表达式:只能是接口
- 使用限制不同
- 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类
- 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用Lambda表达式
常用API
equals() 和 hashCode() 和 ==
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象的内存地址是否相等, ==两个都可以
equals()属于Object类, 当其他类比如String类重写了equals()方法, 就是比较两个字符串对象的值是否相等, 而不是比较对象的内存地址是否相等
- hashCode() 将对象的内存地址转换为int整数之后返回。
为什么重写 equals() 时必须重写 hashCode() 方法?
- 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
- 原因: 因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
- 举例子: 如果两个由equals()规定相等的key对象生成的hashCode不等,对于hashMap来说,这两个key很可能分别映射到不同位置,没有调用equals()比较是否相等的机会,两个实际上相等的对象可能被插入不同位置,出现错误。
//重写hashCode, 根据属性值name和age计算hashCode,
同时重写的equals()方法也要根据这两个属性值来判断两个对象是否相等
@Override
public int hashCode(){
int nameHash = name.toUpperCase().hashCode();
return nameHash ^ age;
}
异常
总结:
分为编译时异常和运行时异常
- 编译时异常: 编译期必须处理, 不处理则无法通过编译, IO 相关的异常、ClassNotFoundException 、SQLException
- 运行时异常: 运行时期异常, 不处理也可以通过编译, 空指针异常, 数组索引越界异常



自定义异常
- 为什么要继承RuntimeException?
- Exception类型的异常必须手动处理
- RuntimeException异常既可以自动处理,也可以手动处理
throw和throws
- Throw用于抛出异常对象,后面跟的时异常对象。是语句抛出一个异常。多用在函数内。
语法:throw(异常对象)
throw e;
2.Throws是方法可能抛出异常的声明(用在声明方法时,表示该方法可能要抛出异常)
语法:修饰符 返回值类型 方法名 参数类型 throws异常类
public void test throws Exception1,Exception2(){}
void doA(int a) throws Exception1,Exception3{
try{
......
}catch(Exception1 e){
throw e;
}catch(Exception2 e){
System.out.println("出错了!");
}
if(a!=b)
throw new Exception3("自定义异常");
}
集合
与数组不同

杂项知识点
- 快速失败和安全失败 : 快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个ConcurrentModification异常。
在java.util包下的都是快速失败。
安全失败:在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
List集合
ArrayList扩容机制
当我们不指定 Arraylist 的大小时, Arraylist 是一个空数组, 第一次添加元素时, 数组扩容为默认长度10, 当添加到第11个元素时, 数组长度扩容为1.5倍左右, 通过右移一位的操作, 如果原数组长度是偶数, 那么就是精确的1.5倍, 如果是奇数, 会丢掉小数.
如果数组长度很大, 当原来数组长度的1.5倍大于Integer的最大值 \(2^{31} - 1\) 减去8时, 判断数组最小需要容量是否大于Integer的最大值-8, 如果是, 那么扩容后数组最大容量为Integer的最大值 , 否则为Integer最大值-8
- 解决线程不安全
LinkedList 双向链表

二者区别
Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构
Arraylist采用数组存储, 所以插入和删除元素的时间复杂度受元素位置的影响, 直接添加元素是在数组末尾添加, 时间复杂度为 O(1), 如果要在指定位置比如 i 位置插入元素, 时间复杂度为O(n-i), 因为数组第i个元素之后的元素都要往后移一位
LinkedList使用链表存储, 在头部和尾部插入删除元素时间复杂度为O(1), 如果要在指定位置位置比如 i 插入删除需要移动到指定位置
ArrayList支持根据索引下标的随机访问, LinkedList只能遍历查询
LinkedList双向链表 , 每个链表节点存放着前驱和后续节点引用以及数据, 内存空间占用大
set集合

HashSet
HashSet特点
HashSet特点 参考
- 它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素
- 非线程安全
- 允许null值
- 添加值得时候会先获取对象的hashCode方法,如果hashCode方法返回的值一致,则再调用equals方法判断是否一致,如果不一致才添加元素。
- 没有索引,不能使用普通for循环遍历, 必须用迭代器访问
注意: 对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。
HashSet集合存储自定义类型的对象, 要想实现存储后的对象的不重复,要求必须重写(override)hashCode方法和equals方法(即在IDEA中用快捷键Alt+Insert 选择override hashCode方法和equals方法, 比如下面, 重写的方法会根据name和age属性来判定是否为同一个对象, 而不是根据对象的哈希值)
//比如存两个重复对象,如果不重写hashCode方法和equals方法, 那么stu1和stu2会被判定为不同的对象
Student stu1 = new Student("张三", "17")
Student stu2 = new Student("张三", "17")
因为不同对象stu1和stu2的哈希值是不同的。而重写hashCode()方法,可以实现让不同对象stu1和stu2的哈希值相同
Map
TreeMap(需要排序时使用)
- TreeMap底层是红黑树结构
- 依赖JDK自带的自然排序或者比较器排序,对键进行排序
- 如果键存储的是自定义对象,需要实现Comparable接口或者在创建TreeMap对象时候给出比较器排序规则
HashMap
-
为什么使用红黑树?
为了提升查找性能, O(n)和O(logn), 并且红黑树相比于AVL二叉平衡树是不严格要求树的平衡
哈希表数组初始容量
- 问题: 初始容量随便设个数、比如7可以吗?
可以, 你输入7, hashmap会把7替换成第一个大于该数值的2的幂作为初始化容量。这里是8
put多线程不安全
两个线程a,b执行put操作 , 并且两个key通过hash函数计算出来的哈希表数组下标都是相同的, 当线程a插入时由于CPU时间片用完而被挂起, 线程b取得时间片后在这个下标处插入了元素, 然后a获得cpu时间片, 又在这下标处插入了元素, 覆盖了线程b插入的数据
jdk8的优化
- hash算法的优化
先调用object类的hashcode()方法返回int类型4个字节32比特的hashcode值, 然后让32比特的hashcode和无符号右移16位后的hashcode进行异或运算(相同为 0 ,不同为 1) - 计算key的哈希桶数组下标的优化: 用与运算替代取模,提升性能。(lenght-1) & hash, 因为HashMap 的哈希表长度是 2 的幂次方, 减一后和hash进行与运算数学上等同于取余运算, 并且二进制位操作 &,相对于%能够提高运算效率
- 为何要右移16位: 由于绝大多数情况下哈希桶数组length一般都小于2^16即小于65536, 所以h & (length-1)运算过程始终是hash的低16位与(length-1)进行&运算, 导致高位完全失效。右移16位使得hashcode值的高位同样参与到key在table中下标的运算,把高位的特征和低位的特征组合起来, 使得求出的哈希表数组下标尽可能的不同, 降低哈希冲突的概率
无符号右移16位降低哈希冲突的例子
- 为何要右移16位: 由于绝大多数情况下哈希桶数组length一般都小于2^16即小于65536, 所以h & (length-1)运算过程始终是hash的低16位与(length-1)进行&运算, 导致高位完全失效。右移16位使得hashcode值的高位同样参与到key在table中下标的运算,把高位的特征和低位的特征组合起来, 使得求出的哈希表数组下标尽可能的不同, 降低哈希冲突的概率
- 哈希表数组扩容后, 重新寻找数组下标的优化, 每次扩容2倍,n - 1这个值的二进制最右边的0变为了1, 判断hashcode的相应的比特位是1还是0,是0不变,是1的话,index = index + 原表大小。意思就是不再进行与运算了
参考 - jdk7 链表扩容复制时链表采用头插法. 会形成死循环的闭环链表, 通过尾部插入法解决

- put 操作
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(1, "two"); 此时 1对应的 one 会被重写为 two
put具体流程
首先判断哈希表数组是否为空, 如果是空的, 那么调用扩容函数, 进行扩容, 如果没有指定哈希表数组大小, 那么就是默认大小16, 如果指定了大小比如为6, 那么就就会把容量设置为大于6的最小的二次幂, 比如这里是2的三次方就是8,
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8并且哈希表数组长度大于64, 那么把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在并且hash()值相同直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过,进行扩容。

图片来源


get流程
跟put一样的根据key的hashcode获取key对应的哈希桶的索引值, 然后判断key和hash值是不是对应链表或者红黑树的首节点相等, 是的话直接返回value值, 不是的话就遍历链表或者红黑树找到key和hash值相等的节点 参考
负载因子0.75 和 每次扩容2倍(扩容使用新的数组, 不是扩充原数组)
threshold size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
loadFactor 装载因子,table 最大能够使用的比例,threshold = (int) (capacity* loadFactor)。
HashMap 的2种遍历方式
for (Map.Entry<String, String> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
for (String key : map.keySet()) {
map.get(key);
}
为何使用红黑树,不用AVL树

任何节点的两个子树的高度最大差别为1, 所以它也被称为严格平衡树。增加和删除树节点可能需要通过多次树旋转来重新平衡这个树,由于旋转比较耗时,所以AVL树适合用于插入与删除次数少的场合,AVL树是更加严格的平衡,因此可以提供更快的查找速度,查找多的时候适用AVL树。
红黑树是弱平衡,在AVL树中,任何节点的两个子树的高度最大差别为1。在红黑树中,最多是2倍。所以红黑树保证平衡时旋转次数少,插入,删除操作较多的情况下,用红黑树
HashMap不用B+树原因
因为hashmap操作的是内存数据,不用考虑磁盘io,所以不用B+树, 然后因为平衡二叉树是查找效率最高的,所以用红黑树. 数据库使用b+树,是因为考虑到磁盘io和范围查找的问题
红黑树性质
-
每一个节点或是红色的,或者是黑色的
-
根节点必须是黑色
-
如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连 的情况)
-
对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

ConcurrentHashMap 线程安全的HashMap
1. 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
2. 计算当前元素应存入的索引。
3. 如果该索引位置为null,利用 CAS算法 尝试写入,失败则自旋保证成功。
4. 其他情况使用synchronized 锁写入数据
6. 不锁住整个哈希表, 使用 synchronized 锁定哈希表数组上某个数组节点, 这个数组节点是链表或红黑二叉树的首节点, 不锁住整个哈希表, 以链表或者红黑树头结点为锁对象
- HashTable锁住整个哈希表, 开销非常大
HashMap对象的key、value值均可为null。
ConcurrentHashMap的key,value都不能为null
Collections 工具类
Collections 是一个操作 Collection(Set、 List )和 Map 等集合的工具类
排序
查找,替换操作
void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑
Stream流 就如同一个迭代器(Iterator)
- filter(里面写过滤条件)
- 遍历 foreach
- reduce 是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。
- 排序sorted
public class Demo2 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("花和尚");
list.add("豹子头");
list.add("摸着天");
list.add("晁盖");
list.add("鼓上骚");
list.stream().filter(name -> name.startsWith("花"))
.filter(name -> name.length() == 3)
.forEach(System.out::println);
}
}
IO流
Java 的 I/O 大概可以分成以下几类:
磁盘操作:File
字节操作:InputStream(抽象类) 和 OutputStream
字符操作:Reader 和 Writer
对象操作:Serializable
网络操作:Socket
新的输入/输出:NIO
BIO、NIO、AIO
I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。按块处理数据比按流处理数据要快得多
- 同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,餐没做好,你就必须等着!
- 同步非阻塞:你在饭馆点完餐,就去玩儿了。
不过玩一会儿,就回饭馆问一声:好了没? - 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于外卖。
BIO(同步阻塞 Blocking I/O):线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。
NIO(同步非阻塞):线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成。
AIO(Asynchronous I/O 异步非阻塞):线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败。
缓冲区

- 原理: 减少数据跟硬盘的传输次数,节省时间, 所有缓冲区的抽象意义是一样的,就是屯够货了,集中干。在屯货的时候,可以干别的事,这样就提高了效率
比如:一个人要把水龙头流出来的水弄到水缸里面去,要是没有缓冲池,每流出一滴水,你都要跑两趟水龙头与缸之间的距离(这个在传文件的时候就是磁盘读写的时间),而当你有一个缓冲池(比如盆),你可以等盆满了再把水弄过去(这之间你可以做其他的事,在JAVA中,你就是CPU)
所以有缓冲区的话,你可以节省CPU的大量时间,而且可以对缓冲区中的数据进行集中读写,这样不必每来一个数据你去到磁道上搜索地址,然后再回来接受数据,再去搜索地址存取数据,再回来接受数据。
缓冲区的大小根据你的用户的上传文件的大小设置,一般取平均值,这个要经验的。注意:缓冲区大小不是上传文件的平均值大小
字节流与字符流
字节流就是 Byte 流,字节就是 Byte。
字符流就是 Character 流,字符就是 Character。
这里面有两个单位换算大家需要记住一下。
1 byte = 8 bit
1 char = 2 byte = 16 bit
面向字节流的 InputStream 和 OutputStream;面向字符的 Reader 和 Writer。
为什么要有字符流呢?那是因为英文,我们可以用字节来表示,但是中文、日文、韩文等没法用字节来表示了,所有人们就想到了新的字符编码集。比如,Unicode 字符集,GB 18030,GBK,Big5,ISO-8859-1 等。
所以,最终就是一个字符用 1 个、2 个或 4 个字节组成。总而言之,一切都是字节流,其实没有字符流这个东西。字符流只是根据编码集对字节流翻译之后的产物。
- 字节流是用来拷贝文件的
- 字符流是为了显示中文,区别于字节流显示的中文乱码,字符流本质上就是字节流+编码表


相关面试题
- Java读取文件,但文件大于内存该怎样办
我们通常不需要把文件的所有行一次性地放入内存中——相反,我们只需要遍历文件的每一行,然后做相应的处理,处理完之后把它扔掉。
使用文件流FileInputStream , 以及使用java.util.Scanner类扫描文件的内容,一行一行连续地读取: 参考
sc = new Scanner(inputStream, "UTF-8");
while (sc.hasNextLine()) {
String line = sc.nextLine();
// System.out.println(line);
}
// 完整代码
FileInputStream inputStream = null;
Scanner sc = null;
try {
inputStream = new FileInputStream(path);
sc = new Scanner(inputStream, "UTF-8");
while (sc.hasNextLine()) {
String line = sc.nextLine();
// System.out.println(line);
}
// note that Scanner suppresses exceptions
if (sc.ioException() != null) {
throw sc.ioException();
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (sc != null) {
sc.close();
}
}
多线程
为什么要多线程:
-
使用多线程就是为了充分利用cpu的资源,提高程序执行效率,当你发现一个业务逻辑执行效率特别低,耗时特别长,就可以考虑使用多线程。
不过CPU执行哪个线程的时间和顺序是不确定的,即使设置了线程的优先级,因此使用多线程的风险也是比较大的,会出现很多预料不到的问题
JUC包(Package java.util.concurrent包)
[图片来源](https://www.zybuluo.com/adamhand/note/1313016)
线程安全如何解决
实现线程同步的方法:
1、volatile关键字。
2、lock关键字。
3、synchronized关键字。
4、ThreadLocal关键字,总的来说是为每个线程创建一个变量,从而避免冲突。
5、BlockQueue,阻塞队列。
6、AtomicInteger,原子整型类。
同步方法和同步代码块: 同步方法使用关键字 synchronized修饰方法,而同步代码块主要是修饰需要进行同步的代码块使用
synchronized(object)
{
代码块内容
};
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

- 进程与线程 一个进程就是一个应用程序, 比如windows系统里面一个运行的exe文件就是一个进程, 一个进程里面有多个线程, 多个线程共享进程的堆和jdk8之后的元空间, 协程: 一个线程也可以拥有多个协程。
- 创建线程有4种方式, 包括线程池创建线程 (好处是容易管理线程)
- 创建线程实现步骤
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
public class Main {
public static void main(String[] args) {
//普通写法
Thread t = new Thread(new MyRunnable()); //
t.start(); // 启动新线程
// lambda表达式写法
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
}
| 好处 | 缺点 | |
|---|---|---|
| 实现Runnable、Callable接口 | ①扩展性强,实现该接口的同时还可以继承其他的类 ②线程池只能放入实现Runable或callable类的线程,不能直接放入继承Thread的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
| 继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
- Runnable的方法没有返回值
- Callable接口,和Runnable接口比,它多了一个返回值:
class Task implements Callable<String> {
public String call() throws Exception {
return longTimeCalculation();
}
}
- Future作用:
对线程池提交一个Callable任务,可以获得一个Future对象;
可以用Future在将来某个时刻异步获取结果。参考
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞
当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。
java中各种锁
有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题
synchorized锁的状态升级: 由低到高
- 偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,避免像轻量级锁那样多次CAS操作来获取锁, 偏向锁通过一个简单的数据结构Mark Word, 里面里存储锁偏向的线程ID (在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁)
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
偏向锁释放: 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁 - 轻量级锁: 是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会
升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

- 重量级锁: 重量级锁是将除了拥有锁的线程以外的线程都阻塞。
- 总结: 综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
自旋锁
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

获取同步资源的锁失败, 不阻塞, 不放弃CPU时间片, 通过自旋等待锁释放, 通过自旋操作减少CPU切换以及恢复现场导致的消耗
- 应用场景: 如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。

synchronized 关键字
同步代码块

对象锁(实例锁)
类声明后,我们可以 new 出来很多的实例对象。这时候,每个实例在 JVM 中都有自己的引用地址和堆内存空间,这时候,我们就认为这些实例都是独立的个体,很显然,在实例上加的锁和其他的实例就没有关系,互不影响了。
-
this
-
非this
synchronized(非this对象) 括号里面必须是同一个对象,
类锁
类锁是加载类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的, 也是这个类的所有对象共享的. 也就是说所有线程共同使用这一把锁,



异步回调: 被调用者执行完了自己的线程任务, 然后通过异步的方式把执行结果通知并返回给调用者
-
异步回调典型例子: 参考文献
提问者A有个问题"1+1=?",于是A打电话给回答者B,B说他现在很忙,忙完了才能给他想答案,A心想我不能这么一直等着把,于是说:“那咱们约定好,B你想出答案了以打电话的形式告诉我”,挂了电话A也去忙他自己的事了,过了一会B想出答案按A约定好的方式打电话告诉了B答案。 -
java使用 参考

守护线程 void setDaemon(boolean on)
例子: 关掉QQ(普通线程),那么QQ的文件传输线程(守护线程)也被关闭
-
相关方法
方法名 说明 void setDaemon(boolean on) 当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了,守护线程挣扎一会就停止运行 -
代码演示
public class MyThread1 extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + "---" + i); } } } public class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName() + "---" + i); } } } public class Demo { public static void main(String[] args) { MyThread1 t1 = new MyThread1(); MyThread2 t2 = new MyThread2(); t1.setName("女神"); t2.setName("备胎"); //把第二个线程设置为守护线程 //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了. t2.setDaemon(true); t1.start(); t2.start(); } }
线程同步
-
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
在计算机中,锁的作用是解决在并发状态下的共享资源互斥问题,保证在同一时间只有一个进程/线程可以掌握资源的控制权。 -
同步的好处和弊端
-
好处:解决了多线程的数据安全问题
-
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
-
synchronized(任意对象) {
多条语句操作共享数据的代码
}
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 对可能有安全问题的代码加锁
//t1进来后,就会把这段代码给锁起来
if (tickets > 0) {
try {
Thread.sleep(100);
//t1休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; //tickets = 99;
}
}
//同步代码块执行完,才会释放锁, 其他线程不允许干扰这个线程同步代码块的执行
}
}
}
synchronized 和 java.util.concurrent.locks.Lock 的异同
主要相同点:Lock 能完成 synchronized 所实现的所有功能
主要不同点:Lock 比synchronized 能提供更多的对线程的操作
synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在finally 从句中释放。
试考虑以下三种情况:
Case 1 :
在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
Case 2 :
我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
Case 3 :
我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。
上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。参考
synchronized是可重入锁, 是非公平锁
可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
如果有两个不同的方法A和B用到同一个锁,其中方法A里面又调用了方法B,那就不会死锁了
是对非可重入锁的增强,避免非可重入锁在嵌套使用时产生死锁。
synchronized可重入锁的实现
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
公平锁和非公平锁在ReentrantLock中如何实现
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。符合队列先入先出
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。先尝试插队, 插队失败再排队. 优点是减少CPU唤起线程的开销
ReentrantLock 是可重入锁, 公平锁
- ReentrantLock 实现原理: 通过AQS抽象队列同步器实现的公平锁和非公平锁
源码: public class ReentrantLockimplements Lock
//创建ReentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();
//获取锁
reentrantLock.lock();
try{
同步代码
} finally{
//释放锁
reentrantLock.unlock();
}
synchronized 和 ReentrantLock 的区别
- ReentrantLock 显示地获得、释放锁,synchronized 隐式获得释放锁
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- 提供分开的读锁和写锁, 读锁只对资源进行读,而不进行写操作。这样对于共享资源而言就不会存在线程不安全问题。所以读锁允许多个线程获取锁访问资源信息, 从而加快读取速度, 而写线程使用排它锁/独占读取 参考
- synchronized 是依赖于 JVM 实现的, 没有直接暴露给我们, ReentrantLock 是 API 级别的, 提供了更多对于锁的细粒度的操作, 可以设置超时时间
可以设置为公平锁
synchronized 底层原理
- 同步代码块(更精细的控制锁): 使用的是 monitorenter 和 monitorexit 指令, monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
在执行monitorenter指令时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 .
对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。- 同步方法: 使用一个标识 ACC_SYNCHRONIZED , 该标识指明了该方法是一个同步方法。
- 总结: 两者的本质都是对对象监视器 monitor 的获取。
ReentrantLock 的Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的
死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,并且只有当获得了对方的资源自己才能继续完成工作, 但是但是没有一方提前退出来释放自己的资源, 导致这些线程处于等待状态,无法继续执行
- 例子: 一个进程 p1占用了显示器,同时又必须使用打印机,而打印机被进程p2占用,p2又必须使用显示器,这样就形成了死锁。 因为p1必须等待p2发布打印机才能够完成工作并发布屏幕,同时p2也必须等待p1发布显示器才能完成工作并发布打印机,形成循环等待的死锁。
死锁的四个条件是:
- 禁止抢占(no preemption):系统资源不能被强制从一个进程中退出。
- 持有和等待(hold and
wait):一个进程可以在等待其他资源时持有系统资源。 - 互斥(mutual exclusion):资源只能分配给一个进程,无法多个进程共享。
- 循环等待(circular waiting):一系列进程互相持有其他进程所需要的资源。
死锁只有在四个条件同时满足时发生,预防死锁必须至少破坏其中一项。
- 死锁解决方法: 参考文献
-
占用部分资源的线程申请其他资源时,如果 申请不到,可以 主动释放自己占用的部分资源。 (自己得不到,可以成全别人)
-
加锁顺序: 按某一顺序申请资源,释放资源则相反顺序释放。
-
给锁加上过期时间
-
死锁检测
-
阻塞队列
多用于消费者和生产者: 阻塞队列使用消息发布者与监听者模式。当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

- 应用: 线程池的工作队列 workQueue就是阻塞队列, 线程池中活跃线程数达到 corePoolSize 时,线程池将会将后续的 task 提交到 BlockingQueue 中;
- 常用阻塞队列:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列 - DelayQueue : 其中的对象只能在其到期时才能从队列中取走。用于淘宝订单业务: 淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单。
线程状态 新建 就绪 运行


volatile两个作用
- 也就是一个线程对于主内存的共享变量修改的结果,其他线程能看到。
- 让每个线程在使用主内存的共享变量的时候, 都会去看一下主内存的共享变量最近更新的值, 而不是读取线程自己内部的内存里面存放的共享变量的副本
注意synchronized也会强制去看一下共享区域(内存)的最新更新的值
- 让每个线程在使用主内存的共享变量的时候, 都会去看一下主内存的共享变量最近更新的值, 而不是读取线程自己内部的内存里面存放的共享变量的副本
- 作用2: 禁止JVM指令重排(比如创建对象的时候)
原子性
悲观锁和乐观锁
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
是否锁住同步资源
- 乐观锁思想: 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
- cas(Compare and swap): 先比较, 只有当内存值等于我的预期值的时候, 我才去修改内存值, 否则自旋等待
- 具体过程:
假设某个线程读取了某个共享变量的值记为A, 线程想要把共享变量的值修改成B, 那么线程就再去读取一下共享变量的值, 假设为V, 先比较V和A是否相等, 相等的话线程就把它改成B,如果不相等就说明被别的线程修改过了, 那么线程就不再修改.
一般会加上自旋操作, 然后把V作为下一次比较的期望值, 线程就继续循环重复以上的比较和修改操作, 比较和修改是原子操作, 要么一起执行成功, 要么一起失败
CAS有三个操作数:内存值V、预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才将内存值修改为B,否则什么都不做。最后返回现在的V值。 - 应用场景 :
concurrentHashMap
原子类操作
缺点
- ABA问题 (检测不出是否修改过)
我认为 V的值为A,有其它线程在这期间修改了值为B,但它又修改成了A,那么CAS只是对比最终结果和预期值,就检测不出是否修改过 - 自旋时间过长,CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
ABA问题改进
- 通过版本号的机制来解决。每次变量更新的时候,版本号加 1,如AtomicStampedReference的compareAndSet ()
synchronized和CAS的区别 :
相同点: 在多线程情况下,都可以保证共享数据的安全性。
不同点: synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。
如果别人修改过,那么我再次获取现在最新的值。
如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)
ThreadLocal
Java中每一个线程都有自己的专属本地变量, 可以将 ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据

- 原理: 每个线程内部都有个ThreadLocalMap,调用ThreadLocal.set(object)方法时,把这个ThreadLocal对象作为key, object作为值存到了这个内部map里面。

参考

AQS AbstractQueuedSynchronizer 抽象队列同步器
- AQS思想: AQS里面有一个State变量表示资源数量, 用volatile修饰修饰的int类型, 表示资源数量,共享模式下取值为n, 独占模式下取值为1, 各个线程通过 CAS算法 完成对 State 值的修改
任何想要获得锁的线程都需要来竞争该变量,获得锁的线程可以继续业务流程的执行,而没有获得锁的线程会被放到一个FIFO的队列中去,等待再次竞争同步变量来获得锁。AbstractQueuedSynchronizer为每个没有获得锁的线程封装成一个Node再放到队列中去 参考

控制并发线程数之Semaphore
- Semaphore的构造函数入一个int类型的参数N作为可颁发的许f可数量,线程通过调用acquire方法获取许可,只有获得许可的线程才可以执行任务,执行完成后调用release方法归还许可。当线程获取许可失败时,表示已经到达了最大的并发线程数,即许可已经颁发完毕,需要等待有线程归还后再次尝试获取。


CyclicBarrier
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

面试题汇总
- 线程通信,c线程要等待a,b两个线程的结果,有哪种方式?
答: 使用 CountdownLatch
- 创建一个计数器,并设置一个初始值,
CountdownLatch countDownLatch = new CountDownLatch(2); - 调用
countDownLatch.await()进入等待状态,直到计数值变为0; - 在其他线程执行完后调用
countDownLatch.countDown(),该方法会将计数值减一; - 当计数器的值变为 0 时,
countDownLatch.await()等待线程中的方法会继续执行下面的代码。
参考
- sleep 与 wait 区别
sleep 属于Thread类, wait()属于Object类
调用sleep不会释放锁, wait会释放锁, 调用notify时才会开始准备获取锁
sleep用Thread调用,在非同步状态下就可以调用, wait必须在同步方法或者同步代码块中调用(因为调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。)
线程池
好处 :
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。
ThreadPoolExecutor

举例子:
-
线程池:核心池大小为5,最大池大小为10,队列为100。
因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建最新的线程最多到10个线程(maxPoolSize),如果再来任务,就拒绝。 -
线程池其他参数
-
keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交给多余线程来执行,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁; -
拒绝策略: 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,
-
拒绝新任务, 并且抛出异常,
-
拒绝新任务并且丢弃新任务但是不抛出异常,
-
让携带任务到线程池的那个线程去处理这个任务, 比如main线程携带了这个任务过来, 那么让main线程来处理这个任务.
-
还有一个是丢弃阻塞队列的最开头的任务, 换成新来的那个任务 参考
-
-
TimeUnit unit: 时间单位, 可以设置为秒
-


线程池参数设置 (这两个参数: 核心线程数和最大线程数)
-
CPU密集型: 高并发、任务执行时间短的业务,可以设置为CPU核数+1,减少线程上下文的切换
-
IO密集型: cpu核心数 * 2;
系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用, 因此我们可以多配置一些线程
提交任务 execute 和 submit
- execute 提交不需要返回值的任务
- submit 提交需要返回值的任务
垃圾回收机制
- 总结: 垃圾回收过程:
- 找出不能再被任何途径使用的对象, 也就是垃圾对象, 通过可达性分析从 GCRoots 引用链开始进行"顺藤摸瓜"找到不可达对象(可回收)
- 使用分代回收算法进行垃圾回收, 老年代和新生代
当对象不再被程序所引用时,它所使用的堆空间就需要被回收,以便被后续的新对象所使用。JVM 的内存分配管理机制会自动帮我们回收无用的对象,它知道如何确定对象不再被引用,什么时候去回收这些垃圾对象,使用什么回收策略来回收更高效,以及如何管理内存,这部分就是JVM的垃圾收集相关的内容了。
垃圾回收算法
从GC Roots作为起点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即不可达对象。
通过"GC Roots"作为起点,从这个节点往下搜索,如果有有引用,则这个对象是存活的,如果没有则判定可回收的对象。
下图中的 Object 6 ~ Object 10 之间虽有引用关系,但它们到 GC Roots 不可达,因此为需要被回收的对象。

- 哪些对象可以作为 GC Roots 呢?
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
参考 - 4种对象持有的引用
- 强引用: Java默认的, 只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。
如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了 - 软引用: 在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
用于缓存 - 弱引用: 无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
- 虚引用: 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
虚引用必须和引用队列一起使用。
- 强引用: Java默认的, 只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。

标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。
这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的内存碎片, 导致没有足够的内存分配给给需要大量连续内存空间的对象, 比如数组这种)

标记-复制算法
它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉存活对象的端边界以外的内存。

JVM采用的是分代回收算法
根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
垃圾收集器
- CMS 用于老年代, 标记-清除
- G1收集器 : 两个年代都有
类加载机制
java new一个对象的过程中发生了什么
java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全部名称来加载。加载并初始化类完成后,再进行对象的创建工作。java是使用双亲委派模型来进行类的加载的
我们先假设是第一次使用该类,这样的话new一个对象就可以分为两个过程:加载并初始化类和创建对象。
类使用时才加载
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
- 类加载的过程:
加载、验证、准备、解析、初始化
双亲委派模型
应用程序是由三种类加载器互相配合从而实现类加载, 除此之外还可以加入自己定义的类加载器
具体工作过程:
- 每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,
系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由身为底层加载器的自己来处理。

好处
- 可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类
- 使得基础类得到统一
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器(最顶层),而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。参考
Class.forName和 classloader 的区别
(1)Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
(2)而classloader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
例子: 通过 Class.forName()传入类的路径获取具体类:
ClassLoader就是遵循双亲委派模型的类加载器,Class.forName()方法源码上看出来也是调用的CLassLoader来实现 (实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。)的。参考
- 小例子:
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");
反射
- 通过反射
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
应用场景:
- 动态代理
- java的注解和spring的注解
- 从IOC容器获取bean对象
java8新增
JVM 内存管理方面,由元空间代替了方法区。
区别:
元空间并不在虚拟机中,而是使用本地内存, 降低内存溢出也就是outOfMemory的几率
杂项知识点
自动拆箱与装箱: 保证通用性
如果频繁拆装箱的话,也会严重影响系统的性能
Integer a = 5; //装箱
int b = a; //拆箱
-
maven打包命令: mvn clean package
maven的三种项目打包方式——jar,war,pom
1:pom工程:用在父级工程或聚合工程中。用来做jar包的版本控制。
2:war工程:将会打包成war,发布在服务器上的工程,如网站或服务。
3:jar工程:将会打包成jar用作jar包使用。
参考 -
金额必须是完全精确的计算, 故不能使用double或者float, 使用BigDecimal
- : 这里的?可以是T类型或者T的子类类型, 被设计用来读取数据的泛型,并且只能读取类型为T的元素。
- : 这里的?可以是T类型或者T的父类, 被设计用来添加数据的泛型,并且只能添加T类型或其子类类型的元素。因为向上转型是安全的
-
super是父类对象的引用。可以通过super来访问父类中被子类覆盖的方法或属性。
Java的父类必须有默认构造函数,
Java获取二维数组行列长度
二维数组定义:int array[][] = new int[3][3];
获取行数: int rowLength = array.length;
获取列数: int colLength = array[0].length;
二维数组的遍历
class Test{
for(int i = 0; i < array.length; i++){
for(int j = 0; j < array[i].length; j++){
System.out.println(array[i][j);
}
}
}
& && | || 区别
& 和 &&, &&只要前面的判断条件为false, 后面的就不会执行, &即使前面为false, 也会执行后面的
| 和 || 同理
-
& 还可以作为按照比特位进行与操作
两位同时为“1”,结果才为“1”,否则为0
0&0=0; 0&1=0; 1&0=0; 1&1=1; -
| 按照比特位进行或操作, 只要前面一个为1, 结果就是1
可变参数
static void print(String...names) { // 接收数组
for(String name : names) {
System.out.print(name + " ");
}
System.out.println();
}
public static void main(String[] args) {
String[] names = {"jerry", "tom", "rose"};
print(); // 不传递参数
print("jerry", "tom"); // 传递多个参数
print(names); // 传递数组
}



浙公网安备 33010602011771号