现在关于Java面试的资料是层出不穷,对于选择困难症的同学来说,无疑是陷入了一次次的抉择与不安中,担心错过了关键内容,现在小曾哥秉持着"融百家之所长,汇精辟之文档"的思想,整理一下目前主流的一些八股文,以达到1+1 > 2 的效果!
面向对象是一种基于面向过程的编程思想,是向现实世界模型的自然延伸,这是一种”万物皆对象”的编程思想。由执行者变为指挥者,在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动。
区别:
(1)编程思路不同:面向过程以实现功能的函数开发为主,用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,而面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
(2)封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
(3)面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显
面向对象是以功能来划分问题,而不是步骤
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
注意事项
编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;
跨平台的是Java程序、而不是JVM,JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM。机器码和字节码的区别:
机器码,完全依附硬件而存在~并且不同硬件由于内嵌指令集不同,即使相同的0 1代码 意思也可能是不同的
我们知道JAVA是跨平台的,为什么呢?因为他有一个jvm,不论那种硬件,只要你装有jvm,那么他就认识这个JAVA字节码~~~~至于底层的机器码,咱不用管,有jvm搞定,他会把字节码再翻译成所在机器认识的机器码~~
Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:
在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:
java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围):
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写:就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。
“两同两小一大”:
“两同”即方法名相同、形参列表相同;
“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。
形参和实参
实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。【say(hello)】
形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。[say(String str)]
基本类型和引用类型
特点:
1、基本的数据类型,传递的是值(值拷贝),形参的任何改变不影响实参
2、引用数据类型(数组):引用类型传递的是地址(传递也是值,但是值是地址),可以通过形参影响实参!
基本数据类型
public static void main(String[] args) {
int a = 10;
int b = 20;
//创建AA 对象名字obj
AA obj = new AA();
obj.swap(a, b); //调用swap
System.out.println("main 方法a=" + a + " b=" + b);//a=10 b=20
}
class AA {
public void swap(int a,int b){
System.out.println("\na 和b 交换前的值\na=" + a + "\tb=" + b);//a=10 b=20
//完成了a 和b 的交换
int tmp = a;
a = b;
b = tmp;
System.out.println("\na 和b 交换后的值\na=" + a + "\tb=" + b);//a=20 b=10
}
}
输出结果:
a 和b 交换前的值 a=10 b=20
a 和b 交换后的值 a=20 b=10
main 方法 a=10 b=20
也充分证明了在基本数据类型中,形参的任何改变不会影响实参值
引用数据类型
public class bb {
public static void main(String[] args) {
//测试数组
B b = new B();
int[] arr = {1, 2, 3};
b.test100(arr);//调用方法
System.out.println(" main 的arr 数组");
//遍历数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
// 测试对象
Person p = new Person();
p.name = "jack";
p.age = 10;
b.test200(p);
System.out.println("main 的p.age=" + p.age);
}
}
class B {
// 数组
public void test100(int[] arr) {
arr[0] = 200;//修改元素
//遍历数组
System.out.println(" test100 的arr 数组");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
}
// 对象
public void test200(Person p) {
p.age = 1000; //修改对象属性
// 特例1
//p = null;
// 特例2
//p = new Person();
//p.name = "tom";
//p.age = 99;
}
}
输出结果:
test100的arr数组:[200,2,3]
main的arr数组:[200,2,3]
main的p.age=1000
分别从引用类型(数组和对象)的角度来举例,可以发现在引用数据类型中传递的是地址,可以通过形参影响实参!
如果大家对上述例子有所了解,下面再添加几个特例
特例1
p = null;
特例2
p = new Person();
p.name = "tom";
p.age = 99;
System.out.println("main 的p.age=" + p.age);
如果test200 执行的是p = null ,下面的结果是10
如果test200 执行的是p = new Person();…, 下面输出的是10
为什么会这样呢?
p = null 代表指向p的地址已经断开,不影响p对象的值
p = new Person() 代表指向了一个新的地址,无论怎么传递值,都不会影响p对象的值。
具体可以查看Java基础补充–查漏补缺(二)
Java语言支持的8种基本数据类型是: byte short int long float double boolean char
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
以Integer对象为例子:
Integer.parseInt(“”);是将字符串类型转换为int的基础数据类型
Integer.valueOf(“”)是将字符串类型数据转换为Integer对象
Integer.intValue();是将Integer对象中的数据取出,返回一个基础数据类型int
基本类型和包装类型的区别?
int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型,然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。
当新对象被创建的时候,构造方法会被调用。每一个类都有构造方法。在程序员没有给类提供构造方法的情况下,Java编译器会为这个类创建一个默认的构造方法。
构造方法特点如下:
构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名;Java中构造方法重载和方法重载很相似。可以为一个类创建多个构造方法。每一个构造方法必须有它自己唯一的参数列表。
Java中类不支持多继承,只支持单继承(即一个类只有一个父类)。 但是java中的接口支持多继承,即一个子接口可以有多个父接口。(接口的作用是用来扩展对象的功能,一个子接口继承多个父接口,说明子接口扩展了多个功能,当类实现接口时,类就扩展了相应的功能)。
clone方法:用于创建并返回当前对象的一份拷贝;
getClass方法:用于返回当前运行时对象的Class;
toString方法:返回对象的字符串表示形式; .
finalize方法:实例被垃圾回收器回收时触发的方法;
equals方法:用于比较两个对象的内存地址是否相等,一般需要重写;
hashCode方法:用于返回对象的哈希值;
notify方法:唤醒一个在此对象监视器上等待的线程。如果有多个线程在等待只会唤醒一一个。
notifyAll方法:作用跟notify() 一样,只不过会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
wait方法:让当前对象等待;
hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:
为什么要重写hashCode()和equals()?
Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。
类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
==和equals()有什么区别?
对于基本数据类型来说,== 比较的是值。
对于引用数据类型来说,== 比较的是对象的内存地址。
主要从可变性、线程安全性、性能三方面进行考虑:
String、StringBuffer 和StringBuilder 的选择
操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
1.抽象类可以有构造方法,接口中不能有构造方法。
2.抽象类中可以有普通成员变量,接口中没有普通成员变量
3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
4.抽象类中的抽象方法的访问类型可以是public,protected、但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
5.抽象类中可以包含静态方法,接口中不能包含静态方法;抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。
7.一个类可以实现多个接口,但只能继承一个抽象类。
接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:
RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
try {
System.out.println("Try to do something");
throw new RuntimeException("RuntimeException");
} catch (Exception e) {
System.out.println("Catch Exception -> " + e.getMessage());
} finally {
System.out.println("Finally");
}
输出:
Try to do something
Catch Exception -> RuntimeException
Finally
1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return语句执行之后,返回之前执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前就已经确定了;
4、finally中如果包含return,那么程序将在这里返回,而不是try或catch中的return返回,返回值就不是try或catch中保存的返回值了。
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList() 这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
优点:1,类型安全、2,消除强制类型转换、3,潜在的性能收益。
注意:泛型只是提高了数据传输安全性,并没有改变程序运行的性能
类型擦除:泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如则会被转译成普通的Object类型,如果指定了上限如< T extends String >则类型参数就被替换成类型上限。
List<String> list = new ArrayList<String>()
、两个 String 其实只有第⼀个起作⽤,后⾯⼀个没什么卵⽤,只不过 JDK7 才开始⽀持 Listlist = new ArrayList<> 这种写法。
2、第⼀个 String 就是告诉编译器,List 中存储的是 String 对象,也就是起类型检查的作⽤,之后编译器会擦除泛型占位符,以保证兼容以前的代码。
每个类都有一个Class对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的.class文件,该文件内容保存着Class对象。类加载相当于Class对象的加载,类在第一次使用时才动态加载到JVM中。也可以使用Class.forName(“com.mysql.jdbc.Driver”)这种方式来控制类的加载,该方法会返回一个Class对象。
具体来说,通过反射机制,我们可以实现如下的操作:
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型是下面这样的,序列化协议属于哪一层呢?
因为,OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。
使用ArrayList()创建ArrayList对象时,不会定义底层数组的长度,当第一次调用add(E e) 方法时,初始化定义底层数组的长度为10,之后调用add(E e)时,如果需要扩容,则调用grow(int minCapacity) 进行扩容,长度为原来的1.5倍。
HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
JDK1.7之前:数组 + 链表
现在JDK1.8之后:数组+链表+红黑树进行数据的存储,当链表上的元素个数超过 8 个并且数组⻓度 >= 64 时⾃动转化成红⿊树,节点变成树节点,以提⾼搜索效率和插⼊效率到 O(logN)。
红黑树特点:
1、每个节点或者是黑色,或者是红色。
2、根节点是黑色。
3、每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
4、如果一个节点是红色的,则它的子节点必须是黑色的。
5、从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]包含n个内部节点的红黑树的高度是 O(log(n)).
优点:红黑树是一种平衡树,他复杂的定义和规则都是为了保证树的平衡性。如果树不保证他的平衡性就是下图:很显然这就变成一个链表了。
1、如果用B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面。这个时候遍历效率就退化成了链表
2、B和B+树主要用于数据存储在磁盘上的场景,比如数据库索引就是用B+树实现的;而红黑树多用于内存中排序,也就是内部排序。
通过 key 的 hash 值找到在 table 数组中的索引处的 Entry,然后返回该 key 对应的 value 即可。
在这⾥能够根据 key 快速的取到 value 除了和 HashMap 的数据结构密不可分外,还和 Entry 有莫大的关系。HashMap 在存储过程中并没有将 key,value 分开来存储,⽽是当做⼀个整体 key-value 来处理的,这个整体就是Entry 对象。同时 value 也只相当于 key 的附属⽽已。在存储的过程中,系统根据 key 的 HashCode 来决定 Entry 在 table 数组中的存储位置,在取的过程中同样根据 key 的 HashCode 取出相对应的 Entry 对象(value 就包含在
⾥⾯)
有两种情况会调用resize 方法:
1.第一次调用HashMap的put方法时,会调用resize方法对table数组进行初始化,如果不传入指定值,默认大小为16。
2.扩容时会调用resize,即size > threshold时,table 数组大小翻倍。
为了快速运算出键值对存储的索引和让键值对均匀分布
首先计算键值对的索引要满足两个要求:不能越界、均匀分布;而 h % length (h根据key计算出来的哈希值)就能满足这一点,但是取模运算速度较慢。
容量为2的次幂时、而 h & (length-1)刚好也能满足,而且按位与运算速度很快。
HashMap 与 ConcurrentHashMap的区别是:HashMap不是线程安全的,ConcurrentHashMap是线程安全的。
HashTable 和 ConcurrentHashMap 相⽐,效率低。Hashtable 之所以效率低主要是使⽤了 synchronized 关键字对 put 等操作进⾏加锁,而 synchronized 关键字加锁是对整张 Hash 表的,即每次锁住整张表让线程独占,致使效率低下;ConcurrentHashMap 在对象中保存了⼀个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个Segment元素,即每个分段则类似于⼀个Hashtable;在执行 put 操作时⾸先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可,因此,ConcurrentHashMap 在多线程并发编程中可是实现多线程 put操作。
迭代器是⼀种设计模式,它是⼀个对象,它可以遍历并选择序列中的对象,⽽开发⼈员不需要了解该序列的底层结构。迭代器通常被称为“轻量级”对象,因为创建它的代价⼩。Java 中的 Iterator 功能⽐较简单,并且只能单向移动:
与 Enumeration 相⽐,Iterator 更加安全,因为当⼀个集合正在被遍历的时候,它会阻⽌其它线程去修改集合。否则会抛出 ConcurrentModificationException 异常。
快速失败机制是java集合的一种错误检测机制,当迭代集合时集合的结构发生改变,就会产生fail-fast机制。
一旦发现遍历的同时,其他人来修改,就立刻抛出异常。fail_salf:当遍历的时候,其他人来修改,应当有相应的策略,例如牺牲一致性来遍历整个数组。
I/O(Input/Outpu) 即输入/输出 。
输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ); 用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
常见的 IO 模型?
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。
Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
总结:简单总结一下 Java 中的 BIO、NIO、AIO
IO(Input Output)用于实现对数据的输入与输出操作,Java把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。
黑色字体的是抽象基类,其他所有的类都继承自它们。红色字体的是节点流,蓝色字体的是处理流。
装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能。
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, FilterInputStream (对应输入流)和FilterOutputStream(对应输出流)是装饰器模式的核心,分别用于增强 InputStream 和OutputStream子类对象的功能。
我们常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。
装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口
适配器(Adapter Pattern)模式主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。
适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
装饰器模式 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。
适配器模式 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。
适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
进程:是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
线程:是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
区别: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
程序计数器为什么是私有的?
1、字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
2、在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
发生以下情况,将会进入阻塞状态
线程调用sleep()方法主动放弃所占用的处理器资源。
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
线程在等待某个通知(notify)。
程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。解除阻塞:
调用sleep()方法的线程经过了指定时间。
线程调用的阻塞式IO方法已经返回。
线程成功地获得了试图取得的同步监视器。
线程正在等待某个通知时,其他线程发出了一个通知。
处于挂起状态的线程被调用了resume()恢复方法。
线程死锁是指由于两个或多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
如何预防死锁? 破坏死锁的产生的必要条件即可:
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
共同点 :两者都可以暂停线程的执行。
区别 :
为什么 wait() 方法不定义在 Thread 中?
wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
线程安全在三个方面体现:
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
Thread类中有个变量threadLocals,它的类型为ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,就是一个普通的Java类,但是实现的类似map的功能。每个线程都有自己的一个map,map是一个数组的数据结构存储数据,每个元素是一个Entry,entry的key是ThreadLocal的引用,也就是当前变量的副本,value就是set的值。
原文链接:https://blog.csdn.net/fengxi_tan/article/details/106629280
原文链接:https://blog.csdn.net/qq_37258531/article/details/122350750
guide哥:https://javaguide.cn/database/mysql/mysql-questions-01.html
牛客网:https://www.nowcoder.com/tutorial/94/e07fdcfc369c49e8a95ea23de51d58b5
帅地玩编程-- Java面试必知必会