Java后端
2023-03-23 16:22:38 0 举报
AI智能生成
Java基础知识
作者其他创作
大纲/内容
<b>Java基础 </b><br>
概念
封装
只能通过规定的方法访问数据。隐藏类的实例细节,方便修改和实现。
继承
在已存在的类上面做扩展,从而减少代码的冗余,提高代码的复用率和执行效率
单继承 static方法 可以被继承,但是不能被重写
实现代码共享,减少创建类的工作量,使子类可以拥有父类的方法和属性。提高代码维护性和可重用性。提高代码的可扩展性,更好的实现父类的方法。
继承是侵入性的。只要继承,就必须拥有父类的属性和方法。<br>降低代码灵活性。子类拥有父类的属性和方法后多了些约束。<br>增强代码耦合性(开发项目的原则为高内聚低耦合)。当父类的常量、变量和方法被修改时,需要考虑子类的修改,有可能会导致大段的代码需要重构。
多态
<b>指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。</b>
接口
<b>成员变量 :默认用 public static final</b>
<b>成员方法 :默认用 public abstract</b>
<b>内部接口 :默认用public static</b>
<b>内部类 :默认用public static</b>
<b>静态方法: 默认用public</b>
<b>接口中的静态方法不能用final修饰</b>
<b>java的接口中还可以定义枚举类,并且接口中的静态方法和默认方法是java8新增的,默认方法可为接口提供新的功能,并且不影响老版本代码的实现,保证了向前兼容。</b><br>
抽象类
<ol><li>抽象类里不一定要有抽象方法。</li><li>有抽象方法的类,必须是抽象类。</li><li>抽象类里的抽象方法,子类要么全部重写,要么子类也要定义为抽象类。</li><li>抽象类里可以有成员变量。</li><li>可以有常量。</li><li>可以有构造方法 -- 构造方法不是为了自己创建对象的,是为了子类创建对象而初始化使用的,因为子类创建对象时,会先去调用父类的无参构造-super();</li><li>可以有抽象方法。</li><li>可以有非抽象方法。</li><li>外部抽象类不允许用static修饰 static修饰的方法可以被子类继承 但不能被子类重写</li><li>抽象类中可以存在static修饰的静态方法 并且可以直接调用抽象类中的静态方法</li></ol>
普通类
类的加载顺序
父类静态代码块--->子类静态代码块--->父类普通代码块--->父类构造方法--->子类普通代码块--->子类构造方法
重写
子类继承父类时将继承父类的方法重新实现的过程。
<ol><li>只有能被继承的方法才能被重写</li><li>final和static修饰的方法不能被重写</li><li>重写的方法参数、方法名、返回额类型必须相同</li><li>重写的方法权限修饰符不能更严格</li><li>子类重写的方法抛出的异常不能比超类更高级</li></ol>
重载
static
字面意思静态的,顾名思义就是和动态的相对,在Java中被称作是 静态变量或类变量。
final
instanceof
基础<br>
⾯向对象和⾯向过程的区别?
⾯向过程
⾯向过程性能⽐⾯向对象⾼。 因为类调⽤时需要实例化,开销比较⼤,比较消耗资源,所以当性能是最重要的考量因素的时候,⽐如单⽚机、嵌⼊式开发、Linux/Unix 等⼀般采⽤⾯向过程开发。但是⾯向过程没有⾯向对象易维护、易复⽤、易扩展。
⾯向对象
⾯向对象易维护、易复⽤、易扩展。
java抽象类和普通类的区别?
抽象类可以有构造函数,抽象方法不能被声明为静态。
抽象类不能被实例化
Java 方法访问权限修饰?
private 私有成员属性和方法 <b>只有本类可以调用,除内部类特殊情况</b>
默认不写 只有本类 同一个包下面的类
protected 本类 同包 不同包的子类<br>
public 本类 同包 不同包的类
<b>Java内部类</b>
内部类种类
成员内部类
成员内部类可以无条件访问外部类所有的成员属性和成员方法(包括private成员)<br>
如果要访问外部类的同名成员,需要以下方式<br>
外部类.this.成员变量<br>
外部类.this.成员方法
在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问<br>
要创建成员内部类的对象,前提是必须存在一个外部类的对象<br>
第一种方式:Outter outter = new Outter();Outter.Inner inner = outter.new Inner();//必须通过Outter对象来创建<br>
第二种方式:Outter.Inner inner1 = outter.getInnerInstance();<br>
内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限
成员内部类不允许 static修饰 属性 和 方法
静态内部类
静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法<br>
向外访问 只能<b>直接</b>访问static修饰属性和方法,同名用 Outter.str2。<br>
非静态的外部属性方法需要实例化外部对象调用
可以有 main方法
方法内部类
不允许使用访问权限修饰符该类堆方法以外的全部隐藏,除了此方法其他的都不能访问<br>
匿名内部类
匿名内部类必须继承一个抽象类或者实现一个接口。<br>
匿名内部类没有类名,因此没有构造方法。匿名内部类是唯一一种没有构造器的类
深入理解内部类
为什么局部内部类和匿名内部类只能访问局部final变量?<br>
背景: 当一个方法执行完成之后,局部变量的生命周期也就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问这个变量就不可能了。但是又必须需要这个变量,怎么办呢?Java采用了复制的手段来解决这个问题造器传参的方式来对拷贝进行初始化赋值。<br>
方案∶也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。<br>
问题∶当在run方法中改变变量a的值的话,会造成数据不一致性
解决:java编译器就限定必须将入参变量限制为final变量<br>
注意:在JDK8版本之中;,方法内部类中调用方法中的局部变量,可以不需要修饰为final,匿名内部类也是一样的,主要是JDK8之后堪加了Effectively final功能<br>
内部类的有点
一个内部类的对象能够访问创建它的对象的实现,包括私有数据
对于同一个包中的其他类来说,内部类能够隐藏起来
匿名内部类可以很方便的定义回调
使用内部类可以非常方便的编写事件驱动程序。
在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类<br>
能够非常好的解决多重继承的问题
内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独<br>
字符串
String
String类底层原理?<br>
String为什么保证不可变?
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变
使用 + 连接符 来进行String的拼接原理?
用常量字符串赋值给String引用 和 用new来创建字符串对象 的区别?
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
StringBuilder <br>
高效,但是线程不安全
StringBuffer
线程安全
字符串常量池在哪儿?<br>
Java 字符串常量存放在堆内存还是JAVA方法区?<br>
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。<br>
String创建对象
String str= "abc" 创建方式
创建对象的过程<br> 1 首先在常量池中查找是否存在内容为"abc"字符串对象<br> 2 如果不存在则在常量池中创建"abc",并让str引用该对象<br> 3 如果存在则直接让str引用该对象
String str= new String("abc")创建方式
创建对象的过程<br> 1 首先在堆中(不是常量池)创建一个指定的对象,并让str引用指向该对象。<br> 2 在字符串常量池中查看,是否存在内容为"abc"字符串对象<br> 3 若存在,则将new出来的字符串对象与字符串常量池中的对象联系起来(即让那个特殊的成员变量value的指针指向它)<br> 4 若不存在,则在字符串常量池中创建一个内容为"abc"的字符串对象,并将堆中的对象与之联系起来。(有可能此时常量池中的"abc"已经被回收,所以要先创建一个内容 为"abc"的字符串对象)
str 放在栈上,用 new 创建出来的字符串对象放在堆上,而 abc 这个字面量是放在常量池中。
为什么 Java 中只有值传递?
值传递:是指在调用函数时,将实际参数复制一份传递给函数,这样在函数中修改参数时,不会影响到实际参数<br>
引用传递:是指在调用函数时,将实际参数的地址传递给函数,这样在函数中对参数的修改,将影响到实际参数
然后 Java中 将一个值传递到方法中是不会影响原的值的,是通过复制的方式。<br> 1. 基本数据类型 放在栈中 每次赋值后都不会影响原来的值<br> 2. 引用数据类型 复制的是栈中的 指向堆 对象的地址 或者 句柄 现在指向的和原来指向的都是同一个对象。 你改了这个对象的值<br> 3. String类型 string 每次 new 改变对象
内存溢出和内存泄漏的区别
内存溢出 没有足够的内存可以使用
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
代码中存在死循环或循环产生过多重复的对象实体
启动参数内存值设定的过小
内存泄漏 强引用对象没有没有释放使用的空间长时间的堆积造成
常见异常?
<ol><li>ArithmeticException(算术异常)<br></li><li>ClassCastException (类转换异常)<br></li><li>IllegalArgumentException (非法参数异常)<br></li><li>IndexOutOfBoundsException (下标越界异常)<br></li><li>NullPointerException (空指针异常)<br></li><li>SecurityException (安全异常)<br></li></ol>
运行时异常与受检异常有何异同?
异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常<br>操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就<br>不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可<br>能因使用的问题而引发。Java 编译器要求方法必须声明抛出可能发生的受检异常,<br>但是并不要求必须声明抛出未被捕获的运行时异常。
里氏代换原则[能使用父类型的地方一定能使用子类型
JDK1.8新特性?
Lambda 表达式
Lambda 表达式是 JDK8 的一个新特性,<b>可以取代大部分的匿名内部类</b>,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。
对接口的要求:<b>Lambda 规定接口中只能有一个需要被实现的方法.</b>
语法形式为 () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to)。
<br>方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。 <br>
新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。
Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。
Date Time API − 加强对日期与时间的处理。 <br>
Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。 <br>
Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
面向对象特点?
封装 继承 多态 抽象类 接口
Java 程序如打日志?
注解
反射<br>
单例模式
就是当前进程确保一个类全局只有一个实例。
单例模式在内存中只有一个实例,减少了内存开支<br>单例模式只生成一个实例,所以减少了系统的性能开销<br>单例模式可以避免对资源的多重占用<br>单例模式可以在系统设置全局的访问点
单例模式一般没有接口,扩展很困难<br>单例模式不利于测试<br>单例模式与单一职责原则有冲突
要求生成唯一序列号的环境<br>在整个项目中需要一个共享访问点或共享数据<br>创建一个对象需要消耗的资源过多实例<br>需要定义大量的静态常量和静态方法(如工具类)的环境
IO
既然有了字节流,为什么还要有字符流?
字符流是由JVM将字节流抓换得到的。方便我们平时对 字符 进行流操作 <br> 如果是 图片 视屏 音频 等媒体文件用字节流 如果是字符 用字符流操作
集合<br>
Collection<br>
List<br>
<b>存取有序 ,元素可以重复,有序就有索引,有索引就可以通过索引操作元素。遍历方式:普通for,增强for,Iterator,ListIterator,集合转数组</b>
LinkedList和ArrayList并不能明确说明谁快谁慢
当数据量较小时,测试程序中,两者新增效率差别不是很明显,ArrayList效率比LinkedList高点;当数据量较大时,LinkedList的效率开始比ArrayList效率高了,而且数据量越大,越明显。
<b>ArrayList</b>
不安全,效率高,数组结构:增删慢;查询快
结构和扩容 ArrayList :初始化时创建一个空数组(Object)。add() 时:创建一个长度等于10的数组,将数据存入数组中,当数组长度大于10的时候会调用扩容方法(原始长度的1.5倍)
get查询时:数组结构,下标查询会很快,直接获取
指定位置add时:从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
效率:查询快、新增删除需要移动下标后面的元素。<br>
空间:扩容的1.5倍,可能存在的情况是,长度越大,浪费空间越大。
<b>LinkedList</b><br>
不安全,效率高,链表结构:增删快;查询慢<br>
LinkedList:初始化时创建一个空对象。add时:LinkedList有成员变量 first 和 last ,获取last节点,将last节点放入新节点的前驱中,将last节点的后驱中存入新的节点,修改成员变量last=新节点。
get时:将长度一分为2,index 在前面从前往后遍历,index 在后面从后往前遍历。
指定位置 add() 先将该位置的节点查询出来,拿出前驱节点放入新节点的前驱中,自身放入新节点的后驱中就可以了。
效率:查询慢,新增末尾和开头快,指定位置新增需要遍历查询所以不一定快。
空间:每一个节点存了三次,但是是同一个对象(只是指向对象的地址存了3次)
<b>Vector</b>
数组结构 安全(方法加锁) 效率低
存取无序,元素唯一。遍历方式:增强for,Iterator,集合转数组
Set
HashSet
哈希算法 哈希结构 存取无序 元素唯一
HashSet 如何检查重复?
当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。<br>
LinkHashSet
LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
TreeSet
二叉树算法可以排序
TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力
Queue<br>
Map
双列集合,键唯一,值可以重复,遍历方式:根据键找值,根据键值对找键和值
HashMap
⾮线程安全 null可以作为键 初始值16 原容量的2倍 数组+链表/红⿊⼆叉树 适用于在Map中插入、删除和定位元素<br>
HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。
用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
(n - 1) & hash
loadFactor 加载因子
oadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。<br>
loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。<br>
threshold
threshold = capacity(最大容量) * loadFactor,当 Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
HashTable
线程安全 null不可以作为键 初始值11 原容量2倍加1 数组+链表<br>
TreeMap<br>
基于红黑树实现 适用于按自然顺序或自定义顺序遍历键(key)。
LinkedHashMap
ConcurrentHashMap<br>
改进
将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构.
原理
默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树。<br>
如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表
HashMap 和 Hashtable 的区别
结构<br>
Hash Map 数组+链表+红黑树
Hash Table 数组+链表
空间
HashMap 默认16 且每次扩容2倍
Hash table 默认11,容量变为原来的 2n+1
安全
HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰
效率
因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
HashMap 和 HashSet 区别
HashSet 底层就是基于 HashMap 实现的
实现接口
Hash Map实现Map接口
Hash Table 实现set接口
设值方式
put方法
set方法
存储
Hash Map 存 K V 值,对K进行Hash计算
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性
HashMap 和 TreeMap 区别
TreeMap 默认是按 key 的升序排序
<b>多线程并发</b>
Java内存模型<br>
基本概念<br>
可见性<br>
一个线程在本地内存中修改了共享内存的数据,对于其他持有该数据的线程是“可见”的。<b>用volatile修饰的变量,就会具有可见性</b><br>
原子性
sychronized 保证原子性 不可分割,同生共死。<b>非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。</b><br>
有序性
此规则决定了持有同一个对象锁的两个同步块只能串行执行。<b>volatile 和 synchronized 两个关键字来保证线程之间操作的有序性</b><br>
内存模型结构<br>
主内存
缓存一致性协议(总线嗅探机制)<br>
工作内存(高速缓存 变量副本)
Java线程 计算操作<br>
原子操作<br>
read 读取 从主内存中读取数据<br>
load 载入 将从主内存中读取的数据加载到工作内存的副本中
use 使用 将工作内存中的数据做计算操作<br>
assign 赋值 将计算结果值赋值到工作内存中<br>
store 存储 将工作内存的值 赋值到主内存<br>
write 写入 将store中的变量赋值到主内存的变量中<br>
lock 加锁 将主内存中的变量锁 表示变量被线程独占状态<br>
unlock 解锁 将主内存中的变量解锁 解锁后其他线程可以锁定<br>
多线程
什么是线程和进程?
进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的
线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程<br>
程序计数器为什么是私有的?<br>
1. 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
2.通过字节码解释器的地址,线程切换回来可以知道上一次执行到哪了
虚拟机栈和本地⽅法栈为什么是私有的?<br>
虚拟机栈
每个 Java ⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。<br>从⽅法调⽤直⾄执⾏完的过程,就对应着⼀个栈帧在 Java 虚拟机栈中⼊栈和出栈的过程<br>
本地⽅法栈<br>
本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务
说说并发与并⾏的区别?
并发:同⼀时间段,多个任务都在执⾏ (单位时间内不⼀定同时执⾏) A 执行一段时间 让B执行一段时间<br>
并行:单位时间内有多个任务在执行 A B 同事执行
为什么要使⽤多线程呢?
从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程<br>
现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能<br>
提高CPU的利用率 目前大多数CPU都是多核的 可以都利用起来
守护线程的优先级很低 GC就是一个经典的守护线程
说说线程的⽣命周期和状态?<br>
新生状态<br>
在执行new Thread(s),线程对象一旦创建就进入新生状态
就绪状态
在线程对象创建完成后调用start方法,但是线程不会立刻调度执行,而是进入就绪状态,因为在运行前还有一些准备工作要做
运行状态
线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法
阻塞状态<br>
waiting sleep 超时等待 time waiting 正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态
终止状态 dead <br>
①run方法正常退出而自然死亡;<br>②一个未捕获的异常终止了run方法而使线程猝死
使⽤多线程可能带来什么问题?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序<br>运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁还有受限于硬件<br>和软件的资源闲置问题。<br>
线程创建方式?
继承 Thread<br>
实现 Runnable
callable<br>
线程池创建方式
守护线程?
只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作<br>
将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon方法<br>
Thread API
start()<br>
启动当前线程 调用当前线程的 run 方法
run()
线程要执行的操作
yield()<br>
释放当前CPU的执行权
join()<br>
在线程A中调用线程B的 join 方法,此时线程A将进入阻塞状态,等B执行完成后,线程 A才结束阻塞状态.<br>
sleep()
让当前线程睡眠 xxx 毫秒,在指定的时间内线程处于阻塞状态
isAlive()
判单线程是否存活
wait()<br>
让当前线程从运行状态转变成休眠状态,释放锁的资源<br>
notify()
唤醒指定被暂停的线程
notifyAll()
唤醒所有的被暂停的线程
说说 sleep() ⽅法和 wait() ⽅法区别和共同点?
两者都可以暂停线程的执⾏。
两者最主要的区别在于:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了锁 。<br>
Wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者notifyAll() ⽅法。<br>sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long timeout)超时后线程会⾃动苏醒。<br>
为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤run() ⽅法?
new ⼀个 Thread,线程进⼊了新建状态;调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,<br>当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏run() ⽅法的内容,这是真正的多线程⼯作。<br> ⽽直接执⾏ run() ⽅法,会把 run ⽅法当成⼀个 main<span style="font-size: inherit;">线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。</span>
线程优先级调度?
同优先级的线程 先进先出队列,先来先服务。
高优先级的线程 使用优先调度的抢占策略。高优先级的线程会抢占低优先级的执行权
setPriority()<br>getPriority()
使⽤多线程可能带来什么问题?
并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序<br>运⾏速度的,⽽且并发编程可能会遇到很多问题
内存泄漏、上下⽂切换、死锁还有受限于硬件和软件的资源闲置问题<br>
什么是上下⽂切换?
线程任务从保存到再次加载的过程就是一次上下文切换<br>
什么是线程安全问题?
当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态<br>
多个线程共享一个主内存变量时,在写操作可能受其他线程的影响发生数据冲突<br>
线程安全解决方法?<br>
1、同步代码块 2、同步方法 3、锁机制Lock
什么是线程死锁?
由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。
产⽣死锁必须具备以下四个条件
互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。<br>
请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。<br>
不剥夺条件: 线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。<br>
循环等待条件: 若⼲进程之间形成⼀种头尾相接的循环等待资源关系
如何避免线程死锁?
破坏互斥条件<br>
这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的<br>
破坏请求与保持条件<br>
⼀次性申请所有的资源
破坏不剥夺条件<br>
占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源<br>
破坏循环等待条件
靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件<br>
synchronized
synchronized 关键字 主要保证它所修饰的方法或者代码块在同一时间只有一个线程执行<br>
三种使⽤⽅式<br>
修饰实例⽅法<br>
作用于当前 <b>实例对象本身</b> 加锁,进入同步代码前要获得当前<b>对象实例的锁</b>
修饰静态⽅法<br>
作用于当前 <b>类对象本身</b> 加锁,进入同步代码前要获得<b>当前类对象的锁。</b>本类的其他静态方法都要等该方法释放锁<br>
修饰代码块
指定加锁对象(实例对象、类),对给定对象加锁,进入同步代码库前要获得给定对象的锁<br>
说说⾃⼰是怎么使⽤ synchronized 关键字,在项⽬中⽤到了吗?<br>
synchronized 关键字的底层原理
JDK1.6 之后的synchronized 关键字底层做了哪些优化
如偏向锁、轻量级锁、⾃旋锁、适应性⾃旋锁、锁消除、锁粗化等技术来减少锁操作的开销<br>
锁主要存在四种状态,依次是:⽆锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈⽽逐渐升级。
Lock 锁 和 synchronized 锁
区别<br>
synchronized 依赖于 JVM 虚拟机层⾯实现的 ,是Java语言的关键字<br>Lock 依赖于 API 是 JDK 层⾯实现,Lock是一个接口。lock() 和 unlock() ⽅法配合try/finally 语句块来完成
synchronized 使用中不需要手动解锁,Lock 需要手动解锁。代码实现 synchronized 要简单<br>
两者都是可重⼊锁
Sync是不可中断的。除非抛出异常或者正常运行完成
synchronized 非公平锁,Lock可以通过实现类 <b>ReentrantLock</b> 的构造方法决定是是否公平
在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,<br>但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。<br>
可实现选择性通知,可以指定唤醒哪个线程,notify()唤醒那个线程由JVM决定<br>可以实现多路通知功能也就是在⼀个Lock对象中可以创建多个Condition实例(即对象监视器)线程对象可以注册在指定的Condition中,<br>从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活<br>
Lock API<br>
void lock()
获取锁 <br>
void lockInterruptibly()
如果当前线程未被中断,则获取锁,可以响应中断
Condition newCondition()
返回绑定到此 Lock 实例的新 Condition 实例
boolean tryLock()
仅在调用时锁为空闲状态才获取该锁,可以响应中断
boolean tryLock(long time, TimeUnit unit)
如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
void unlock()
释放锁
ReadWriteLock锁<br>
ReadWriteLock 接口只有两个方法: <br>
Lock readLock() //返回用于读取操作的锁 <br>Lock writeLock() //返回用于写入操作的锁 <br>
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的。<br>
可重⼊锁
⾃⼰可以再次获取⾃⼰的内部锁,同⼀个线程每次获取锁,锁的计数器都⾃增1,所以要等到锁的计数器下降为0时才能释放锁<br><b>比如 在一个同步方法中调用了 另外一个同步锁</b>
可中断锁
响应中断的锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性
公平锁
尽可能的按照先后顺序获取锁。synchronized就是非公平锁,Lock可选择<br>
volatile 原理<br>
主要作⽤就是保证变量的<b>可见性。</b>然后还有⼀个作⽤是防⽌<b>指令重排序</b><br>
停止线程方式
<b>可以通过标记判断走完run()代码</b> 或者 <b>异常</b>
ThreadLocal
<b>ThreadLocal简介</b><br>
创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题<br>
底层<br>
使用的是类似 Hashmap 的结构
正对每一个具体的线程创建的
<font color="#e74f4c"><b>1、在Thread类中定义了一个 ThreadLocal的ThreadLocalMap的变量,结构是一个Key-value格式的。</b></font>
<b><font color="#e74f4c">2、get和set方法时都是先取当前线程对象(thread.currentThread()),然后在通过当前线程对象获取变量。</font></b>
<font color="#e74f4c"><b>3、所以 这个Map中的 key 存的是线程对象,value存的是具体的值。</b></font>
ThreadLocal特性
Synchronized是通过线程等待,牺牲时间来解决访问冲突<br>
并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,
内存泄漏问题
<b>使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。</b>
ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。
<ol><li>如果ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。</li><li>这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。</li><li>假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。</li></ol>
JAVA多线程中线程之间的通信方式
同步 这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。
while轮询的方式
wait/notify机制
管道通信
<b>线程池</b>
1. 为什么要⽤线程池?
<b>降低资源消耗</b><br>
通过重复利用已创建的线程减少线程的创建和销毁造成的消耗<br>
<b>提高响应速度</b><br>
当任务到达时,可以不用等待线程创建就可以直接执行<br>
<b>提⾼线程的可管理性</b>
线程时稀缺资源,不断地无限制创建,不仅会消耗系统的资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优、监管<br>
2、如何创建线程池?
Executors.newFixedThreadPool(3)
大小
核心线程数 == 最大线程数
时间
不会出现核心线程以外的线程,所以无需等待时间
队列
容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue(无界队列)
策略
默认的抛异常策略(AbortPolicy())
缺点
线程数量固定导致没有弹性,无界队列有溢出风险
Executors.newSingleThreadExecutor()
大小
核心线程和最大都是一个 <br>
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
时间
0
队列
容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue(无界队列)
策略
默认的抛异常策略(AbortPolicy())
缺点
一个线程效率低,无界队列有溢出风险OOM
Executors.newCachedThreadPool()
大小
核心线程数==0<br>
最大线程数==Integer.MAX_VALUE
时间
60s
队列
SynchronousQueue(同步策略)
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
策略
默认的抛异常策略(AbortPolicy())
缺点
允许创建线程数量Integer.MAX_VALUE ,创建大量的线程可能会OOM
Executors.newScheduledThreadPool(5)
大小
核心线程数 N<br>
最大线程数==Integer.MAX_VALUE
时间
0
队列
SDelayedWorkQueue (延迟策略)
此线程池支持定时以及周期性执行任务的需求。——延迟执行<br>
策略
默认的抛异常策略(AbortPolicy())
缺点
允许创建线程数量 和 队列的长度都可以 ==Integer.MAX_VALUE ,可能会OOM风险
ThreadPoolExecutor 自定义的方式
这样的处理方式让写的更加明确线程池的运行规则,规避资源耗尽的风险
3、为什么不建议使用内置线程池?
分析原生4种创建方式的缺点
4、线程池的参数有哪些?
<b>corePoolSize</b>
<b>任务队列未达到队列容量时,最大可以同时运行的线程数量</b><br>
<b>maximumPoolSize</b>
<b>任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数</b><br>
<b>keepAliveTime</b>
当线程数大于核心线程数时,多余的空闲线程存活的最长时间
<b>TimeUnit</b>
表示keepAliveTime的单位
<b>workQueue</b>
<b>任务队列,用来储存等待执行任务的队列</b>
<b>新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中</b>
<b>threadFactory</b>
线程工厂,用来创建线程,一般默认即可
<b>handler</b>
<b>如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时</b><br>
5、线程池的饱和策略?
ThreadPoolExecutor.AbortPolicy
抛出异常,中止任务
抛出拒绝执行 RejectedExecutionException 异常信息。<b>线程池默认的拒绝策略</b>。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行
ThreadPoolExecutor.CallerRunsPolicy
由向线程池提交任务的线程来执行该任务
<b>当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。<br>但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大。</b><br>因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略<br>
ThreadPoolExecutor.DiscardPolicy
不处理新任务,直接丢弃掉
ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最老任务,添加新任务
实现RejectedExecutionHandler接口,可自定义处理器。
6、线程池的阻塞队列有哪些?<br>
ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量
LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;
DelayedWorkQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。<br>
SynchronousQueue(同步队列) :CachedThreadPool 。SynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,CachedThreadPool 的最大线程数是<br>
7、线程池处理任务的流程了解吗?
如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务
8、线程池的大小配置多少合适?
常见的配置方式
如果是 CPU密集型应用,则线程池大小设置为N+1(或者是N),线程的应用场景:主要是复杂算法<br>
如果是IO密集型应用,则线程池大小设置为2N+1(或者是2N),线程的应用场景:主要是:数据库数据的交互,文件上传下载,网络数据传输等等<br>
线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务计算时间 + 1)
当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值。
9、执⾏execute()⽅法和submit()⽅法的区别是什么呢?
提交任务的类型
execute只能提交Runnable类型的任务
submit既能提交Runnable类型任务也能提交Callable类型任务。
异常
execute会直接抛出任务执行时的异常,可以用try、catch来捕获,和普通线程的处理方式完全一致
submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
返回值
execute()没有返回值
submit()有返回值,线程池会返回⼀个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执⾏成功
API定义
execute() 是在Executor接口中定义的
submit方法是ExecutorService接口里面定义的
submit方法被重载了三次,分别对用三个不同的参数
10、如何终止线程池?<br>
singleThreadPool.<b>shutdown</b>(); 只是不接受新任务
singleThreadPool.<b>shutdownNow</b>(); 对正在执行的任务停止
11、如何给线程池命名?
初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题
利用 guava 的 ThreadFactoryBuilder
12、线程池创建的线程会一直运行吗?
超过核心线程数的线程 空闲后根据配置的时间回收。
核心线程:默认是一直存活的,可以根据参数设置(allowCoreThreadTimeOut)
0 条评论
下一页