Java面试题总结 | Java基础部分(持续更新)
Posted 小七rrrrr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试题总结 | Java基础部分(持续更新)相关的知识,希望对你有一定的参考价值。
Java基础
文章目录
- Java基础
- 一个Java文件里可以有多个类吗(不含内部类)?
- 创建对象的方法
- 面向对象和面向过程
- 简述自动装箱拆箱
- Java代码块执行顺序
- java中的基本数据类型对应的字节数
- 包装类型和基本数据类型的场景
- java中的关键字分类
- final关键字
- static关键字
- 字符串比较问题
- 多态
- 简述throw与throws的区别
- java的异常
- 捕获异常机制
- Java 1.8新特性
- Sting类的常见方法
- 四种修饰限定符
- 修饰符范围
- private修饰符
- 请介绍全局变量和局部变量的区别
- 构造方法的特点
- 为什么要有包装类型
- 内部类
- 简述内部类及其作用
- String和StringBuffer有什么区别?
- 说一说重写与重载的区别
- 深拷贝和浅拷贝
- 静态代码块/非静态代码块什么时候执行
一个Java文件里可以有多个类吗(不含内部类)?
- 一个java文件里可以有多个类,但最多只能有一个被public修饰的类;
- 如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
创建对象的方法
-
使用 new 关键字(最常用):
ObjectName obj = new ObjectName(); -
使用反射的Class类的newInstance()方法:
ObjectName obj = ObjectName.class.newInstance(); -
使用反射的Constructor类的newInstance()方法:
ObjectName obj = ObjectName.class.getConstructor.newInstance(); -
使用对象克隆clone()方法:
ObjectName obj = obj.clone(); -
使用反序列化(ObjectInputStream)的readObject()方法:
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME)))
ObjectName obj = ois.readObject();
public class ObjectCreate
private static final String FILE_NAME = "employee.obj";
public static void main(String[] args) throws Exception
// 使用 new关键字 创建对象
Employee employee = new Employee();
employee.setName("张三");
// 使用 Class类的 newInstance()方法
// Employee employee2 = (Employee) Class.forName("Employee").newInstance();
Employee employee2 = Employee.class.newInstance();
employee2.setName("xxx2");
System.out.println("Class类的newInstance()方法:" + employee2);
// 使用 Constructor类的newInstance()方法
Employee employee3 = Employee.class.getConstructor().newInstance();
employee3.setName("xxx3");
System.out.println("Constructor类的newInstance()方法:" + employee3);
// 使用 clone()方法:类必须实现Cloneable接口,并重写其clone()方法
Employee employee4 = (Employee) employee.clone();
// employee4.setName("xxx4");
System.out.println("对象clone()方法:" + employee4);
// 使用 反序列化ObjectInputStream 的readObject()方法:类必须实现 Serializable接口
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME)))
oos.writeObject(employee);
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_NAME)))
Employee employee5 = (Employee) ois.readObject();
System.out.println("反序列化:" + employee5);
面向对象和面向过程
-
面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
-
面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
-
面向对象相比于面向过程更易扩展、复用
-
面向对象,它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
简述自动装箱拆箱
java每一个基本数据类型都会对应着一个包装数据类型
自动装箱就是将基本数据类型转换为包装数据类型的过程
自动拆箱就是将包装数据类型转换为基本数据类型的过程
Java代码块执行顺序
父类的静态代码块
子类的静态代码块
父类的构造代码块
父类的构造方法
子类的构造代码块
子类的构造方法
普通代码块
class Father
static
System.out.println("Fatcher class");
public Father()
System.out.println("Father");
class Son extends Father
static
System.out.println("Son class");
public Son()
System.out.println("Son");
Fatcher class
Son class
Father
Son
java中的基本数据类型对应的字节数
short - 2个字节
int - 4个字节
long - 8个字节
float - 4个字节
double - 8个字节
char - 2个字节
byte - 1个字节
boolean - 一位
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768 ~ 32767 |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 |
char | 16 | 2 | ‘u0000’ | 0 ~ 65535 |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
boolean | 1 | false | true、false |
包装类型和基本数据类型的场景
public static void main(String[] args)
int num1 = 10;
int num2 = 12;
swap(num1,num2);
System.out.println(num1 + " " + num2);
public static void swap(int a,int b)
int temp = a;
a = b;
b = temp;
System.out.println(a);
System.out.println(b);
12 10
10 12
也就是交换失败
public static void main(String[] args)
int num1 = 10;
int num2 = 12;
swap(num1,num2);
System.out.println(num1 + " " + num2);
public static void swap(Integer a,Integer b)
int temp = a;
a = b;
b = temp;
System.out.println(a + " " + b);
也是交换失败的
java中的关键字分类
分类 | 关键字 | ||||||
---|---|---|---|---|---|---|---|
访问控制 | private | protected | public | ||||
类,方法和变量修饰符 | abstract | class | extends | final | implements | interface | native |
new | static | strictfp | synchronized | transient | volatile | enum | |
程序控制 | break | continue | return | do | while | if | else |
for | instanceof | switch | case | default | assert | ||
错误处理 | try | catch | throw | throws | finally | ||
包相关 | import | package | |||||
基本类型 | boolean | byte | char | double | float | int | long |
short | |||||||
变量引用 | super | this | void | ||||
保留字 | goto | const |
final关键字
final代表不可变的,被final修饰的类不可以被继承,修饰变量不可以被修改,被final修饰的方法不可以被重写
static关键字
static关键字可以修饰类、方法、变量、初始化块、内部类,被static修饰的成员都是类成员,可以只用类名的方式进行访问。
- 被static修饰的变量,叫做静态变量,只加载一次
- 静态代码块也是只加载一次,不论创建多个对象都是被加载一次
- 如果是非静态的内部类,需要通过外部类进行创建,而静态内部类不需要,且不可以访问外部类的非静态成员
字符串比较问题
String s1 = "abc";
String s2 = new String("abc");
String s3 = "a"+"bc";
System.out.println(s1 == s2);//false
System.out.println(s1 == s3);//true
System.out.println(s3 == s2);//false
System.out.println(s1 == s1.intern());//true
System.out.println(s1 == s2.intern());//true
多态
是指子类对象可以直接赋值给父类变量,在运行时依然表现出子类的特征,这意味着同一类型的对象在执行同一个方法时,可能表现出多种行为特征。
简述throw与throws的区别
throw一般是用在方法体的内部,由开发者定义当程序语句出现问题后主动抛出一个异常。
throws一般用于方法声明上,代表该方法可能会抛出的异常列表。
java的异常
分为运行时异常和编译时异常,编译时异常必须显示的进行处理,运行时异常可以使用throws抛出或者try catch进行捕获
throw和throws的区别
总结下 throw 和throws 关键字的区别
1、写法上 : throw 在方法体内使用,throws 函数名后或者参数列表后方法体前
2、意义 : throw 强调动作,而throws 表示一种倾向、可能但不一定实际发生
3、throws 后面跟的是异常类,可以一个,可以多个,多个用逗号隔开。throw 后跟的是异常对象,或者异常对象的引用。
4、throws 用户抛出异常,当在当前方法中抛出异常后,当前方法执行结束(throws 后,如果有finally语句的话,会执行到finally语句后再结束。)。可以理解成return一样。
异常抛出之后,会跳转到调用这个方法或类的上一层,运行的系统会找到处理此异常的异常处理器,返回对应的错误
try-catch-finally 如何使用?
try
块 : 用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。catch
块 : 用于处理 try 捕获到的异常。finally
块 : 无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。
注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
捕获异常机制
当catch多个异常的时候,如果前面的catch先捕获了,那么后面的catch将不会捕获这个异常
public static void main(String[] argßs) throws Exception
testAbove();
public static void testException() throws Exception
try
System.out.println(10%0);
catch(ArithmeticException r)
System.out.println("Catch function exception" + r.toString());
catch (Exception e)
throw new Exception("testException()方法出现异常"+e.toString());
public static void testAbove() throws Exception
try
testException();
System.out.println("execute test above ");
catch(Exception e)
throw new Exception("testAbove()方法执行出现异常"+e.toString());
输出
被子类捕获
Catch function exceptionjava.lang.ArithmeticException: / by zero
execute test above
在这里插入图片描述
Java 1.8新特性
- 新增lambda表达式
- 新增函数式接口
- 新增stream api
- hashmap和concurrenthashmap实现底层优化
- jvm内存布局进行了修正,元数据区取代了永久代
Sting类的常见方法
和长度有关的方法
返回类型 方法名 作用
int length() 得到一个字符串的字符个数(一个中文是一个字符,一个英文是一个字符,一个转义字符是一个字符)
和数组有关的方法
返回类型 方法名 作用
byte[] getBytes() 将一个字符串转换成字节数组
char[] toCharArray() 将一个字符串转换成字符数组
String[] split(String) 将一个字符串按照指定内容劈开
和判断有关的方法
返回类型 方法名 作用
boolean equals(String) 判断两个字符串的内容是否一模一样
boolean equalsIgnoreCase(String) 忽略大小写的比较两个字符串的内容是否一模一样
boolean contains(String) 判断一个字符串里面是否包含指定的内容
boolean startsWith(String) 判断一个字符串是否以指定的内容开头
boolean endsWith(String) 判断一个字符串是否以指定的内容结尾
和改变内容有关的方法
和改变内容有关的方法,都不会直接操作原本的字符串
而是将符合条件的字符串返回给我们,所以注意接收
返回类型 方法名 作用
String toUpperCase() 将一个字符串全部转换成大写
String toLowerCase() 将一个字符串全部转换成小写
String replace(String,String) 将某个内容全部替换成指定内容
String replaceAll(String,String) 将某个内容全部替换成指定内容,支持正则
String repalceFirst(String,String) 将第一次出现的某个内容替换成指定的内容
String substring(int) 从指定下标开始一直截取到字符串的最后
String substring(int,int) 从下标x截取到下标y-1对应的元素
String trim() 去除一个字符串的前后空格
和位置有关的方法
返回类型 方法名 作用
char charAt(int) 得到指定下标位置对应的字符
int indexOf(String) 得到指定内容第一次出现的下标
int lastIndexOf(String) 得到指定内容最后一次出现的下标
四种修饰限定符
修饰成员变量和方法:
- public:可以被任何包下任意类的成员进行访问
- protected:可以被该类内部的成员访问,可以被同一包下的其他类进行访问,可以被他的子类访问
- default:可以被该类的内部成员访问,可以被同一包下的其他类进行访问
- private:可以被该类的内部成员访问
在修饰类的时候:
- defalut:可以被同一包下的其他类访问
- public:可以被任意包下的任意类访问
修饰符范围
范围 | private | default | protected | public |
---|---|---|---|---|
同一类 | √ | √ | √ | √ |
同一包中的类 | √ | √ | √ | |
同一包中的类、不同包中的子类 | √ | √ | ||
所有 | √ |
private修饰符
private主要用来修饰变量和方法,一般不会用来修饰类,除非是内部类。
被private修饰的变量和方法,只能在自己对象内部使用,当一个变量或方法被定义为私有变量,则在别的类中用该类实例化的对象将无法直接访问该成员变量和方法。(在自身所在类的内部,依旧可以访问)
如果是继承关系呢?也是不行的,你依然无法通过super来调用被private修饰的方法和变量。
请介绍全局变量和局部变量的区别
Java中的变量分为成员变量和局部变量,它们的区别如下:
成员变量:
- 成员变量是在类的范围里定义的变量;
- 成员变量有默认初始值;
- 未被static修饰的成员变量也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同;
- 被static修饰的成员变量也叫类变量,它存储于方法区中,生命周期与当前类相同。
局部变量:
- 局部变量是在方法里定义的变量;
- 局部变量没有默认初始值;
- 局部变量存储于栈内存中,作用的范围结束,变量空间会自动的释放。
注意事项
Java中没有真正的全局变量,面试官应该是出于其他语言的习惯说全局变量的,他的本意应该是指成员变量。
构造方法的特点
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
为什么要有包装类型
new对象就会在堆上分配空间,如果我们频繁的使用Integer和Byte对象的时候,这样就会频繁的访问堆空间,对垃圾回收机制会造成一定影响,影响了效率,基本数据类型只在栈上创建、使用以及销毁,提高了效率节省了空间
Java是面向对象的编程语言,为了让基本数据类型具有面向对象的特征,对其进行包装,使其具有面向对象的特征,具有属性和方法;并且在某些情况下,方法的参数必须是对象;包装类提供了更强大的方法;方便了其他对象与基本数据类型的转换
内部类
静态内部类(static 修饰类的话只能修饰内部类):静态内部类与非静态
内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向
创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的
创建。2. 它不能使用任何外围类的非 static 成员变量和方法
非静态内部类
外部类.this.成员变量
外部类.this.成员方法
成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问;
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
//第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
外部类访问内部类的私有成员
内部类相当于外部类的一个成员,和其它成员处于同一个级别,因此可以在内部类中直接访问外部类的各个成员(包括私有属性)。
需要注意的是在外部类中要想访问内部类的各个成员(这里也包括内部类的私有属性)就必须先实例化内部类,然后才能访问。对于为什么能访问内部类的私有属性,是因为即使内部类的成员是私有的,但是也是在外部类中,和外部类的其它成员是平等的,只不过被内部类囊括是在内部中,因此访问都必须先实例化。
简述内部类及其作用
- 成员内部类:作为成员对象的内部类。可以访问private及以上外部类的属性和方法。外部类想要访问内部类属性或方法时,必须要创建一个内部类对象,然后通过该对象访问内部类的属性或方法。外部类也可访问private修饰的内部类属性。
- 局部内部类:存在于方法中的内部类。访问权限类似局部变量,只能访问外部类的final变量。
- 匿名内部类:只能使用一次,没有类名,只能访问外部类的final变量。
- 静态内部类:类似类的静态成员变量。
String和StringBuffer有什么区别?
String用于字符串操作,属于不可变类。String对象一旦被创建,其值将不能被改变。而StringBuffer是可变类,当对象创建后,仍然可以对其值进行修改。
说一说重写与重载的区别
重载发生在同一个类中,若多个方法之间方法名相同、参数列表不同,则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关,即重载的方法不能根据返回类型进行区分。
重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。还有,若父类方法的访问修饰符为private,则子类不能对其重写。
深拷贝和浅拷贝
浅拷贝
是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
特点:
-
如果属性是基本类型,拷贝的就是基本类型的值;除此之外还有 String 对象、Integer,Double。例如 String 类虽然是引用类型,但是是 final 类,同时也有字符串常量池的存在,因此看到的现象也是拷贝出了新的值。
-
如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。例如下图中,拷贝出的 stu2 与 stu1 中,其存在的引用类型还是指向同一个地址。
-
实现方式:实现对象拷贝的类,需要实现 Cloneable 接口,并覆写 clone()方法。
深拷贝
在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。
特点:
-
对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
-
对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。例如下图中拷贝出的 stu2 与stu1 中存在的引用类型指向的是不同的区域。
-
对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法, 进而实现了对象的串行层层拷贝。
- 深拷贝相比于浅拷贝速度较慢并且花销较大。
深拷贝实现方式
①让每个引用类型属性内部都重写 clone() 方法:既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。
②利用序列化:序列化这个对象,再反序列化回来。每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为transient,即将其排除在克隆属性之外。因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都能实现深拷贝.
【Java深入】深拷贝与浅拷贝详解_白夜行515的博客-CSDN博客
静态代码块/非静态代码块什么时候执行
类加载的时候执行并且只执行一次
非静态代码块随着对象的加载而加载
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
Java面试题⭐多线程篇⭐(万字总结,带答案,面试官问烂,跳槽必备,建议收藏)
个人主页: Java李小立
后面会持续更新java面试专栏,请持续关注
如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连❤️❤️❤️)
面试宝典列表(持续更新):
序号 | 内容 | 链接地址 |
---|---|---|
1 | Java基础篇 | (点击跳转)java面试宝典-基础篇 |
2 | Java集合框架篇 | (点击跳转)java面试宝典-集合框架篇 |
3 | Java多线程篇 | (点击跳转)java面试宝典- 多线程篇 |
4 | JVM篇 | 待分享 |
5 | Spring篇 | 待分享 |
6 | Mybatis篇 | 待分享 |
7 | SpringcCloud篇 | 待分享 |
8 | Redis篇 | 待分享 |
9 | Mysql篇 | 待分享 |
10 | dubbo篇 | 待分享 |
11 | zookeeper篇 | 待分享 |
12 | kafka篇 | 待分享 |
13 | RocketMq篇 | 待分享 |
14 | Nacos篇 | 待分享 |
Java中实现多线程有几种方法
创建线程的常用四种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口( JDK1.5>= )
- 线程池方式创建
采用继承Thread类的方式创建线程的优缺点
Thread代码解析:Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法(单独执行run方法不会创建新线程,只会在当前线程执行run方法)。
public class MyThread extends Thread
public void run()
System.out.println("MyThread.run()");
MyThread myThread1 = new MyThread();
myThread1.start();
优点:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用
this即可获取当前线程
缺点:因为线程类已经继承了Thread类,Java语言是单继承的,所以就不能再继承其他父类了。
采用实现Runnable、Callable接口的方式创建线程的优缺点
Runnable代码解析 :通常情况下我们自己不会使用Thread类,因为如果已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口。
public class MyThread extends OtherClass implements Runnable
public void run()
System.out.println("MyThread.run()");
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
callable代码解析:有返回值的任务必须实现 Callable 接口执行,Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务
返回的 Object,再结合线程池接口 ExecutorService 就可以实现传多线程返回结果。
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++)
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list)
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
优点:线程类只是实现了Runnable或者Callable接口,还可以继承其他类,Callable接口里定义方法返回值,可以声明抛出异常。
缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用 Thread.currentThread() 方法
实际开发中一般都会使用线程池线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
线程池的方式代码实例(固定大小线程池)
// 创建线程池(定长线程池)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true)
threadPool.execute(new Runnable() // 提交多个线程任务,并执行
@Override
public void run()
System.out.println(Thread.currentThread().getName() + " is running ..");
try
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();
);
4 种线程池
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
其实四种线程池都是 ThreadPoolExecutor ,只是创建参数不同
-
newSingleThreadExecutor: 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
-
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
-
newCachedThreadPool: 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
-
newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
如何停止一个正在运行的线程
- run()方法执行完,线程就会正常结束。
- Interrupt 方法结束线程。
- stop 方法终止线程(线程不安全)
线程的生命周期,以及线程的状态。
状态
- 新建(New):新创建了一个线程对象。
- 就绪(Runnable)::线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于
可运行线程池中,变得可运行,等待获取CPU的使用权。 - 运行态(Running):就绪状态的线程获取了CPU,执行程序代码。
4.** 阻塞态(Blocked)**:有三种情况,阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进
入就绪状态,才有机会转到运行状态。 - 死亡(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期
阻塞的三种情况
- 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待
池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤
醒,wait是object类的方法 - 同步阻塞:synchronized 运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放
入“锁池”中。 - 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状
态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
sleep是Thread类的方法
sleep、wait、join、yield的区别
1.锁池:所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被一个线程拿到,其他线程就需要进入锁池等待,等锁释放后去竞争锁,某线程得到锁后会进入就绪态,等待cpu分配资源。
2.等待池
当我们调用wait()方法后,会释放锁池,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放
到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
sleep和wait的区别五点
1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
- sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu 的调度,获取到cpu资源后就可以继续运行了。而如果sleep时该线程有锁,那么sleep不会释放这个锁,而 是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程 序。如果在睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出 interruptexception异常返回,这点和wait是一样的。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
join和yield方法的区别
yield:执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,
所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。
join:执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队
列,直到线程A结束或中断线程
说说线程安全的理解
应该是可以说线程安全是内存的数据安全,堆是共享内存,可以被所有线程访问
- 当多个线程访问一个对象时进行操作,如果得到正确的结果(和单线程一致),我们就说这个对象是线程安全的。
堆是所有线程共享的一块内存区域,堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
栈是每个线程独有的,,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈
互相独立,因此,栈是线程安全的。
线程安全
进程内存空间独立,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以
访问到该区域,这就是造成问题的潜在原因。
说说守护线程理解
守护线程:为所有非守护线程提供服务的线程;并不是某个,而是所有线程的守护线程。
举例: GC垃圾回收线程:就是一个经典的守护线程,所以当所有线程结束垃圾回收线程是JVM上仅剩的线程时,垃圾回收线
程会自动离开。
应用场景:
(1)来为其它线程提供服务支持的情况;
(2) 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;
Java中的线程池比如executeService会自动将守护线程转化为用户线程。
引用类型有哪些?有什么区别?
引用类型主要分为强软弱虚四种:
- 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
- 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
- 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
- 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。
threadLocal的原理以及使用场景
-
ThreadLocal提供了线程内存储变量的能力。通过get和set方法就可以得到当前线程对应的值,做到了线程之间互相隔离,相比于synchronized的做法是用空间(类似map储存)来换时间。
-
Thread 对象含有一个 ThreadLocalMap 类型的成员变量 它存储本线程中所
有ThreadLocal对象及其对应的值 -
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
-
ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 Entry 由 ThreadLocal 对象和 Object 构成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收 -
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
-
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
使用场景:
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种 隔离
threadLocal 内存泄露原因,如何避免
内存泄露:不再被使用的对或者变量占用的内存不能被回收,这就是内存泄露,最终会导致oom。
强引用:一般创建的对象,new对象,反射 newInstance,都是强引用,内存空间不足,抛出oom也不会回收强引用。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:weakRefeface,只要垃圾进行就回收,缓存用的比较多。
thread中有threadLocalMap,key为弱引用的threadLocal,线程value为变量
泄漏原因:由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长(线程池的线程不会被回收。同一个线程,可能执行了任务一,又执行任务2),如果都没有手动删除对应key,都会导致内存泄漏。但是使用弱引用可以多一层保障:弱引用ThreadLocal(Key值)不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()等方法的时候会被清除。
并发的三大特性
保证三大特性才能保证线程安全
原子性:不可分割的操作,多个步骤cpu同一时间执行。多线程i++,数据不安全i++为什么线程不安全点击查看,原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要
不都不执行。
可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行顺序和代码顺序一致。虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按
照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
synchronized关键字同时满足以上三种特性,但是volatile关键字不满足原子性。
volatile关键字
volatile(保证有序性,可见性):
new一个对象分为三步
- 申请内存
- 给内存赋值
- 内存地址赋值给栈空间。
**由于指令重排序 多线程中上下文切换等原因,可能执行1与3步骤还没有执行2就切换到其他线程 **
使用volatile
第一(可见性):使用volatile关键字会强制将修改的值立即写入主存
第二(有序性):volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。
什么是指令重排序
- 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
- 显然重排序对单线程运行是不会有任何问题(保证单线程最终执行结果一致),但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
重排序遵守的规则
需要了解as-if-serial与happens-before
大致可以总结为
- as-if-serial,单线程程序是按程序的顺序来执行的(可能发生指令重排序,但是程序员无感知)
- happens-before,正确同步的多线程程序是按执行顺序执行的(正确同步的操作需要程序员完成)
- as-if-serial语义和happens-before原则,都是为了在不改变程序执行结果的前提下,尽可能地提高程序的执行效率
为什么使用线程池
1、降低资源消耗;创建线程是很消耗资源,提高线程利用率,降低创建和销毁线程的消耗。
2、提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
3、提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
核心参数:
-
corePoolSize: 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会
消除。 -
maxinumPoolSize:代表的是最大线程数,比如当前任务较多,将核心线程数都用完了,此时就会创建新的线程,但
是线程池内线程总数不会超过最大线程数 -
keepAliveTime:
unit单位当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
次时间内会被销毁。 -
unit:keepAliveTime 的单位。
-
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
-
workQueue: 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
Handler:
主要有4种拒绝策略:
- 默认:ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
Java线程池中队列常用类型有哪些?
- ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue 。
- SynchronousQueue 一个不存储元素的阻塞队列。
- PriorityBlockingQueue 一个具有优先级的无限阻塞队列。 PriorityBlockingQueue 也是基于最小二叉堆实现
- DelayQueue
- 只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
- 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
线程池执行流程
线程池中阻塞列队的作用,为什么先放列队,后创建最大线程。
-
普通列队超出列队长度,无法保任务等待,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。使得线程进入wait状态,释放cpu资源,列队空闲notify唤醒。
-
在创建新线程的时候,是要获取全局锁的,这个时候所有线程会阻塞,影响了整体效率。
线程复用原理
其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。
提交任务线程池已满
- 如果设置的无界队列(LinkedBlockingQueue默认大小是Integer.MAX_VALUE):可以继续提交。
- 如果是有界队列,则会判断是否达到最大线程数,如果打到则采用拒绝策略
fixedThreadPoll阻塞队列是什么
fixedThreadPool是固定长度的线程池,底层用的linkedBlockingQueue,无界阻塞队列,长度是Integer的最大值,无限放入可能造成机器内存溢出(阿里巴巴规范禁用fixedThreadPoll)。
Java死锁如何避免
- 互斥条件:一个资源每次只能被一个线程使用
- 请求和保持条件:一个线程在等待阻塞某个资源时,不释放已经占有的资源
- 不剥夺条件:一个线程获得的资源,在使用完成前,不能强行剥夺,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件:若干线程形成头尾相接的等待循环关系。
在开发过程中:
- 注意加锁顺序
- 设置超时的时间reentrantLoack
synchronized和reentrantLock的区别
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如
果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
- synchronized是Java中的关键字,reentrantLock是一个类。
- synchronized会自动的加锁释锁,但是reentrantLock需要程序员手动的加锁与释放锁(lock unlock)。
- synchronized是jvm层面的锁(native方法C++实现),reentrantLock是api层面的锁(可以看到Java代码 Lock接口)
- synchronized是非公平锁,reentrantLock构造函数选择是否公平
- synchronized是锁的对象,锁信息保存在对象头部,reentrantLock(aqs)是锁的线程,。
- synchronized涉及到锁升级的过程。
synchronized锁的类型
在了解锁类型前受限了解一下JVM内存中对象的数据结构
JVM对象的储存
- 对象
- 对象头:
- 对象自身的运行数据Mark Word(标记字):
- 对象的hash码(identity_hashcode),分代年龄(age)
- 指向锁记录的指针(biased_lock)
- 指向重量级锁的指针
- 偏向锁的ID(thread),时间戳(epoch)
- 类型指针:
- 数组长度:
- 对象自身的运行数据Mark Word(标记字):
- 实例数据: 对象真正存储的有效信息就是放在这里的,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
- 对其补充: 对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
- 对象头:
- Java的锁就是在对象的markword记录状态 :无锁、偏向锁,轻量级锁,重量级锁对应不同的状态编码
- Java的锁竞争机制就是根据锁竞争程度不断升级的过程。
- 偏向锁:(还没有上锁) 在锁的对象的对象头部记录当前线程获取的Id,该线程下次如果又来获取锁直接可以获取到,因为大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁。
- 轻量级锁(自旋锁cpu的调用):由偏向锁升级而来,偏向锁时,如果有第二个线程来竞争锁,偏向锁就会升级为轻量锁,底层采用cas自旋实现(等待偏向锁ID释放),并不会阻塞,因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。。
- 如果自旋的线程过多,任然没有获取锁,就会升级重量级锁(操作系统层面的调用),会导致线程索塞。
- 自旋锁:不需要阻塞,和唤醒线程,这个是操作系统底层完成,比较耗时间,cas获得一个预期值,如果没有获取则自旋获取,如果获取到了则获得锁,线程还是在运行,不会锁线程,相对比较轻量。
对AQS的理解,AQS如何实现可重入锁
- AQS是Java线程同步安全机制的框架,是JDK锁工具的实现。extends abstractQueuedSynchronzied
2.(线程组成双向链表排队) 每个节点都有个头属性,指向上一个节点,尾节点指向下一个属性,也有个通过volatile int state (信号量 类似红绿灯) 控制线程排队或者放行。 - 在可重入锁的场景下,state用来表示加锁的次数,0无锁,没加一次锁就+1,释放锁-1.
countDownLatch cylicbarrier semaphore常用简介
- countDownLatch :模拟高并发,排队,所有的线程等待,到达某个条件执行,同时被唤醒,countDownLatch(1) 每个线程调用await方法,countDown 1变成0 同时完成
- cylicbarrier: 栅栏,等待其他多个线程完成某件事情之后才能执行,可循环使用的屏障,它要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,excel(多个页)
- semaphore: 给排队的线程加一个权重,可对方法限流。
版权声明:本文为博主原创文章,未经博主允许不得转载
https://blog.csdn.net/qq_44614710/article/details/120308144
以上是关于Java面试题总结 | Java基础部分(持续更新)的主要内容,如果未能解决你的问题,请参考以下文章
Java面试题⭐多线程篇⭐(万字总结,带答案,面试官问烂,跳槽必备,建议收藏)
Java面试题⭐多线程篇⭐(万字总结,带答案,面试官问烂,跳槽必备,建议收藏)