admin管理员组文章数量:1532742
前言:本文是为了秋招准备,主要以点和面的形式进行复习。(小知识用点,多一点的将以面的形式展示)。
Java的特点
- 继承(集成一个父类,实现多个接口)
- 封装
- 多态
- 线程以对象的形式存在,因此多线程编程较为容易。
- 半解释半编译语言。(先翻译成class文件,再由JVM执行)。
JDK、JVM、JRE
JDK = JRE + 各种Java的api等开发的工具
JRE = JVM + 各种核心类库
简单来说JDK contains JRE contains JVM
Java和C++的区别
- Java有垃圾回收机制,因此不需要像C++那样手动释放内存,所以不用特别去关心内存泄漏的问题(当然Java也是有内存泄漏问题的)
- Java是没有指针的,C++是有的
- Java是集成单个父类,实现多个接口。而C++是可以多继承的。
抽象类和接口的区别
- 概念上的问题:抽象类抽象的是对象,一个模板。而接口抽象的是动作,是行为。
- 抽象类:
- 抽象类中可以有非抽象的方法,但是抽象方法只能存在于抽象类中。因为抽象类是不能呗实例化的。
- 抽象方法可以没有主体。
- 接口:
- 接口的方法默认是public abstract的,不用显示去写。
- 接口中的变量是final static的,所以要记得赋予初始值。
- 接口有一个默认的default,这个default可以让接口的方法不强制被重写。
Protect、Static、final
Protect和static只能修饰内部类
final内外都可以修饰
final、finally、finalize
final
- final修饰变量,表示这个是常量,不能被修改。
- final修饰方法,表示这个方法,不能被子类重写。
- final修饰类,表示这个类不能被集成,里面的方法全是final修饰的。
- final修饰对象,对象引用不能变,对象中的值可以变。
finally
- try catch语句中常用,在finally中的语句一定会被执行
- 常用于释放资源
finalize
- Object中的一个方法,在gc的时候会调用对象finalize方法实现对资源的清理
- finalize不保证一定会被执行
构造方法
- 特点:
- 没有返回值
- 与类同名
- 生成对象的时候自动被调用。
- 子类对象会调用父类对象来完成自身初始化。
- 子类的构造方法和父类的构造方法
- 在子类中会使用super这个关键字来代表父类。一般子类会有(java会自动生成)空的构造函数来调用父类的构造方法。
== 和equals的区别
- == 是值的判断,对于基本类型,就是值的判断,引用类型就是地址的判断。
- equlas本身是一个方法,是一个函数。里面的行为是我们自定义的。经典的用法就是String.equls和==的区别。
HashCode和equals
最重要的一点。如果不用散列表,比如HashMap、HashSet这种数据结构,两者其实没有本质上的联系。
如果采用了上述的数据结构:
- 会先判断两者的HashCode是否相等,如果两者的HashCode相等,那么就会被判定为同一个对象,这样就会插入失败。当HashCode相等的时候,就会调用equals方法判断两者是否相同,如果两者相同同样会插入失败。
try catch finally
try catch finally的执行顺序是这样子的
先去执行try中的内容,如果出现异常就跳转到catch中执行,最后执行finally中的内容。注意,如果如果再try 和 catch中执行了return,会先去执行finally中的内容在return,这就导致如果再finally执行return,那么在try catch中的return不会生效
Java中的IO部分
其实有两个大类分别是:
- 字节流
- InputStream:
- FileInputStream
- dataInputStream
- BufferInputStream
- OutputStream
- FileOutputStream
- dataOutputStream
- BufferOutputStream
类似这样的。在java中字符是通过jvm转换字节流得到的,比较耗时。所以还有住啊们的字符流
- InputStream:
- 字符流:
- xxxRead
- xxxWriter
- Java中的IO模型详解
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。- BIO(同步阻塞模型):用户在发出IO请求后会陷入阻塞状态。然后等待内核(系统调用)执行,把数据写会用户空见。但是这种模型,无法应对高并发。
- NIO(No-blocking-IO):对于传统的同步非阻塞IO模型
应用成语在发送IO请求之后,并不会阻塞自身,而是间隔一段时间,就发起read请求,去观察内核中数据是否准备好了。但是应用程序不断的轮询是挺小号CPU资源的。这个时候就可以使用I/O多路复用模型。
没戳,通过进行select/poll/epoll就可以实现一次的请求。具体怎么做请详见操作系统。IO多路复用模型减少了无效的系统调用。
同步IO和异步IO最大的区别在于
- BIO(同步阻塞模型):用户在发出IO请求后会陷入阻塞状态。然后等待内核(系统调用)执行,把数据写会用户空见。但是这种模型,无法应对高并发。
- 发起IO请求,期间等待CPU指挥DMA搬运树局(这一步异步不管,只发请求,同步产生区别)
- 请求进程阻塞自身,发生IO读写(这一步,同步请求进程会阻塞自身,异步不会阻塞自身)
深拷贝和浅拷贝
- 浅拷贝:拷贝一个地址
- 深拷贝:生成一个新的对象,这个对象的值和原来的值是一样的。
Java中的数据结构
总共有两个大类
Collection和Map
先说Collection
Collection
- List:在List当中,重点关注三个数据结构ArrayList、LinkedList、Vector。
- LinkedList:
- LinkedList的性能并不是特别好,一般都是用ArrayList。
- LinkedLis底层的数据结构是双向链表的数据结构。因此在头和尾的操作都是O(1)其余相同。
- 虽然用的是离散内存空间,但是需要花一部分空间存储next指正。
- ArrayList:
- ArrayList的底层是一个数组,占用的是连续空间。因此它支持随机访问。其次,ArrayList在遍历删除数据的时候是需要注意的。因为索引会搬迁。
- ArrayList的扩容机制。
- ArrayList在初始化的时候,如果调用无参构造器,一开始是没有初始化底层的数组elementData()的,等到调用add的时候才会初始化。懒加载来节省内存。
- ArrayList初始化的底层数组长度为10,需要扩容的时候会扩展到原来的1.5倍。负载因子为1,因为他不需要处理哈希碰撞的问题。
- Vector:是一个古老的数据类型。它是线程安全的。但是现在也不怎么用,底层和ArrayList很像,但是加入了Synchronized加锁了,所以他是线程安全的,但是他并没有很好的性能。
- LinkedList:
-
Set:在Set的部分重点关注HashSet、LinkedHashSet、TreeSet三者的异同。
- HashSet:其实底层就是一个HashMap。无序的
- LinkeHashSet:底层是一个LinkedHashMap。是有序的
- TreeSet:底层是红黑树,是有序的。
-
Map:老熟客之HashMap,老八股文了。
- Hashtable:
- 线程安全的
- 继承了Dictionary虚拟类,而HashMap是集成了abstractMap这个虚拟类
- 底层很相似,只不过Hashtable加了Synchronized
- 为什么Hashtable的性能没有ConcurentHashMap好:因为Hashtable在很多实例方法上加入了Synchronized关键字,相当于拿了对象的锁,这锁的粒度太大,并发效果就没有ConcurrentHashMap好了。而ConcurrentHashMap在java1.7之前使用的是分段锁,之后使用的是Node + 红黑树 + 大量的CAS操作,因此比Hashtable的性能好。
- HashMap:
- HashMap和HashTable.HashTable是线程安全的,但是现在已经被废弃不怎么使用了。HashMap是线程不安全的,CurrentHashMap是线程安全的,TreeSet的底层数据结构是TreeMap,而TreeMap的底层数据结构是红黑树。这个也是我们需要重点关注的四个数据结构啦。
- HashMap的底层数据结构:
- 在java1.7之前:底层的数据结构是数组+链表的数据结构。采用拉链法来解决哈希冲突,链表采用头插法插入数据,在扩容的场景下再加入数据,可能会造成循环链表的问题。
- 在java1.8之后:底层的数据结构是数组+链表or红黑树的结构。链表的长度在超过8之后会采用新的数据结构红黑树,数组的初始长度为16。
- 采用红黑树的好处。红黑树是一颗不严格的自平衡树,它允许局部部分不平衡,这样虽然损失一部分查找的性能,但是对于增删有挺大的好处的。
- Hashtable:
HashMap扩容机制详解
大体的流程是
- Map.put(),发现链表的长度已经达到8了,再判断数组的长度,如果数组的长度高于64,会将链表转换为红黑树,否则会优先进行数组扩容。
- HashMap的长度为什么是2的幂次方:其核心目的是为了减少哈希碰撞,通过减少哈希碰撞来实现高性能。底层原因是因为通过函数可以发现hash&(length-1),如果是2的幂次方,其实就是很多个1,这样能让值均匀的散步在数组上。
- HashMap的负载因子为0.75.这个负载因子过大,会造成频繁的哈希碰撞,这个负载因子过小,空间的利用率就不足了。
- HashMap在扩容的时候,在1.7之前是头插法,1.8之后变成了尾插法,虽然还有是覆盖的问题,但是不会造成循环链表了。
- 注意HashMap在resize()之后,是用一个大的数组替换了原来的数组,因此原数组中数据的下标是可能发生变换的。抓住本质是hash&(length-1)。length发生变化,下标发生变化。
- CurrentHashMap,在1.7之前,是把数据分块,变成一个个Segment,再根据Segment来加锁。但是这样的颗粒度太大了,于是在1.8之后做出了改变,在1.8之后是采用node+红黑树加上大量的CAS操作和Synchronized来完成的。
HashMap线程不安全详解
- 1.7java采用头插法,在扩容的时候会导致环形链表和数据丢失
在1.7采用头插法是认为,新加入的节点被访问的概率更高
先扩容,后插入 - 在1.8采用尾插法,在并发put的时候会导致数据被覆盖,先插入后扩容
ConcurrentHashMap详解
- 在java1.7,采用分段锁的机制进行保护,采用的锁是ReentrantLock。Segment + HashMap + ReentrantLock
- 在java1.8 采用 node + 红黑树+ Synchronized + 大量的CAS操作
- 为什么要使用Synchronized来代替ReentrantLock,主要原因是Synchronized可以自旋,而ReentrantLock不能自旋,在这一部分开销比较大。
- 1.8的ConcurrentHashMap,是针对每一个节点进行加锁操作的。比如红黑树,就针对红黑的头结点加锁。加锁的粒度更小,并发就更高了。
- 如何让HashMap变得线程安全
- 使用ConcurrentHashMap
- 使用HashTable
- Collections.synchronizedMap()
HashMap的put get
- 传入的时候判断数组是否为空,为空就进行初始化
- 计算hash&(length - 1)计算下标,hash是,hashcode异或hashcode右移16位的结果
- 查看是否发生了哈希冲突(hashcode相同,再用equals判断),没有就直接加入,有的话就用拉链法解决冲突。当链表长度大于8且数组长度大于64时,链表转换成红黑树
- 加入节点之后判断是否需要扩容
get就是去对应的索引找数据比较简单
有序的Map
- TreeMap:底层是一个红黑树,所以是有序的
- LinkedHashMap:底层维护了一个双向链表,所以是有序的
Java JVM部分
先介绍一下堆和栈的不同
- 数据结构不同:
栈是一种先进先出的线性,只能在一端操作
堆是一种树状的数据结构,但是它有唯一后继。分为大根堆和小根堆,子节点也是堆,只能在一端操作。 - 在java中的不同之处
- 堆对程序员是可见的,程序员可以控制堆的大小,调整新生代老年代的大小,做一些参数的调整,而栈是系统分配的,不受程序员控制
- 堆和栈的作用不同
- 堆一般用于存放对象,是垃圾回收机制主要处理的地方
- 栈一般用于存放线程私有的变量
Java很特殊的一点就在于他本质上是一个解释与编译共存的语言。编译语言的本质,是把代码翻译成机器指令然后让电脑去运行,而解释语言的本质是一行一行把代码翻译成指令去运行。
JVM内存分布这张图其实说明的很完整了。
JVM的内存有四大块
- 堆区域:是Java内存中最大最重要的一个部分,基本上来说成员变量都是在这里存储的。他主要被划分为3块
- 新生代Eden区和survivor区(survivor区分为survivor1和survivor2)
- 老年代
- 栈区域:如果说堆区域是线程共享的,那么栈区域就是线程私有的了。主要由
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 直接内存:直接内存是Java内存在操作系统内核开辟了的共享内存,数据直接映射在这片区域提升读写能力。
- 元空间/永生代:在这个地方主要放一些常量,或者类的一些静态变量等内容。
关于元空间、方法区、永生代的关系。
元空间和永生代是方法去的一个实现方式,方法去是Jvm的一个规范
元空间区在java8取代了永生去
所以应该问的是:
方法区是什么:方法区概念上装载了一个class类文件的信息,一些运行时常量、字符串常量。但是1.7后把字符串常量池搬到了堆中去。因为1.7的永生代占用的是java的堆内存,只有在full gc的时候才会被清理造成大量资源浪费
元空间和永生代的区别在哪儿:元空间用的是直接内存,用的不是Java的内存,而永生代用的是java的内存。这样元空间的大小不归jvm管了。
所谓GC
垃圾回收机制发生在堆区域中。
- 什么是垃圾?怎边辨别这个对象是垃圾?
- 垃圾就不会再次被用到的对象。这个对象还占据堆空间的内存,因此需要释放这部分内存。
- 如何判断这个对象已经死亡:
- 引用计数法:给对象添加一个引用计数器。如果有地方引用到它,计数器加1,引用失效计数器-1。问题在于如果存在相互引用,那么这个计数器永远不会归零。
- 可达性判断,通过GC ROOT作为节点访问,如果能访问到就不算死亡不算垃圾(一般来说要经过两次判断才会真正被判定为死亡)
- 判断一个无用类:ClassLoader被回收、class对象未被使用、不存在对应实例。
- 垃圾收集算法
- 标记清除算法:先标记哪些对象是要使用的,然后全部释放未被标记的内存空间。这个算法带来的问题是,内存空间都是离散的
- 标记复制算法:把内存分为了两块,他会标记还存活的对象,然后一次性复制到另外一块去。并且释放原来那一块的空间,优点是性能比较高,缺点是浪费内存
- 标记整理算法:标记存活的对象,然后开始移动,把端后的空间释放。
- 这就是为什么要分代了。根据代不同的特性可以选择不同的算法。比如新生代,对象的存活周期短,用标记复制算法性能较高,而老年代用标记整理或者标记清除的算法性能较高。
- JVM中的垃圾回收器
- Serial:单线程垃圾回收器,运行时暂停其他工作线程Stop The World。在新生代采用标记复制算法,在老年代采用标记整理算法。
- ParallelNew:并行垃圾回收器。是Serial的多线程版本。也是Stop THe World的。新生代标记复制,老年代标记整理。
- ParallelScavenge:和ParrallelNew差不多。只不过前者注重吞吐量,后者注重实时性。新生代采用标记复制,老年代采用标记整理
- 注意在java1.8版本,通过命令看出是UseParallelGC也就是默认是ParallerScavenge 收集新生代用Serial Old手机老年代
- CMS收集器:一个跨时代的收集器。他最大的特点就是并发,他不需要停止用户线程。采用的是垃圾清除算法,所以会有一定的内存碎片。他共有四个阶段,而且CMS及其注重用户体验,会有较低的停顿时间。
- 初始标记
- 并发标记
- 重新标记
- 并发清除
- G1收集器:
- 也是并发的
- 不需要搭配其他的垃圾回收器
- 可以预测停顿时间
- 基于标记整理算法
- GC的分类
- young gc/minor gc
- old gc
- mixed gc
- full gc
- 什么时候会触发minor gc:进程创建对象的时候,发现内存不足,会触发minor gc。或者新生代到达一定阈值会触发minor gc。
- 什么时候会触发Full GC?
- 主动调用system.gc
- 老年代空间不足的时候。比如发生minor gc之前,使用空间担保机制(事实上每次minor gc都会触发空间担保机制),发现老年代的空间不够大,就会触发full gc
- JVM的空间分配担保机制
所谓的空间担保机制。在发生minor gc的时候,会先使用空间担保机制,保证老年代有足够的空间去装载本次会进入老年代的对象。如果没有就会发生full gc。
类的加载
在java中,类的加载就是.class文件被jvm生成一个类对象的过程。类的加载是通过几个类加载器来实现的
- bootstrap classloader
- extension classloader
- application classloader
- 双亲委派机制:如果父类加载器能够加载这个类,用父类加载器来加载,这样的好处是,避免重复加载。
- 类的加载过程
- 加载:获取字节流生成对应的class对象
- 验证:进行一个语法的检查
- 准备:会为static的一些变量附上一些初始值
- 解析:将符号引用替换成直接引用,符号引用可以理解为com.ice.people,他唯一标识了一个资源,直接引用可以是,指向这个类对象的指针。
- 初始化
- 双亲
new一个对象的流程
在Java中new一个对象的流程分为两步,第一步查看对应的类是否被加载进入内存,如果不存先加载类,如果已经被加载进了内存,执行第二步生成对象
- 第一步:加载类
- 双亲委派机制:加载会把类信息传递到顶层的类加载器,当顶层的类加载器不能加载才会逐步往下传递,直到找到能加载该类的类加载器。
- 好处是不会重复加载类
- 而且能保证用户不会破坏java中的核心类
- 执行流程,Java会先把.java文件编译成.class文件(java的半解释半编译,.java编译成.class,交于不同的jvm去解释)
- 加载:生成一个class对象,并且在元空间存放必要的信息
- 验证:验证语法、
- 准备:为类变量划分空间并设置初始值
- 解析:符号引用替换成直接引用
- 初始化:调用java程序,把类变量定义成程序员定义的值
- 有了类对象之后就开始生成一个对象
- 判断类是否被加载过了,没有加载类需要先加载类。
- 在堆内存中划分空间
- 设置变量默认值,这个是把boolean = false这样,不是调用程序员定义的构造方法
- 设置对象头,设置偏向锁的、年龄、hashcode等等
- 初始化,先调用实例代码,再调用构造方法,先执行父类的代码
- 双亲委派机制:加载会把类信息传递到顶层的类加载器,当顶层的类加载器不能加载才会逐步往下传递,直到找到能加载该类的类加载器。
生成的对象一定在堆空间中
java生成的空间不一定在堆空间中
- 逃逸分析:经过JIT分析,如果一个对象不会逃逸,就会被优化在栈上创建,而不是创建在堆中
- 什么是逃逸:
- 这个对象作为参数传递就是逃逸
- 只在当前代码块中使用就不是逃逸
- 标量替代:被认定为不会逃逸的对象,会被分配在栈空间中,并且会被分解成一个个不能分割标量。
JMM
JMM是java的内存模型而不是内存结构
JMM是抽象的概念,核心目的是让java程序在各个平台都能达到一致的并发效果。
-
两种区域
在JMM中划分了两种区域,一种是主内存,一种是线程的工作内存。Java的所有变量都存储在主内存中(静态变量,实例变量),但是方法参数、局部变量等,不需要。工作内存存放了线程会用到的变量和主内存的部分拷贝。线程不能直接读写主内存的数据。线程之间也不能直接通信。一般都是,线程把主存的数据读进工作内存,修改之后再写回。 -
JMM定义了什么?JMM是围绕着三个特性建立的
- 原子性:一个原子操作是不可分割的
- 可见性:当一个线程修改了主存的数据,其他线程能够感知。
- 通过volatile关键字实现。
- Synchronized 在 unlock结束之后也会把数据同步到主存
- final修饰的也可以实现可见性
- 有序性:
- 指令重排是指:指令执行顺序与代码不同,但是结果相同。
- volatile通过内存屏障,防止指令重排序保证有序性
- Synchronize是通过线程同步实现有序性的
-
JMM的八个基本操作
- lock
- read
- load
- use
- Assign
- store
- write
- unlock
-
volatile的工作原理:
- 在线程的工作区域修改变量
- 同步主存
- 修改主存变量
- 失效其他线程的变量,通过CPU总线嗅探机制
- 其他线程因变量失效,就会返回主存中读取新的数据。
- volatile会在读写前面插入内存屏障
Java死锁案例
public class DeadLockTest {
public static void main(String[] args) {
Thread A = new Thread(new ThreadA());
Thread B = new Thread(new ThreadB());
A.start();
B.start();
}
}
class Resource {
public static Resource resourceA = new Resource();
public static Resource resourceB = new Resource();
}
class ThreadA implements Runnable {
@Override
public void run() {
System.out.println("线程A启动");
synchronized (Resource.resourceA) {
System.out.println("获取resourceA成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Resource.resourceB) {
System.out.println("获取resourceB成功");
}
}
return;
}
}
class ThreadB implements Runnable {
@Override
public void run() {
System.out.println("线程B启动");
synchronized (Resource.resourceB) {
System.out.println("获取resourceB成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Resource.resourceA) {
System.out.println("获取resourceA成功");
}
}
return;
}
}
Java同步锁案例
public class SynchronizedTest {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread threadA = new Thread(ticket,"a");
Thread threadB = new Thread(ticket,"b");
Thread threadC = new Thread(ticket,"c");
threadA.start();
threadB.start();
threadC.start();
}
}
class Ticket implements Runnable{
// 假定有20张票
private int ticket = 200;
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "售票员卖出"+ticket+"号票");
ticket--;
}
}
}
}
}
多线程部分
- volatile关键字:这个关键字可以保证,线程本身不适用自己的缓存,每次访问这个关键字的时候,都去内存中拿出对应数据。
- ThreadLocal的原理
- ThreadLocal出现,是为了保证每一个线程都有一个变量的副本,实现数据的隔离。每一个线程都有ThreadLocalMap,里面存放着变量副本。
ThreadLocal vs Synchronized - ThreadLocal本质上,是为了每一个线程创建了一个数据的副本,所以多线程在同一时间段内,访问的变量是不同的。而Synchroniezd是通过锁的机制实现了数据的共享,在同一个时间段内,多个线程访问的变量是相同的,这就是两者最本质的区别。
- ThreadLocal在我眼中是用空间换时间,而Synchronized是用时间换空间。
- ThreadLocal出现,是为了保证每一个线程都有一个变量的副本,实现数据的隔离。每一个线程都有ThreadLocalMap,里面存放着变量副本。
从java的调度看进程和线程
- 线程在java中就是一个对象,因此在Java的角度上,线程的管理比较方便
- 通常情况,在多线程并发时,如果有一个线程崩溃了,或者非法访问内存地址,会导致整个进程崩溃,这是通过信号来实现的,而在java中jvm重写了这个过程,所以在java中,线程的崩溃不会引起进程的崩溃。
- 在java中可以通过集成Thread类和实现runnable接口来编写一个线程
- 在java中,一个进程的内存被分为
- 堆空间
- 栈空间
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 元空间
- 直接内存
Java线程池
- 创建线程池的优缺点
优点- 减少了创建销毁线程带来的开销
- 方便管理线程
- 响应比较快
缺点 - 需要维护一个线程池
- 创建线程池:
- 创建线程池可以通过ThreadPoolExecutor,或者通过工具类来实现
- 核心参数
- corePoolSize:核心线程的数量,初期线程池中是没有线程的,只有任务来了,才会创建线程。核心线程池的数量设计。如果是IO密集型,也就是说对CPU压力负担不大的可以多设置一些,比如设计成CPU核数/1-阻塞系数。CPU密集型就设计的少一些,最好和CPU核数相同
- maxPoolSize:线程池中最多容纳线程的数量。注意只有当线程池中所有线程都不空闲,并且workQueue队列都满的情况,才会去临时的线程
- keepAlive:多出来的线程存活的时间
- unit:keepAlive的单位
- workQueue:
- 有界队列:ArrayBlockingQueue
- 无解队列:LinkedBlockingQueue
- 同步队列:
- threadFactory:制定创建线程的工厂
- handler:饱和策略,当workQueue已满,并且线程池中的线程池已经达到maxPoolSize时,对于新加入的任务的策略
- 丢弃,悄悄丢弃
- 选择最早没有执行的任务丢弃
- 抛出异常 Abort,默认策略
- 把这个任务返回给调用者(让提交这个任务的线程去执行)
java线程池踩坑记录
-
调用了Executor来创建线程池,而Executor创建线程池都是有无界队列,因此会出现OOM问题。比如遇到执行很久的任务,那工作队列会一直加任务,直到溢出。
-
Java的阻塞队列:
- 有界队列:底层是个Array
- 无界队列:默认长度为Integer.Max_value
- 同步队列:相当于只有一个单位的长度
-
Runnalbe和Callable
- Runnable不能抛出异常,没有返回值,而Callable可以跑出异常,有返回值
- Runnable可以作为Thread的参数,开启线程来使用,也可以通过线程池来使用,而Callable只能通过线程池
CAS详解
- CAS = compare and swap
- 三个概念,旧值 = 新值才会更新变量
V:var,需要更新的变量
E:expect,预期值
N:new,新值 - 在Java中有一个Unsafe类,里面定义了CAS操作,在Java中的CAS操作是被native修饰,也就是并不是由Java去实现的。
- CAS底层是操作系统的原语,保证安全性
- 在Java的Atomic底层就是调用Unsafe类完成CAS操作的
- CAS的弊端 ABA问题,在java中通过一个stamp版本号来解决这个问题
- CAS修改不成功会自身进入自旋,高并发大量的自旋浪费了cpu功能。但是CAS产生的原因,就是经过判断发生冲突的可能不大才会用乐观锁的。
- 思想之狭隘:i++的过程思考CAS的作用
AQS
- AQS维护了一个先进先出FIFO的队列CLH
- 用voliate修饰一个变量state = 0,表示资源空闲让进,= 1表示资源不用先,线程进队列
- 有两种第一资源的方式
- 独占资源:ReentrantLock
- 共享资源:ReentrantReadWriteLock,这个lock是独占资源和共享资源都用的。
JUC
JUC是java.util.concurrent,是java为了支持应对高并发提供的类。
对于JUC你需要掌握的
- Automic类在JUC中
- ThreadPoolExcutor
- Lock
- AQS
在Java中的锁
- 乐观锁:CAS
- 悲观锁:ReentrantLock、Sychronized、
- 轻量锁:Synchronized
- 偏向锁:Synchronized
- 重量锁:Synchronized
- 公平锁:ReentrantLock可以制定是不是公平锁
- 不公平锁:Synchronized是不公平锁,ReentrantLock可以是不公平锁
- 公平锁vs不公平锁:公平锁,当前线程或许对象锁资源,先进入一个等待队列,如果没有其他线程竞争资源,它就是对头,可以获得资源。不公平锁,就是先尝试获取对象,然后再进入队列
- 自旋锁:
- 可重入锁:Synchronized、ReentrantLock就是可重入锁
- 分段锁:ConcurrentMp
- 读写锁:ReadWriteLock
- 自适应锁:自适应锁和自旋锁放一起,因为在Synchronized的轻量锁,如果线程没有竞争到锁资源,就会陷入自适应自旋锁状态(其实不是加锁,就是不断循环)。自适应的原因是,想根据竞争到这个锁的可能性做出调整,如果竞争到锁资源的可能比较大,自适应久一些,如果竞争到资源的可能性比较小,就自适应短一些。
谈一谈在java中Synchronized和lock
浅谈一下Synchronized
-
使用方法
- 作用在实例方法上:锁定的是这个对象
- 作用在静态方法上:锁定的是这个类对象
- 作用在代码块上:看的是你传入的值
-
Synchronized的实现原理:
在一个对象的内部有一个monitor锁通过这个锁来完成Synchronized。通过这个monitor锁,最后再字节码锁住这一块代码的前后会加两条指令,monitorenter和monitorexsit,通过这两条指令,再调用操作系统的Mutex Lock完成,在这个过程中会从用户态切换进内核态,所以说Synchronized是重量锁。考虑到这一点对Synchronized做出了优化。
现在的Synchronized有四种状态- 无锁状态:
- 偏向锁状态:处于这个状态,被偏向的线程执行操作不需要加同步操作,一般会偏向第一个访问的对象
- 轻量锁状态:处于这个状态下,没有竞争到锁资源的线程会进入自适应自旋状态(不断轮询CPU)
- 重量锁状态:处于这个状态下,没有竞争到锁资源的线程会陷入阻塞的状态(切记,阻塞状态会携带着资源阻塞)
Synchronized只能升级,不能降级。升级的驱动力是多线程竞争,修改了Mark Word的字段
-
可重入锁
概念,当一个线程获得了对象的锁资源,这个线程的其他流程同样可以获得这个对象的锁资源。会有一个计数器+1
在java中,Synchronized和ReentrantLock都是可重入锁
synchronized和lock
- 在java中Synchronized和Lock
- synchronized是一个关键字,而lock是一个接口,他的实现类有ReentrantLock
- synchronized执行不受我们控制,只有在程序执行结束或者抛出异常之后才会释放自动释放锁资源,而lock必须手动去释放锁资源
- 在Synchronized中,如果竞争不到锁资源,会陷入阻塞,这个情况会携带着资源一起阻塞,很容易就造成了死锁,而在lock中这个问题就得到了很好的解决。我们可以设定等待时间,过了这个等待时间就不再加锁,或者加锁失败抛异常等策略
- 可以说Synchronized比Lock来说,lock更加灵活一些
Synchronized和ReentrantLock
ReentrantLock就是lock的一个实现类
- 等可以终端
- 可以实现公平锁
- 可以根据Condition分组唤醒线程
Unicode 和 Ascii的区别
- Unicode通常用两个字节表现一个字符,ASCii是用单字节表现一个字符,因此Unicode的作用范围更广
- 如果用Unicode来表示Ascii码,前一个字节为0
- Utf-8是Unicode的一种实现方式
在java中实现线程同步的方法
https://blog.csdn/qq_40178533/article/details/119698602
- Synchronized + notify + wait
- ReentrantLock + Condition
- Semaphore/CountDownLatch/CyclicBarrier
- AtomicInteger
- volatile + 阻塞队列
从java和Os的角度来程序运行的过程
从Java的角度
- 编写Java程序,行程.java文件
- 再通过编译器编译,生成.class字节码文件
- jvm翻译字节码文件,针对不同的操作系统生成不同的机器码,并执行,会找到main方法作为入口执行
从os的角度来看
预编译、编译、汇编、链接、载入
其中预编译、编译把代码转换成汇编码
汇编把汇编码转换成机器码
链接:分为静态链接和动态链接
- 静态链接:把引用的库全部加入可执行文件中,缺点是文件变得很大
- 动态链接:在引用处加一些小的描述,等到真正用的时候再把引入的代码加载入内存。
载入:将可执行文件载入到内存中区
红黑树
说到红黑树不得不说一下
树的结构从
二叉树 -> 二叉搜索树 -> AVL平衡树 ->红黑树
说一下为什么不用AVL树和红黑树
AVL树是严格的平衡,所以每一次插入修改,会频繁的引擎树的结构变化,所以适用于插入修改不多的场景。
而红黑树不那么严格的要求,所以插入修改带来的树的结构变化不太多。所以整体性能更好。
AVL的左旋与红黑树的左旋
补充
volatileto通过内存屏障,在写操作读操作前都会加上内存屏障,保证写操作立刻刷入内存,如果数据被修改读操作能感知
ThreadFactory是一个接口,里面只有一个方法newThread方法。实现了这个工厂类,可以帮助我们自定义一些线程放入线程池。
引用
在java中内存泄漏通常有两种情况。
内存中有不能被使用的数据段
内存中有大量不能被回收,长时间存活的大对象
- 强引用:被强引用修饰的对象,在java在GC过程中一定不会被回收,即使内存不够也不会被回收
- 软引用:内存不够才会被回收的对象,平时不会被回收。
- 若引用:无论内存是否充足,GC的时候会回收这些对象
- 虚引用:主要用于跟踪对象的被垃圾回收的状态
JVM何时启动
一般来说当我们执行
java -jar xxx.jar的时候
会先去找到 jvm.cfg文件 在找到jvm.ddl文件,执行,此时就运行了一个jvm实例了
JVM 8-18的一些新特性
- lamda表达式,允许我们把一个函数作为参数传递进入
- 用::实现方法引用
- stream流:一般用来做一些数据的过滤
- 引入新的关于时间的api
- 函数式接口
- 默认方法,default关键字,接口不一定要实现默认方法
JAVA异常体系
Error:这种错误不可捕获一定会导致进程终止,常见的OutOfMemeoryError、StackOverError
Exception:运行时异常和编译时异常
throw:代码中写,抛出一个异常,交由上层处理
throws:方法后面跟上,声明这个方法可能会抛出一个异常
自己实现一个锁系列
使用mutext实现一个读写锁
class ReadWriteLock` {
Mutext readLock;
Mutext writeLock;
volatile int readCnt;
void readLock(
readLock.lock();
readCnt++;
if(readCnt == 1) {
writeLock.lock();
}
)
void readUnLock() {
readLock.unLock();
radCnt--;
if(readCnt == 0) {
writeLock.unlock();
}
void wirteLock() {
writeLock.lock()
}
void writeUnlock(){
writeLock.unLock();
}
}
java的泛型
泛型可以理解为java引入的一个编译检测机制。
如果没有使用泛型,可能就会在list中加入不同类型的数据,这后续时候可能会有问题。
- 为了保护安全性,引入了泛型在编译时做了检查
- 除了保证安全,在取用数据的时候也避免了做一个强制转换
- 提高了代码的重用性
泛型可以不指定的类型的比如
泛型:T表示任意类型、E表示元素等等 - 泛型擦除:如果这个类没有界(也就是没有用extends或者super语法指定父类或者界限),那么最后会被编程Object,如果制定了就会被编程指定的界限
java的修饰符
- private:作用范围是这个当前类,同一个包下,或者子类不可见
- public:所有可见
- protect:同包下,或者子类可见,其余不可见
- default
String StringBuffer StringBuilder
- String StringBuffer StringBuilder都是final类,不允许被继承
- StringBuffer是线程安全的,StringBuilder是线程不安全的。Buffer锁的粒度是这个对象。
- 因为String是一个不可变对象,所以频繁的增删改效率不高,因此产生了StringBuffer、StringBuilder这个东西。
String str1 = "abc";
String str2 = new String(abc);
第一句话是,查看abc存在不存在,不存在就在堆中生成一个对象,这个对象最后会被放入常量池,然后str1指向这个变量。如果常量池中存在这个对象直接引用。
第二句,不管abc是否在常量池中存在,直接生成一个新的abc放入常量池,并且str2指向这个abc。所以str1 != str2
java的装箱和自动拆箱
java有很多装箱拆箱的类
Integer 与 int等等
而且这个装箱拆箱的会加载自己的缓存。
java的反射
创建一个对象
new
newInstance
clone
反序列化
反射的坏处是会暴露一些类的实现细节,但是反射的作用是帮助java在运行时动态的创建一些对象,并且调用他的方法
反射的一些api
类.class
Class.forName
对象.class
newInstance
getFiled
getMethod
getAnnotation
setAccessable
本文标签: Java
版权声明:本文标题:java学习分享 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dongtai/1725925899a1049360.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论