文章目录
- 一、基础
- 二、进阶
- 01、Object类中方法有哪些?
- 02、equals和==的区别?
- 03、String,StringBuilder,StringBuffer的区别?
- 04、自动装箱与拆箱?
- 05、集合
- 06、遍历Map的方式?
- 07、异常?
- 08、Synchronized 和 Lock 区
- 09、讲讲实例化(new的时候)顺序,比如父类静态数据 ,父类构造函数、父类属性、子类静态数据,子类构造函数
- 10、反射的原理,反射创建类实例的三种方式是什么
- 11、反射中,Class.forName 和 ClassLoader 区别?
- 12、this和super关键字
- 13、说一说你对Object 类对象中 hashCode 和 equals 方法的理解,在什么场景下需要重新实现这两个方法?
- 14、java 中 IO 流分为几种?
- 三、线程和线程池
一、基础
01、变量,常量
Java的数据类型分为两大类:
-
基本数据类型:包括数值类型 、字符类型 、 布尔类型 。
数值类型
:字节类型(byte),短整型(short),整形(int),长整型(long),单精度(float),双精度(double)内存占用:字节类型(1字节),短整型(2字节),整形(4字节),长整型(8字节),单精度(4字节),双精度(8字节)
初值值:字节类型(0),短整型(0),整形(0),长整型(0),单精度(0.0),双精度(0.0)
字符类型
:字符类型(char),内存占用(2字节),初始值(null)布尔类型
:布尔类型(boolean),内存占用(1字节),初始值(false) -
引用数据类型:包括 类 、 数组 、 接口 。
02、两个小面试题
+=符号的扩展
public static void main(String[] args){
short s = 1;
s+=1;
System.out.println(s);
}
分析:
s += 1 逻辑上可以看作是 s = s + 1 计算结果被提升为int类型,再向short类型赋值时发生错误,因为不能将取值范围大的类型赋值到取值范围小的类型。
但是, s=s+1是进行两次运算,+= 是一个运算符,只运算一次,并带有强制转换的特点,s += 1 就是 s = (short)(s + 1) ,因此程序没有问题编译通过,运行结果是2
常量和变量的运算
public static void main(String[] args){
byte b1=1;
byte b2=2;
byte b3=1 + 2;
byte b4=b1 + b2;
System.out.println(b3);//正确
System.out.println(b4);//错误
}
分析:
b3 = 1 + 2 , 1 和 2 是常量 ,为固定不变的数据,在编译的时候(编译器javac),已经确定了 1+2 的结果并没有超过byte类型的取值范围,可以赋值给变量 b3 ,因此 b3=1 + 2 是正确的。
反之,b4 = b2 + b3,b2 和 b3 是变量,变量的值是可能变化的,在编译的时候,编译器javac不确定b1+b2的结果是什么(两个byte类型的数据相加结果可能会超出byte类型所能表示的范围,因此编译器会将结果以int类型处理)因此会将结果以int类型进行处理,所以int类型不能赋值给byte类型,因此编译失败
03、数组
数组定义的三种方式“
//方式一:数组存储的数据类型[] 数组名字 = new 数组存储的数据类型[长度];
//方式二:数据类型[] 数组名 = new 数据类型[]{元素1,元素2,元素3...};
//方式三:数据类型[] 数组名 = {元素1,元素2,元素3...};
//注意:数组中只能保存类型一致的数据
04、类
类的三大特性:封装,继承,多态
05、抽象类和接口的对比
先说概念:
- 抽象类:
- 概念:用
abstract
关键字,修饰的类叫做抽象类,抽象类不可以实例化(也就是不能new) - 继承特点:类只能单继承
- 特点:
- 包含抽象方法的类一定是抽象类,抽象类中不一定包含抽象方法
- 抽象类的子类必须重写其中的
所有
抽象方法,否则该子类也是一个抽象类。 - 抽象类和普通类差不多,各种形式的成员变量都可以声明,也可以有构造方法
- 概念:用
- 接口:
- 概念:接口就是一个规范和抽象类比较相似,只不过比抽象类更加抽象。
- 继承特点:接口支持多继承
- 特点:
- 在接口中成员变量都
静态成员变量
,默认会加上public static fina
l修饰符。 - 接口中的所有方法默认为
抽象方法
,默认会加上public
关键字。但是:JDK8
之后,把接口和抽象类做的更加贴近了,即接口中可以有静态成员方法
。 - 声明接口使用interface关键字,默认是
public
类型。 - 实现接口时要实现接口中声明的所有原有的抽象方法。
- 在接口中成员变量都
对比分析:
对比项 | 抽象类 | 接口 |
---|---|---|
构造器 | 可以有 | 一定没有 |
实现方式 | 使用extends关键字 | 使用implement关键字 |
修饰符 | 可以是public,protected,default | 默认public,也只能是public |
是否有方法实现 | 可已有 | 只能是抽象方法实现(JDK8以后可以),普通的不可以有 |
06、权限修饰符
-
在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,
public protected default private 同一类中 √ √ √ √ 同一包中(子类和无关类) √ √ √ 不同包中的子类 √ √ 不同包中的无关类 √ -
可见,public具有最大权限,private则是最小权限。
编写代码时,如果没有特殊的考虑,建议这样使用权限: - 成员变量使用 private ,隐藏细节。 - 构造方法使用 public ,方便创建对象。 - 成员方法使用 public ,方便调用方法。
-
注意:不加权限修饰符,其访问能力与default修饰符相同
07、final、finally、finalize区别,怎么使用?
-
final
:用于声明属性
,方法
,类
,分别表示属性不可交变,方法不可覆盖,类不可继承 -
finally
:是异常处理语句结构的一部分,表示总是执行。 -
finalize
:Object类的一个方法,在垃圾回收器执行的时候会被调用。当该方法被系统调用时则代表该对象即将死亡
,但需要注意的是,我们主动行为上去调用该方法不一定会导致对象死亡
,这是一个被动的方法,不需要我们调用。
08、重载和重写区别?
在说重载和重写的之前先说一下如何声明一个方法,声明一个方法包括以下几部分:
-
访问修饰符 返回值类型 方法名 参数列表 方法体
-
例如:
public int count(int a,int b){ 方法体 }
重载:
- 发生在同一个类中,重载的方法就是
两个方法
- 特点:
- 1、方法名必须相同
- 2、方法的参数列表一定不一样
- 3、访问修饰符和返回值类型可以相同也可以不同
- 总结:重载只要关注的就是
参数列表
重写:
-
发生在父类和子类中
-
特点
- 1、方法名相同
- 2、参数相同
- 3、但是具体的实现不同
-
总结:重写主要关注的是方法体,一般都是子类对父类方法的增强,或者接口的实现类进行重写接口中的方法。
二、进阶
01、Object类中方法有哪些?
02、equals和==的区别?
-
equals可以判断这两个对象是否是相同的,这里的相同有
默认
和自定义
两种方式。 -
默认是地址比较:如果没有覆盖重写equals方法,那么Object类中默认地址比较,只要不是同一个对象(同一对象地址相同),结果必然为false。
-
自定义对象内容比较:如果希望进行对象的内容比较,即所有或指定的部分成员变量相同就判定两个对象相同,则可以覆盖重写equals方法。
-
==比较的是地址
- 基本类型:比较的是值是否相同;
- 引用类型:比较的是引用是否相同;
-
如果不重写equals方法,二者比较的是一样的东西
03、String,StringBuilder,StringBuffer的区别?
在JDK5之后,使用的是StringBuilder
,在JDK5之前使用的是StringBuffer
.
String | StringBuffer | StringBuilder |
---|---|---|
值是不可变的,导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | 值可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
多线程操作字符串 | 单线程操作字符串 |
- 注意:和
变量
进行拼接操作也不一定
都是使用StringBuilder
04、自动装箱与拆箱?
由于我们经常要做基本类型与包装类之间的转换,从Java 5(JDK 1.5)开始,基本类型与包装类的,装箱、拆箱动作可以自动完成。例如:
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
//注意:如果数值范围在-128到127会默认从常量池获取对象
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。
接下来说一下int
和Integer
吧:
-
int
和Integer
的区别:int是基本数据类型,直接存储的数值,默认是0 Integer是int的包装类,是个对象,存放的是对象的引用,必须实例化之后才能使用,默认是null
-
int
和Integer
比较的特性-
==
:比较的是地址Integer i = 4; Integer j = 5; System.out.println(i==j);//true /*数值类型int自动装箱成对象Integer,实际上是调用了Integer.valueOf()。 *如果数值范围在-128到127会默认从常量池获取对象,引用地址相同。 *超过范围会new对象出来,在堆中重新分配内存,指向这个新内存的引用地址必然不相同。 *int 和 Integer 比较时,会自动进行Integer的拆箱操作,即比较的是数值。 *但是:如果是new出来的对象,都在堆中重新分配内存,引用地址必然不相同。 */
-
equals 数值的比较
//Object里默认的equals方法比较的是地址 == //Integer里重写了equals()方法,比较的是value //下面是Integer重写Object类中的equals方法 public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
-
-
笔试题,关于 == 、 equals
public static void compareInt2Integer() { /** * i、j实际上都是调用了 Integer.valueOf(127),都是从常量池取的,所有内存地址一样,true */ Integer i = 127; Integer j = 127; System.out.println(i == j); // true System.out.println(i.equals(j)); // true /*到 * m、n 实际上都是调用了 Integer.valueOf(128),进行自动装箱操作,但是超过范围-128~127,会新建对象 * m、n 都是新创建的对象,会在堆中重新分配内存,地址不同,false */ Integer m = 128; Integer n = 128; System.out.println(m == n); //false System.out.println(m.equals(n)); //true /** * k取的是缓存,h是新创建的对象,false * 注意:只要是new出来的都是在堆区分配内存,这点是毋庸置疑的 */ Integer k = 127; Integer h = new Integer(127); System.out.println(k == h); //false System.out.println(k.equals(h));//true /** * new 操作符会分配内存,a、b都是新创建的对象 * == 比较的是引用的地址,false */ Integer a = new Integer(127); Integer b = new Integer(127); System.out.println(k == h); //false System.out.println(k.equals(h)); //true /** * Integer.valueOf() 会优先判断常量池缓存,缓存范围是-128~127,超过范围会new一个对象, * w 实际也是调用了 Integer.valueOf(127) * y 只是显示调用了 Integer.valueOf(127) * w、y 本质都是从常量池中拿取值,所以地址是相同的 */ Integer w = 127; Integer y = Integer.valueOf(127); System.out.println(w == y); // true System.out.println(w.equals(y)); //true /** * Integer.valueOf() 会优先判断常量池缓存,缓存范围是-128~127,超过范围会new一个对象, * w 实际也是调用了 Integer.valueOf(128) * y 只是显示调用了 Integer.valueOf(128) * w、y 超过范围,所以会在堆区开辟内存,两个值不相等。 */ Integer w = 128; Integer y = Integer.valueOf(128); System.out.println(w == y); // false System.out.println(w.equals(y)); //true /** * Integer和int == 比较,Integer会自动拆箱成int,数值比较,true * Integer和int进行算术运算符 + - * / 等,会自动拆箱 */ Integer x = 128; int z = 128; System.out.println(x == z); //true System.out.println(x.equals(z)); //true } /* * 总结: * 如果是Integer和Integer之间的==比较,那么当超过范围会因为创建新的对象导致二者不相等。 * 如果是Integer和int之间的==比较,不管Integer的值为多少,在比较之前都会被自动拆箱,比较的是值 */
05、集合
集合分为单列集合和双列集合:
- 单列集合为Collection接口下的实现类
- 双列集合为Map接口下的实现类
Collection接口下面有:List和Set
List:ArrayList(底层数组),LinkedList(底层链表)
查找多使用ArrayList,增删多使用LinkedList
Set:HashSet(hash算法)—>LinkedHashSet(链表实现),TreeSet(二叉树)
Set中存储自定义对象,并保证元素唯一
HashSet是value为null的,只是用key的HashMap
Map接口下的实现类有:HashMap(无序)—>LinkedHashMap(有序)
06、遍历Map的方式?
使用MAP中两个方法:
-
public Set<K> keySet()
: 获取Map集合中所有的键,存储到Set集合中。 -
public Set<Map.Entry<K,V>> entrySet()
: 获取到Map集合中所有的键值对对象的集合(Set集合)。
方式一:分析步骤:
-
获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法:
keyset()
-
遍历键的Set集合,得到每一个键。
-
根据键,获取键所对应的值。方法:
get(K key)
public class MapDemo01 { public static void main(String[] args) { //创建Map集合对象 HashMap<String, String> map = new HashMap<String,String>(); //添加元素到集合 map.put("C", "第一"); map.put("C++", "第二"); map.put("JAVA", "第三"); //获取所有的键 获取键集 Set<String> keys = map.keySet(); // 遍历键集 得到 每一个键 for (String key : keys) { //key 就是键 //获取对应值 String value = map.get(key); System.out.println(key+"的语言排名是:"+value); } } }
方式二:分析步骤
-
Map中存放的是两种对象,一种称为key(键),一种称为value(值),它们在在Map中是一一对应关系,这一对对象又称做Map中的一个Entry(项)。Entry将键值对的对应关系封装成了对象,即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对对象(Entry)中获取对应的键与对应的值。
-
1、获取Map集合中,所有的键值对对象(Entry),以Set集合形式返回。方法:entrySet()。
-
2、遍历包含键值对(Entry)对象的Set集合,得到每一个键值对(Entry)对象。
-
3、通过键值对对象(Entry),获取Entry对象中的键与值。 方法:getkey() getValue()
public class MapDemo02 { public static void main(String[] args) { // 创建Map集合对象 HashMap<String, String> map = new HashMap<String,String>(); // 添加元素到集合 map.put("C", "第一"); map.put("C++", "第二"); map.put("JAVA", "第三"); // 获取 所有的 entry对象 entrySet Set<Entry<String,String>> entrySet = map.entrySet(); // 遍历得到每一个entry对象 for (Entry<String, String> entry : entrySet) { // 解析 String key = entry.getKey(); String value = entry.getValue(); System.out.println(key+"的语言排名是:"+value); } } }
07、异常?
异常的父类是Throwable类,在Throwable类下有两个异常类:Exception,Error
-
Error类:专门用来处理严重影响程序运行的错误,可是通常程序设计者不会设计程序代码去捕捉这种错误,其原因在于即使捕捉到它,也无法给予适当的处理。
虚拟机错误:Virtual Machine Error,内存溢出:Out Of Memory Error,栈溢出:Stack Over Flow
-
Exception 类:包含了一般性的异常,这些异常通常在捕捉到之后,便可做妥善的处理,以确保程序继续运行。
-
Exception类中可细分为:
编译时期异常
,运行时期异常
-
编译时期异常:
checked异常
。在编译时期,就会去检查,如果有异常并且没有去处理,则会编译失败。 -
运行时期异常:
runtime异常
。在运行时期,才会检查异常,在编译时期,运行异常不会被编译器检测(不报错)数组越界异常,算术异常,类加载异常,空指针异常
-
-
异常的处理方式:两种
-
第一种:使用java
默认的的异常处理机制
,也就是我们不做捕获,出现异常直接停止运行。 -
第二种:我们自己进行处理,使用5个关键字:try,catch,finally,throw,throws
try-catch-finally - 把可能发生异常的代码使用try包裹起来,然后在catch中进行捕获,如果捕获到,我们可以自己处理,程序继续运行,也可以向外抛出。 - 如果向外抛出就要用到throw关键字了,我们在catch中抛出异常,不过这种做法没有实际意义,程序还是会停止执行,如果有程序调用这种会抛出异常的方法,对其异常进行处理程序也是可以运行的 - 使用throws关键字在指定方法上抛出异常,如果方法内的程序代码可能会发生异常,且方法内又没有使用任何的代码块来捕捉这些异常时,则必须在声明方法时一并指明所有可能发生的异常,并使用throws关键字抛出,以便让调用此方法的程序得以做好准备来捕捉异常。也就是可能产生异常的方法自己不捕获,让调用此方法的的方法进行捕获,如果调用此方法的方法不进行捕获,也可将其声明出去,再让别调用的方法进行捕获,大家都不处理,逐层抛出,不过这样最后会导致程序终止运行。
异常处理顺序:
多个异常分别处理。
多个异常一次捕获,多次处理。
多个异常一次捕获一次处理。
08、Synchronized 和 Lock 区
- 1、Synchronized 内置的Java关键字, Lock 是一个Java接口
- 2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
- 3、Synchronized 会自动释放锁(被修饰的类或者方法执行结束会自动释放),lock 必须要手动释放锁!如果不释放锁,死锁
- 4、Synchronized 线程 1(获得锁之后阻塞)、线程2(等待,傻傻的等),Lock锁就不一定会等待下去
- 5、Synchronized 可重入锁,不可以中断的,非公平
- 6,Lock 可重入锁,可以判断锁,非公平还是公平(可以自己设置,参考ReentrantLock的构造方法)
- 7、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
09、讲讲实例化(new的时候)顺序,比如父类静态数据 ,父类构造函数、父类属性、子类静态数据,子类构造函数
此题考察的是类加载器实例化时进行的操作步骤(加载–>连接->初始化
)。
- 父类静态代变量、父类静态代码块、
- 子类静态变量、子类静态代码块、
- 父类非静态变量(父类实例成员变量)、父类构造函数、
- 子类非静态变量(子类实例成员变量)、子类构造函数。
当我们进行new实例化进行操作的时候,JVM都进行了什么操作呢?
- 当我们使用new关键字进行实例化的时候,会进行如下几步处理:
加载
:此阶段加载类的字节码信息到内存链接
:此阶段进行验证,分配默认值,符号引用转直接引用初始化
:为成员进行赋值等
使用
对实例进行操作比如sons.toString()
对于我们的实例而言,我们只关注链接和初始化阶段即可:
- 链接阶段:我们按照类结构(成员变量(先),方法(后))的顺序(不是书写顺序),先对变量进行赋默认值0,对象的话为null。
- 初始化阶段:就是对对象进行赋值,比如c执行initc()进行赋值。
10、反射的原理,反射创建类实例的三种方式是什么
-
JAVA反射机制是在
运行状态
中,对于任意一个类
,都能够知道这个类的所有属性和方法,对于任意一个对象
,都能够调用它的任意一个属性和方法,这种动态获取类的信息
以及动态调用对象的方法和属性
的功能称为java语言的反射机制。 -
方式一:如果知道一个类的全类名,且该类在类路径下,可通过Class类的静态方法
forName()
获取实例:
Class cls1=Class.forName(classPath);
-
方式二:如果已知具体的类,通过类的
Class
获取,该方式最为安全可靠,程序性能最高Class cls2=Cat.class;
-
方式三:通过类加载器来获取类的对象
1、先拿到Cat的类加载器
ClassLoader classLoader=cat.getClass().getClassLoader();
2、通过类加载器得到对应的对象
Class cls=classLoader.loadClass(classPath);
11、反射中,Class.forName 和 ClassLoader 区别?
-
1、
Class.forName()
除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块,当然还可以在改方法中指定是否执行静态块。public static Class<?> forName(String className){....} public static Class<?> forName(String className, boolean initialize,ClassLoader loader){....}
-
2、
ClassLoader
只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance
才会去执行static块。public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
-
Class类其实也是使用的ClassLoader类加载器加载的
12、this和super关键字
this关键字主要有三个应用:
- 1、this调用本类中的属性,也就是类中的成员变量;
- 2、this调用本类中的其他方法;
- 3、this调用本类中的其他构造方法,调用时要放在构造方法的首行。
super关键字的用法有三种:
- 1、在子类的成员方法中,访问父类的成员变量
- 2、在本类的成员方法中,访问本类的另一个成员方法
- 3、在本类的构造方法中,访问本类的另一个构造方法
13、说一说你对Object 类对象中 hashCode 和 equals 方法的理解,在什么场景下需要重新实现这两个方法?
hashCode方法:是通过hash算法计算对象的hashCode值
equals方法:比较两个对象的地址是否相同,一般继承Object类的对象会对该方法进行重写
。
equals与hashcode的关系:
- equals相等两个对象,则hashcode一定要相等。
- 但是hashcode相等的两个对象不一定equals相等(可能存在哈希冲突,导致两个不同的对象的hash值相同)。
14、java 中 IO 流分为几种?
-
按功能来分:输入流(input)、输出流(output)。
-
按类型来分:字节流和字符流。
-
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
三、线程和线程池
01、创建线程的方式以及优缺点
方式一: 通过继承 Thread 类实现多线程,重写该类的run()方法,然后创建子类实例,调用start方法运行线程。
- 优点:代码编写最简单直接操作。
- 缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差。
方式二:通过实现 Runnable 接口实现多线程
- 1、编写一个类实现Runnable接口
- 2,重写父接口中的run()方法,将要执行的语句放入方法体中
- 3,创建Runnable接口子类的实例化对象,把它丢到Thread类中
- 4,通过 Thread 类的 start()方法,启动多线程序
- 为什么可以这样做:在 Thread 类之中,有这样一个构造方法,可以将一个 Runnable 接口的实例化对象作为参数去实例化 Thread 类对象
- 优点:线程类可以实现多个几接口,可以再继承一个类。
- 缺点:没返回值,不能直接启动,需要通过构造一个 Thread 实例传递进去启动。
方式三:实现Callable接口来实现多线程,类似于Runnable接口,可以创建一个可以被其他线程执行的实例
-
无论使用什么样的方式创建多线程,我们都需要使用Thread的
start()方法
来启动线程,通过查Thread类发现,此类的构造方法中只能接收Runnable
接口的实现类。 -
1、创建一个类实现Callable接口(通过泛型指定返值的类型)
-
2、把实现类的实例放到FutureTask的构造中,FutureTask是Runnable接口的实现类
-
3、把FutureTask的实例放到Thread中用来创建线程
-
优点:有返回值,拓展性也高 ,Callable可以有返回值,返回值在FutureTask里面,使用get方法获取返回值
-
缺点:Jdk5以后才支持,需要重写call()方法,结合多个类比如 FutureTask 和 Thread 类
public class CallableTest01 { public static void main(String[] args) { MyThread2 myThread2=new MyThread2(); //2.把实现类的实例放到FutureTask的构造中,FutureTask是Runnable接口的实现类 FutureTask futureTask=new FutureTask(myThread2); //3.把FutureTask的实例放到Thread中用来创建线程 new Thread(futureTask,"Thread_A").start(); new Thread(new FutureTask<String>(new MyThread2()),"Thread_B").start(); try { String returnValue=(String) futureTask.get();//此方法会产生阻塞,一般往后放 System.out.println(returnValue); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } //注意:泛型的参数等于方法的返回值 //1.创建一个类实现Callable接口(通过泛型指定返值的类型) class MyThread2 implements Callable<String>{ @Override public String call() throws InterruptedException{ System.out.println("Callable.call....."); return "aismall"; } }
02、线程状态有哪些?
任何线程一般具有五种状态,即创建
、就绪
、运行
、阻塞
、终止
线程新建好之后是不会自己执行的,调用start方法进入就绪状态,然后获得执行权后进入运行状态,通过调用方法可以阻塞线程,run方法执行结束或者调用stop方法线程死亡。
堵塞状态
:
-
一个正在执行的线程在某些特殊情况下,如被人为挂起将让出 CPU 并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用
sleep()
、suspend()
、wait()
等方法,线程都将进入堵塞状态,堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。 -
等待阻塞
:运行的线程执行wait()
方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤 醒,wait()是 Object 类的方法
。 -
同步阻塞
:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。 -
其他阻塞状态
:当前线程执行了sleep()
方法,或者调用了其他线程的join()方法,或者发出了 I/O 请求时,就会进入这个状态。线程会进入到阻塞状态,当sleep()状态超时
、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪状态。
03、线程等待:wait和sleep的对比
1、来自不同的类:
-
wait是Object类中的方法
-
sleep是Thread类中的方法
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程, 但是它的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 在调用sleep()方法的过程中,线程不会释放对象锁。 而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备
2、关于锁被释放
- wait会释放锁
- sleep不会释放锁
3、使用的范围不同
wait必须在同步代码块中
public synchronized void set(){
try {
// 睡眠线程,会被放入线程的等待池中,等待其它线程唤醒
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用notify方法,唤醒等待池中最先被睡眠的的此类(持有该类对象锁的线程)的线程
notify();
}
- sleep可以在任何地方休眠
04、线程之间的通信
Java 是通过 Object 类的 wait
、 notify
、 notifyAll
,这几个方法来实现线程间的通信的,又因为所有的类都是从 Object 继承的,所以任何类都可以直接使用这些方法。下面是这三个方法的简要说明:
- wait:调用此方法的线程进入睡眠状态,直到其它线程调用该线程的 notify 方法才能被唤醒。
- notify:唤醒同一对象(synchronize锁住的对象)监听器中调用 wait 的第一个线程。(等待的线程会去等待池中排队,最先调用等待方法的线程优先排队,自然优先被唤醒)。
- notifyAll:唤醒同一对象监听器中调用 wait 的所有线程,具有最高优先级的线程首先被唤醒并执行。
05、mt.run() : 和 mt.start()的区别
前景提要:
- 假设我有一个继承了Thread类的MyThread类,然后创建一个该类的对象:
MyTread mt=new MyThread();
- 执行:在main方法中执行mt.run() 和 mt.start()有什么区别?
- 分析:
- 执行
mt.run();
:类似于一个类执行了自己的成员方法,就是一个普通的类,属于单线程
。 - 执行
mt.start();
:多线程
的开启形式,会开辟一个新的栈空间,执行run方法, 每次执行mt.start();
方法,JVM都会请求OS(操作系统)开辟一条线程,然后在jvm中创建这个线程的栈内存。
- 执行
06、什么是线程死锁?
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源
或者由于彼此通信
而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)
。
例如:线程A拿着资源1,想要线程B的资源2,线程B拿着资源2,想要线程A资源1
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
07、构成死锁的条件以及如何预防死锁发生?
构成死锁的条件是:
-
互斥
: 某种资源一次只允许一个进程访问,即该资源一旦分配给某个进程,其他进程就不能再访问,直到该进程访问结束。其实就是对资源加了同步锁(例如:synchronize) -
占有且等待
: 一个进程本身占有资源(一种或多种),同时还有资源未得到满足,正在等待其他进程释放该资源。 -
不可抢占
: 别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。 -
循环等待
: 存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源。 -
当以上四个条件均满足,必然会造成死锁,相反,
而只要上述条件之一不满足,就不会发生死锁
。
只要打破四个必要条件之一就能有效预防死锁的发生:
打破互斥条件
:改造独占性资源为虚拟资源,大部分资源已无法改造。打破不可抢占条件
:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。打破占有且等待条件
:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。打破循环等待条件
:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
08、什么是线程池?
好多同学知道如何创建线程池,但是让他们描述什么是线程池的时候,却说不清楚了,下面就来谈谈什么是线程池以及一些概念!
线程池
:
- 是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
- 如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 假设一个服务器完成一项任务,创建线程时间T1 ,在线程中执行任务的时间T2,销毁线程时间为T3,如果:T1 + T3 远大于 T2,则可以采用线程池,大大缩短T1、T3时间,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
线程池管理器
(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;工作线程
(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;任务接口
(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;任务队列
(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
当线程池中有任务需要执行时,线程池会判断:
- 如果池里的
核心线程数
没有占满就会新建线程池进行任务执行, - 如果线程池中的线程数量已经超过
核心线程数
,这时候任务就会被放入任务队列中排队等待执行; - 如果任务队列也满了,并且线程池没有达到最大线程数,就会新建非核心线程来执行任务;
- 如果超过了最大线程数,就会执行拒绝策略。
09、创建线程池的方式?
创建线程池可以使用Executors
类中的静态方法来创建线程池,但是不推荐,因为底层还是调用ThreadPoolExecutor类来创建线程池
创建线程池推荐使用ThreadPoolExecutor类
,该类的构造方法中有七大参数,用来设置线程池的属性:
public ThreadPoolExecutor(int corePoolSize,//核心线程池大小
int maximumPoolSize,//最大核心线程池大小
long keepAliveTime,//存活时间,没人调用时的存活时间,过期自动释放
TimeUnit unit,//超时单位
BlockingQueue<Runnable> workQueue,//阻塞对列
ThreadFactory threadFactory, //创建线程的工厂,用来创建线程,一般不动
RejectedExecutionHandler defaultHandler)//拒绝策略
{
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);
}
拒绝策略有四种:
-
RejectedExecutionHandler
接口的四个实现类,也是ThreadPoolExecutor类中的内部子类(使用默认的拒绝策略即可)默认拒绝策略:AbortPolicy())
不同拒绝策略的区别:
-
new ThreadPoolExecutor.AbortPolicy()
:队列满了,再创建线程,不处理这个创建过程,抛出异常最大承载线程个数:最大核心线程池+阻塞对列
-
new ThreadPoolExecutor.CallerRunsPolicy()
:哪来的去哪里 -
new ThreadPoolExecutor.DiscardPolicy()
:队列满了,丢掉任务,不会抛出异常 -
new ThreadPoolExecutor.DiscardOldestPolicy()
:队列满了,尝试去和最早的竞争,也不会抛出异常!
10、线程池的状态有哪些?
参考链接:https://www.cnblogs.com/shujiying/p/11924465.html
线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。
1、RUNNING
-
状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
-
状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0
-
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
2、 SHUTDOWN
- 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
- 状态切换:调用线程池的
shutdown()
接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
- 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
- 状态切换:调用线程池的
shutdownNow()
接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
-
状态说明:当所有的任务已终止,
ctl记录的任务数量为0
,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 -
状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING,当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
- 状态说明:线程池彻底终止,就变成TERMINATED状态。
- 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
注意:线程池的状态及工作线程数的表示是使用线程池类中原子类的 ctl 属性
:一部分二进制位表示状态
,一部分表示工作线程数
) 。