JAVA面试

文章目录

1、JRE or JDK

JRE: Java Runtime Environment,Java 运行环境的简称,为 Java 的运行提供了所需的环境。它是一个 JVM 程序,主要包括了 JVM 的标准实现和一些 Java 基本类库。
JDK: Java Development Kit,Java 开发工具包,提供了 Java 的开发及运行环境。JDK 是 Java 开发的核心,集成了 JRE 以及一些其它的工具,比如编译 Java 源码的编译器 javac 等。

2、面向对象的三大特征以及简单解释

封装是将类的某些信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来对隐藏的信息进行操作和访问。
好处:
1.只能通过规定的方法访问数据
2.隐藏类的实现细节,方便修改和实现
封装的实现步骤:
1.修改属性的可见性为private
2.创建get/set方法(用于属性的读写)
3.在get/set方法中加入属性控制语句

继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性的继承父类。必须继承父类的全部特征和行为,
java只支持单继承,但是支持多层继承。
语法:子类 extends 父类
好处:
1:提高了代码的复用性。
2:让类与类之间产生了关系,提供了另一个特征多态的前提。
在java中,即使没有声明父类,也有一个隐含的父类,就是Object类
在子类中可以使用super来调用父类的方法

多态指同一个行为具有多个不同表现形式或形态的能力。
多态的好处: 提高了程序的扩展性。继承的父类或接口一般是类库中的东西,(如果要修改某个方法的具体实现方式)只有通过子类去覆写要改变的某一个方法,这样在通过将父类的应用指向子类的实例去调用覆写过的方法就行了!
多态的弊端: 当父类引用指向子类对象时,虽然提高了扩展性,但是只能访问父类中具备的方法,不可以访问子类中特有的方法。(前期不能使用后期产生的功能,即访问的局限性)
实现多态的三个条件(前提条件,向上转型、向下转型)
1、继承的存在;(继承是多态的基础,没有继承就没有多态)
2、子类重写父类的方法。(多态下会调用子类重写后的方法)
3、父类引用变量指向子类对象。(涉及子类到父类的类型转换)

继承

1:成员变量。
当子父类中出现一样的属性时,子类类型的对象,调用该属性,值是子类的属性值。
如果想要调用父类中的属性值,需要使用一个关键字:super
注意:子父类中通常是不会出现同名成员变量的,因为父类中只要定义了,子类就不用在定义了,直接继承过来用就可以了。
2:成员方法。
当子父类中出现了一模一样的方法时,建立子类对象会运行子类中的方法。好像父类中的方法被覆盖掉一样。所以这种情况,是方法的另一个特性:覆盖(复写,重写)
3:构造方法。
子类中所有的构造方法都会默认访问父类中的空参数的构造方法,因为每一个子类构造内第一行都有默认的语句 super();
如果父类中没有空参数的构造方法,那么子类的构造方法内,必须通过 super 语句指定要访问的父类中的构造方法。
如果子类构造方法中用 this 来指定调用子类自己的构造方法,那么被调用的构造方法也一样会访问父类中的构造方法。

3、数据类型

基本类型
byte/8
char/16
short/16
int/32
float/32
long/64
double/64
boolean/~

引用类型
数组、类、接口

包装类型

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

4、String

概览

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)

在 Java 8 中,String 内部使用 char 数组存储数据。
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

不可变的好处

  1. 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

  1. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

  1. 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

  1. 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

String, StringBuffer and StringBuilder

1.可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2.线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

优先考虑使用StringBuilder,因为它的效率更高
多线程同时访同同一个资源才需要虑线程安全问题
jvm 为每个线程创建一个栈,用于存放该线程执行的方法信息。StringBuilder一般都写在方法中,每个线程独享一份StringBuilder

5、关键字

final

  1. 数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。

  • 对于基本类型,final 使数值不变;
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
  1. 方法

声明方法不能被子类重写。

private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

声明类不允许被继承。

final和abstract关键字的作用

final和abstract是功能相反的两个关键字,可以对比记忆

abstract可以用来修饰类和方法,不能用来修饰属性和构造方法;使用abstract修饰的类是抽象类,需要被继承,使用abstract修饰的方法是抽象方法,需要子类被重写。

final可以用来修饰类、方法和属性,不能修饰构造方法。使用final修饰的类不能被继承,使用final修饰的方法不能被重写,使用final修饰的变量的值不能被修改,所以就成了常量。

特别注意:final修饰基本类型变量,其值不能改变,由原来的变量变为常量;但是final修饰引用类型变量,栈内存中的引用不能改变,但是所指向的堆内存中的对象的属性值仍旧可以改变。

static

static: 关键字,是一个修饰符,用于修饰成员(成员变量和成员方法)。
特点:
1,想要实现对象中的共性数据的对象共享。可以将这个数据进行静态修饰。
2,被静态修饰的成员,可以直接被类名所调用。也就是说,静态的成员多了一种调用方式。类
名.静态方式。
3,静态随着类的加载而加载。而且优先于对象存在。
弊端:
1,有些数据是对象特有的数据,是不可以被静态修饰的。因为那样的话,特有数据会变成对象的共享数据。这样对事物的描述就出了问题。所以,在定义静态时,必须要明确,这个数据是否
是被对象所共享的。
2,静态方法只能访问静态成员,不可以访问非静态成员。
(这句话是针对同一个类环境下的,比如说,一个类有多个成员(属性,方法,字段),静态方法A,那么可以访问同类名下其他静态成员,你如果访问非静态成员就不行)
因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。
3,静态方法中不能使用 this,super 关键字。
因为 this 代表对象,而静态在时,有可能没有对象,所以 this 无法使用。
4,主方法是静态的。

1.静态变量

  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
  1. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因此这两个关键字与具体对象关联。

  1. 静态语句块

静态语句块在类初始化时运行一次。

  1. 静态内部类

非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
静态内部类不能访问外部类的非静态的变量和方法。

  1. 静态导包

在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。

  1. 初始化顺序

静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。

存在继承的情况下,初始化顺序为:

父类(静态变量、静态语句块)
子类(静态变量、静态语句块)
父类(实例变量、普通语句块)
父类(构造函数)
子类(实例变量、普通语句块)
子类(构造函数)

this

this:代表对象。就是所在方法所属对象的引用。
哪个对象调用了 this 所在的方法,this 就代表哪个对象,就是哪个对象
的引用。

super

访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super() 函数。
访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。

6、Object 通用方法

概览

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native Class<?> getClass()

protected void finalize() throws Throwable {}

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

==和equals的区别和联系

“==”是关系运算符,equals()是方法,同时他们的结果都返回布尔值;

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。

“==”使用情况如下:
a) 基本类型,比较的是值
b) 引用类型,比较的是地址
c) 不能比较没有父子关系的两个对象

equals()方法使用如下:
a) 系统类一般已经覆盖了equals(),比较的是内容。
b) 用户自定义类如果没有覆盖equals(),将调用父类的equals(比如是Object),而Object的equals的比较是地址
(return (this == obj);)
c) 用户自定义类需要覆盖父类的equals()

注意:Object的==和equals比较的都是地址,作用相同

hashCode()

hashCode() 返回哈希值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。

在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象哈希值也相等。

HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。

toString()

默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。

clone()

1. cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。

2. 浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象。

3. 深拷贝

拷贝对象和原始对象的引用类型引用不同对象。

4. clone() 的替代方案

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

7、抽象类与接口

1. 抽象类

抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。abstract class类中的方法不必是抽象的。abstract class类中定义抽象方法必须在具体(Concrete)子类中实现,所以,不能有抽象构造方法或抽象静态方法。如果子类没有实现抽象父类中的所有抽象方法,那么子类也必须定义为abstract类型。

抽象类和普通类最大的区别是,抽象类不能被实例化,只能被继承。

2. 接口

接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的方法定义默认为public abstract类型,接口中的成员变量类型默认为public static final。
接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

下面比较一下两者的语法区别:

1.抽象类可以有构造方法,接口中不能有构造方法。

2.抽象类中可以有普通成员变量,接口中没有普通成员变量

3.抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

4.抽象类中的抽象方法的访问类型可以是public,protected和(默认类型,虽然
eclipse下不报错,但应该也不行),但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。

5.抽象类中可以包含静态方法,接口中不能包含静态方法

6.抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

7.一个类可以实现多个接口,但只能继承一个抽象类。

下面接着再说说两者在应用上的区别:

接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用,例如,模板方法设计模式是抽象类的一个典型应用,假设某个项目的所有Servlet类都要用相同的方式进行权限判断、记录访问日志和处理异常,那么就可以定义一个抽象的基类,让所有的Servlet都继承这个抽象基类,在抽象基类的service方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中只是完成各自的业务逻辑代码,

普通类和抽象类有哪些区别?

普通类不能包含抽象方法,抽象类可以包含抽象方法。抽象类不能直接实例化,普通类可以直接实例化。

匿名内部类(对象):

没有名字的内部类。就是内部类的简化形式。一般只用一次就可以用这种形
式。匿名内部类其实就是一个匿名子类对象。想要定义匿名内部类:需要前提,内部类必须继承
一个类或者实现接口。

8、重写和重载

重写:发生在父类子类之间的,方法名相同,参数列表相同
重载:发生在一个类里面,方法名相同,参数列表不同(混淆点:跟返回类型没关系)
重写发生在运行期,重载发生在编译期
重写的注意事项:访问修饰符范围不能变小,异常不能更广

9、值传递、引用传递的区别

1)值传递 : 将原变量内容复制下来,再用一个新的内存空间来保存,两个变量之间相互独立; 函数范围内对值的任何改变在函数外部都会被忽略
2)引用传递 : 给当前变量起了一个别名,实际上这两个变量引用的是一个值。函数范围内对值的任何改变在函数外部也能反映出这些修改, 它们指向同一内存空间.
#4. jdk、jvm、jre的区别和联系,哪个提供垃圾回收
1)JVM是运⾏ Java 字节码的虚拟机,针对不同系统的特定实现
2)JDK是Java开发工具包,能够创建和编译程序,它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。
3)JRE 是 Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 JVM,Java 类库,java 命令和其他的⼀些基础构件。但是,它不能⽤于创建新程序

10、容器

容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

Collection

在这里插入图片描述

1.List

List的元素以线性方式存储,可以存放重复对象,List主要有以下两个实现类:

  • ArrayList: 基于动态数组实现,支持随机访问。
  • LinkedList: 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。
  • Vector: 和 ArrayList 类似,但它是线程安全的。

2. Set

Set中的对象不按特定(HashCode)的方式排序,并且没有重复对象,Set主要有以下两个实现类:

  • TreeSet: 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
  • HashSet: 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历HashSet 得到的结果是不确定的。
  • LinkedHashSet: 具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。

3. Queue

  • LinkedList: 可以用它来实现双向队列。
  • PriorityQueue: 基于堆结构实现,可以用它来实现优先队列。

HashSet

HashSet集合特点:
1)底层数据结构是哈希表。
2)对集合的迭代顺序不作任何保证,也就是说不保证存储和取出的元素顺序一致。
3)没有带索引的方法,所以不能使用普通for循环遍历。
4)由于是Set集合,所以是不包含重复元素的集合。

HashSet集合保证元素唯一性
HashSet集合添加一个元素的过程:
在这里插入图片描述
HashSet集合存储元素:要保证元素唯一性,需要重写hashCode()和equals()。

TreeSet集合概述和特点

TreeSet集合特点:

1)元素有序,这里的顺序不是指存储和取出的顺序,而是按照一定的规则进行排序,具体排序方式取决于构造方法。TreeSet​():根据其元素的自然排序进行排序。TreeSet​(Comparator comparator) :根据指定的比较器进行排序。

2)没有带索引的方法,所以不能使用普通for循环遍历

3)由于是Set集合,所以不包含重复元素的集合。

LinkedHashSet集合概述和特点

LinkedHashSet集合特点:

1)哈希表和链表实现的Set接口,具有可预测的迭代次序

2)由链表保证元素有序,也就是说元素的存储和取出顺序是一致的

3)由哈希表保证元素唯一,也就是说没有重复的元素

list和set的区别,有序性方面的区别

1)List 可以控制元素的插⼊位置,可以通过整数索引访问元素,并搜索列表中的元素。
2)List 允许重复的元素,Set不允许。
3)List 是有序集合⽽ Set是⽆序集合。

集合的有序无序是指在插入的时候,保持插入的顺序性,先插入的放在集合前面,后插入的放在集合后面。

如果要按照存和取的顺序来讲,ArrayList和LinkedList就属于有序集合,因为ArrayList底层是动态数组实现的,而数组是一块连续的空间,每次存的时候都是找到索引,一个接着一个的存储,取的时候也要按照索引遍历出来。
在这里插入图片描述

链表也是一样,不是存到链表头就是存到链表尾。因为存和取的顺序有序,模拟栈(先进后出)和队列(先进先出)这两种数据结构也很容易。但这两种结构它们本身并不能对元素进行排序,这也决定了我不能轻易的找到数组或链表中的最大值和最小值,或者说元素和元素之间存储的并没有什么规律。
在这里插入图片描述

同样,按照存储顺序来讲,HashSet依赖哈希存储,计算哈希值之后,会分散到不同的存储位置上,这也就代表存储的时候,元素不是一个挨着一个存储的,而是根据每个元素的hash值,散列到了不同的位置。存取的顺序也是不能保证的,元素的排序顺序也是不能保证的,但好处就是存取效率高。
在这里插入图片描述

而TreeSet依赖的是树存储,在树这种结构中,无论是二分查找树,还是红黑树,在存储元素的时候都会对元素本身进行比较,按照大小放到合适的位置,这也就说明,元素会按照树的性质去存储,那么也就无法保证存和取元素的顺序。但是元素可以在存储的时候根据自身的大小排好序,从而可以很轻易的找到最大值,最小值,以及给定一个元素,找到比他大和比他小元素等操作。
在这里插入图片描述

Map

Map是一种把键对象和值对象映射的集合,它的每一个元素都包含一个键对象和值对象。 Map主要有以下两个实现类:

  • TreeMap:基于红黑树实现。
  • HashMap:基于哈希表实现。数组+链表,jdk8之后引入红黑树,链表超过8,数组超过64,才用到红黑树。
  • HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable
    不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
  • LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。

hashMap和hashTable

1)线程安全的角度。HashTable线程安全,HashMap线程不安全(要保证线程安全的话就使⽤ ConcurrentHashMap)
2)效率角度。HashMap 要⽐ HashTable 效率⾼⼀点
3)对Null⽀持: HashMap 中,null 可以作为键,这样的键只有⼀个,可以有⼀个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有⼀个 null,直接抛出 NullPointerException
4)初始容量⼤⼩和每次扩充容量⼤⼩的不同。①创建时如果不指定容量初始值,Hashtable 默认的初始⼤⼩为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为2的幂次⽅⼤⼩
5)底层数据结构不同。JDK1.8 以后的 HashMap ,当链表⻓度⼤于阈值(默认为8)时转化为红⿊树。Hashtable 没有这样的机制

list、 set与map区别

在这里插入图片描述

在这里插入图片描述

11、Java并发

进程和线程

进程
进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程
其实就是进程中一个程序执行控制单元,一条执行路径。进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行过程中可以产生多个线程。与进程不同的是同类子多个线程进享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

进程:一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Java默认有几个线程? 2 个 mian、GC

Java 真的可以开启线程吗? 开不了

有三种使用线程的方法:

  • 继承 Thread 类
  • 实现Runnable接口
  • 实现 Callable 接口

继承 Thread 类,由子类复写 run 方法

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

步骤:
1,定义类继承 Thread 类;
2,目的是复写 run 方法,将要让线程运行的代码都存储到 run 方法中;
3,通过创建 Thread 类的子类对象,创建线程对象;
4,调用线程的 start 方法,开启线程,并执行 run 方法。

实现 Runnable 接口

需要实现接口中的 run() 方法。
使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。

Runnable 没有返回值、效率相比入 Callable 相对较低!

步骤:
1,定义类实现 Runnable 接口。
2,覆盖接口中的 run 方法(用于封装线程要运行的代码)。
3,通过 Thread 类创建线程对象;
4,将实现了 Runnable 接口的子类对象作为实际参数传递给 Thread 类中的构造方法。
为什么要传递呢?因为要让线程对象明确要运行的 run 方法所属的对象。
5,调用 Thread 对象的 start 方法。开启线程,并运行 Runnable 接口子类中的 run 方法。

start 与 run 区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,
    可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运
    行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运
    行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

为什么要有 Runnable 接口的出现?

因为实现 Runnable 接口可以避免单继承的局限性。

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

并发、并行

并发(多线程操作同一个资源)

  • CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替

并行(多个人一起行走)

  • CPU 多核 ,多个线程可以同时执行; 线程池

并发编程的本质:充分利用CPU的资源

如何判断一个线程是安全的?

1、多线程环境下
2、对这个对象的访问不需要加入额外的同步控制
3、操作的数据的结果依然是正确的

多线程安全问题的原因:

通过图解:发现一个线程在执行多条语句时,并运算同一个数据时,在执行过程中,其他线程参与进来,并操作了这个数据。导致到了错误数据的产生。

涉及到两个因素:
1,多个线程在操作共享数据。
2,有多条语句对共享数据进行运算。
原因:这多条语句,在某一个时刻被一个线程执行时,还没有执行完,就被其他线程执行了。

什么时候需要考虑线程安全?

1.多个线程访问同一个资源
2.资源是有状态的,比如字符串的拼接,这个时候数据是会有变化的

Synchronized 同步锁

同步锁
当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
Synchronized 作用范围

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen
    (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

解决安全问题synchronized:

java 中提供了一个解决方式:就是同步代码块。
格式:
==synchronized(对象) =={ // 任意对象都可以。这个对象就是锁。
需要被同步的代码;
}

同步:★★★★★

//就是在操作共享数据代码时,访问时只能让一个线程进去访问,此线程执行完退出后,别的线程才能再对此共享数据代码进行访问。
好处: 解决了线程安全问题。Synchronized
弊端: 相对降低性能,因为判断锁需要消耗资源,产生了死锁。
定义同步是有前提的:
1,必须要有两个或者两个以上的线程,才需要同步。
2,多个线程必须保证使用的是同一个锁。

同步的第二种表现形式:
//对共享资源的方法定义同步
同步方法: 其实就是将同步关键字定义在方法上,让方法具备了同步性。
==同步方法是用的哪个锁呢?
//synchronized(this)==用以定义需要进行同步的某一部分代码块通过验证,方法都有自己所属的对象 this,所以同步方法所使用的锁就是 this 锁。This.方法名

同步代码块和同步方法的区别?

同步代码块使用的锁可以是任意对象。
同步方法使用的锁是 this,静态同步方法的锁是该类的字节码文件对象

在一个类中只有一个同步的话,可以使用同步方法。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。

wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

notify()随机唤醒一个线程,notifyAll()唤醒所有线程

它们都属于 Object 的一部分,而不属于 Thread。

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

sleep 和 wait 的区别

Sleep是休眠线程,wait是等待
1、来自不同的类
wait是object的方法
sleep是thread的静态方法
2、关于锁的释放
wait 线程会释放执行权,而且线程会释放锁,可以指定时间也可以不指定时间。不指定时间,只能由对应的 notify 或者 notifyAll来唤醒。
Sleep线程会释放执行权,依旧持有锁,并在指定时间自动唤醒。
3、使用的范围是不同的
wait必须在同步代码块中
sleep 可以再任何地方
4、是否需要捕获异常
wait 不需要捕获异常
sleep 必须要捕获异常

线程的停止:

第一种方式:定义循环的结束标记。
第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过 Thread 类中的 interrupt 方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标
记,并结束。
---------< java.lang.Thread >----------
interrupt(): 中断线程。
setPriority(int newPriority): 更改线程的优先级。
getPriority(): 返回线程的优先级。
toString(): 返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
Thread.yield(): 暂停当前正在执行的线程对象,并执行其他线程。
setDaemon(true): 将该线程标记为守护线程或用户线程。将该线程标记为守护线程或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
join: 临时加入一个线程的时候可以使用 join 方法。
当 A 线程执行到了 B 线程的 join 方式。A 线程处于冻结状态,释放了执行权,B 开始执行。A
什么时候执行呢?只有当 B 线程运行结束后,A 才从冻结状态恢复运行状态执行。

LOCK 的出现替代了同步:

同步是隐示的锁操作,而 Lock 对象是显示的锁操作,它的出现就替代了同步。
在之前的版本中使用 Object 类中 wait、notify、notifyAll 的方式来完成的。那是因为同步中的锁是任意对象,所以操作锁的等待唤醒的方法都定义在 Object 类中。
而现在锁是指定对象 Lock。所以查找等待唤醒机制方式需要通过 Lock 接口来完成。而 Lock 接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是
Condition,将 Object 中的三个方法进行单独的封装。并提供了功能一致的方法 await()、signal()、signalAll()体现新版本对象的好处。

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

Lock 与 Synchronized 的区别

共同点: 两者都保持了并发场景下的原子性和可见性
不同点:
1、Synchronized 是内置的Java关键字, Lock 是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;
5、synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断(添加了类似锁投票、定时锁等候和可中断锁等候的一些特性)、可公平(两者皆可)
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!

公平锁与非公平锁的区别

公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平)
1.公平锁

公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。
锁的实现方式是基于如下几点:
表结点Node和状态state的volatile关键字。
sum.misc.Unsafe.compareAndSet的原子操作(见附录)。

2.非公平锁

在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。
ReentrantLock锁都不会使得线程中断,除非开发者自己设置了中断位。
ReentrantLock获取锁里面有看似自旋的代码,但是它不是自旋锁。
ReentrantLock公平与非公平锁都是属于排它锁。

线程状态和转换过程

线程有五种状态:
新建状态(new)
用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存.它会一直保持这个状态直到启动了start()方法.
就绪状态(Runnable)
当一个线程对象启动了start()方法后,该线程就处于就绪状态,
Java虚拟机会为它创建方法调用栈和程序计数器.处于这个状态的线程位于可运行池中,等待获得CPU的使用权.
运行状态(Running)
如果一个线程处于这个状态,那么它正在占用CPU,执行程序代码.只有处于就绪状态的线程才有机会转到运行状态.
阻塞状态(Blocked)
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行.当线程处于阻塞状态时,java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会=转到运行状态.
死亡状态(Dead)
当线程退出run()方法时,就进入死亡状态,该线程结束生命周期

线程的状态转换
在这里插入图片描述

  • 当一个线程执行了start方法后,不代表这个线程就会立即被执行,只代表这个线程处于可运行的状态,最终由OS的线程调度来决定哪个可运行状态下的线程被执行。
  • 一个线程一次被选中执行是有时间限制的,这个时间段叫做CPU的时间片,当时间片用完但线程还没有结束时,这个线程又会变为可运行状态,等待OS的再次调度;在运行的线程里执行Thread.yeild()方法同样可以使当前线程变为可运行状态。
  • 在一个运行中的线程等待用户输入、调用Thread.sleep()、调用了其他线程的join()方法,则当前线程变为阻塞状态。
  • 阻塞状态的线程用户输入完毕、sleep时间到、join的线程结束,则当前线程由阻塞状态变为可运行状态。
  • 运行中的线程调用wait方法,此线程进入等待队列。
  • 运行中的线程遇到synchronized同时没有拿到对象的锁标记、等待队列的线程wait时间到、等待队列的线程被notify方法唤醒、有其他线程调用notifyAll方法,则线程变成锁池状态。
  • 锁池状态的线程获得对象锁标记,则线程变成可运行状态。
  • 运行中的线程run方法执行完毕或main线程结束,则线程运行结束。

死锁

何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

死锁产生的原因

1、系统资源的竞争

通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。

2、进程推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。

死锁产生的必要条件:

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

(1)互斥条件: 进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

(2)不剥夺条件: 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

(3)请求和保持条件: 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

(4)循环等待条件: 存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图1所示。

如何避免死锁

在有些情况下死锁是可以避免的。下面介绍三种用于避免死锁的技术:

加锁顺序(线程按照一定的顺序加锁)
**加锁时限(**线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测

1、加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

2、加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

3、死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

避免死锁的方式

1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。

2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。

3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

 我们可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1。此时如果T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就可以获得L3了,然后T2执行完之后释放L2, L3,所以T1也可以获得L2了执行完然后释放锁L1, L2,然后T3睡眠醒来,也可以获得L1, L3了。打破了死锁的闭环。

3、死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

避免死锁的方式

1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。

2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。

3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是我们可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

 我们可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放以及获得的锁,并睡眠一小段时间。这样就打破了死锁的闭环。比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1。此时如果T3申请锁L1失败,那么T3释放锁L3,并进行睡眠,那么T2就可以获得L3了,然后T2执行完之后释放L2, L3,所以T1也可以获得L2了执行完然后释放锁L1, L2,然后T3睡眠醒来,也可以获得L1, L3了。打破了死锁的闭环。

线程池

线程池原理

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

线程池的好处:

1、降低资源的消耗
2、提高响应的速度
3、方便管理。
线程复用、可以控制最大并发数、管理线程

线程复用

每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

线程池的组成

一般的线程池主要分为以下 4 个组成部分:

  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:

  1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
  2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
  3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
  4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
    以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

Java 线程池工作过程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

12、Socket:★★★★,套接字,通信的端点。

就是为网络服务提供的一种机制,通信的两端都有 Socket,网络通信其实就是 Socket 间的通信,数据在两个 Socket 间通过 IO 传输。
UDP 传输:
1,只要是网络传输,必须有 socket 。
2,数据一定要封装到数据包中,数据包中包括目的地址、端口、数据等信息。

13、JVM

JVM的主要组成部分

主要分为4部分:

1,类加载器

2,运行时数据区

3,执行引擎

4,本地库接口

JVM的划分

java 分了 5 片内存。
1:寄存器。2:本地方法区。3:方法区。4:栈。5:堆。

静态变量存在方法区中,
栈: 存储的都是局部变量 ( 方法中定义的变量,方法上的参数,语句中的变量 ); 只要数据运算完成所在的区域结束,该数据就会被释放。
堆: 用于存储数组和对象,也就是实体。啥是实体啊?就是用于封装多个数据的。
1:每一个实体都有内存首地址值。
2:堆内存中的变量都有默认初始化值。因为数据类型不同,值也不一样。
3:垃圾回收机制

1.概述

jvm在java程序运行中会把内存区域划分成若干区域进行管理,每个区域的创建、销毁和功能各不相同。 其中方法区和堆是所有线程共享的数据区,栈(虚拟机栈和本地方法栈)和程序计数器是线程隔离的。

2.程序计数器(Programmer Counter Register)

功能:用来记录当前线程所执行的字节码行号,如果是native方法。计数器为undefined
内存空间:很小,相对于其他区域可以忽略
线程私有
内存溢出异常:不存在,是jvm中唯一没有规定内存溢出情况的区域。
理论上来说,字节码解释器通过程序计数器选取下一条需要执行 的字节码指令,循环、跳转、异常处理、线程恢复等都需要。程序计数器是线程私有的,各条线程之间计数器相互独立。

3.jvm栈(Java Virtual Machine Stack)

功能:java方法执行的内存模型,每个方法执行的时候都有一个栈帧,一个方法从调用到执行完毕,对应着一个栈帧从入栈到出栈的过程。栈帧中存储着局部变量表,方法出口等信息,其中局部变量表的内存大小是在程序编译期间就完成分配的,所以当进入一个方法的时候,栈帧中分配多少空间给局部变量是完全确定的,方法运行期间不会更改局部变量表的大小。
内存空间:
线程私有,生命周期和线程相同
内存溢出异常:
1)线程请求栈的长度>jvm允许的深度,StackOverflowError异常
2)通常来说jvm是允许动态扩展的,如果动态扩展申请不到需要扩展的内存空间,就会报OutOfMemoryError异常

4.本地栈(Native Method Stack)

功能:基本上和jvm栈一样,区别在于jvm栈是为了java程序(字节码)服务的,本地栈是为了native方法服务的。

5.堆(Heap)

功能:堆的唯一目的就是存放对象实例,最初jvm规范上所有实例对象和数组都要在堆上分配,但是随着JIT编译器和逃逸分析技术的发展,这个规范变得不那么绝对了。
内存空间:
线程共享,堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),这个划分是为了更好的垃圾回收,更快的分配内存,存储的仍然是实例对象。
内存溢出异常:堆内存是逻辑连续,不是物理连续,一般来说都是可以扩展的,当堆没有内存完成实力分配,且没有办法扩展的时候,就会报出OutOfMemoryError异常。

6.方法区(Method Area)

功能:方法区是用来存储jvm加载的类信息、常量、静态变量、即时编译器编译后的代码。
内存空间: 。在Hotspot在1.7之前,GC回收分代会用永久代实现方法区,但这种方式并不好,由于永久代的-XX:MaxPermSize的上限问题,会出现内训溢出,还会由于部分方法(如:string.intern())。JDK1.7的Hotspot把原本放在永久代的字符串常量池移出。
线程共享,和堆一样。但是方法区有个别名为Non-Heap。
内存溢出异常:方法区无法满足内存分配的时候,报出OutOfMemoryError异常。

7.运行时常量池(Runtime Constant Pool)
功能:它其实是方法区的一部分

JVM的垃圾回收算法

1,什么是垃圾回收?(GC垃圾回收线程)

所谓的垃圾回收,是指回收哪些死亡的对象所占据的堆空间。

2,如何判断一个对象是垃圾?

判断一个对象已经死亡,有两种方式,引用计数法可达性分析算法

引用计数法,需要额外的空间来存储计数器,如果有一个引用指向某一个对象,则该对象的引用计数器+1,如果该引用指向另一个对象,则原先的对象计算器-1.

但这种算法,会存在循环引用的bug问题,存在内存溢出的风险。

**可达性分析算法,**是以GC Root作为起点,能够引用到的对象则是有用对象,反之则是死亡的。

那么,什么是GC Root,一般可以理解为堆外指向堆内的引用,包括以下常见的两种:
1,java方法栈帧中的局部变量

2,已被加载的类静态变量

垃圾回收机制:

不需要程序员直接控制内存回收,由垃圾回收器在后台自动回收不再使用的内存。避免程序忘记及时回收,导致内存泄露。避免程序错误回收程序核心类库的内存,导致系统崩溃。

垃圾回收机制的特点:

1)垃圾回收机制回收JVM堆内存里的对象空间,不负责回收栈内存数据。
2)对其他物理连接,比如数据库连接、输入流输出流、Socket连接无能为力。
3)垃圾回收发生具有不可预知性,程序无法精确控制垃圾回收机制执行。
4)可以将对象的引用变量设置为null,暗示垃圾回收机制可以回收该对象。

GC算法总体概述:

JVM在进行GC时,并非每次都对上面三个内存区域一起回收,大部分时候回收的区域都是只新生代。因此GC按照回收的区域又分了两种类型,一种时普通的GC(Minor GC),一种是全局GC(Major GC或者Full GC)。
普通GC(Minor GC):只针对新生代区域的GC,
全局GC(Major GC或者Full GC):针对老年代的GC,偶尔伴随着对新生代的GC以及对永久代的GC。

垃圾回收算法主要分为4种:

标记清除算法、标记压缩算法、复制算法、分代收集算法

1,标记清除算法

是现在垃圾算法的思想基础,它将垃圾回收分为两个阶段:

标记阶段和清除阶段。

首先,是通过根节点GC Root,标记所有从根节点开始的可达对象。

因此,未被标记的对象都是垃圾对象。

然后,在清除节点,则删除所有未被标记的对象。

标记清除算法的缺点:

1,效率不高

2,该算法会产生不连续的内存碎片,当我们需要分配较大对象时,会因为无法找到足够的连续内存空间,而不得不再次提前触发垃圾回收,如果内存还是不够,则报内存不足异常。

2,标记压缩算法

标记压缩算法是老年代的一种回收算法

首先,标记阶段跟“标记清除算法”一致

区别在于清理阶段,为了避免内存碎片产生,所有的存活对象会被压缩到内存的一端
这个算法解决之前标记清除算法的碎片问题

但是标记和压缩的效率依然不高

3,复制算法

复制算法是为了解决效率问题,它将内存一分为二,每次只使用其中一块,

这样,当这一块内容用完了,就将存活的对象复制到另一个块上,然后将另一块内存一次清理掉,这样回收的效率也就提升了,也不存在内存碎片的问题。

算法优点是回收效率高,不存在内存碎片,但是浪费内存一半的内存空间,另外在对象存活率高的情况下,采用复制算法,效率将会变低。

4,分代收集算法

目前,主流的虚拟机大都采用分代收集算法,它根据对象存活周期的不同,而将内存划分为多块区域。一般就是我们耳熟能详的新生代和老年代,然后再各自采用不同的回收算法。

新生代(Eden),对象的存活率低,所以采用复制算法

老年代(Old),对象的存活率高,所以采用标记清除或标记整理算法

对象会优先分配到新生代,如果长时间存活或者对象过大会直接分配到老年代(新生代空间不够)。

算法细节:

1,对象新建,将存放在新生代的Eden区域,注意Suvivor区又分为两块区域,FromSuv和ToSuv
2,当年轻代Eden满时,将会触发Minor GC,如果对象仍然存活,对象将会被移动到Fromsuvivor空间,对象还是在新生代
3,再次发生minor GC,对象还存活,那么将会采用复制算法,将对象移动到ToSuv区域,此时对象的年龄+1
4,再次发生minor GC,对象仍然存活,此时Survivor中跟对象Object同龄的对象还没有达到Surivivor区的一半,所以还是会继续采用复制算法,将fromSuv和ToSuv的区域进行互换
5,当多次发生monorGC后,对象Object仍然存活,且此时,此时Survivor中跟对象Object同龄的对象达到Surivivor区的一半,那么对象Object将会移动到老年代区域,或者对象经过多次的回收,年龄达到了15岁,那么也会迁移到老年代。

垃圾回收器有哪些?

做垃圾回收的时候,都有一个统一的特点,叫stop the world.

往回收效率越来越高的方向来走的,垃圾回收的时间(stop the world)在变短

1,单线程回收器

采用单个线程的方式来进行回收,效率一般。服务器是多核CPU,资源无法得到更好利用

2,多线程回收器

可以充分利用CPU资源

3,CMS回收器

3.1 初始化标记

GCRoot
这个时候会stop the world,但是由于我们只是标记GCRoot,所以花费的时间很短

3.2 并发标记

一边可以继续往下跟踪,做可达性分析,相比比较耗时 100

一边可以让程序继续运行,可能重新创建对象,也可能创造垃圾 20

3.3 重新标记

处理在并发标记过程中,再次产生新的垃圾,stop the world 20

3.4 并发回收

一边针对我们刚才的垃圾对象进行回收

一边程序继续运行

4,G1垃圾回收器

将内存划分多个块 ,每个块再独立进行回收

14、异常

在这里插入图片描述

java中异常分类

Throwable类有两个直接子类:

(1)Exception:出现的问题是可以被捕获的

(2)Error:系统错误,通常由JVM处理

异常分类

在这里插入图片描述
(1)Check(受检)异常: 表示程序可以处理的异常,如果抛出异常的方法本身不能处理它,那么方法调用者应该去处理它,从而使程序恢复运行,不至于终止程序。派生自Exception的异常类,需要用 try…catch… 语句捕获并进行处理,并且可以从异常中恢复;除了RuntimeException及其子类以外,都是checked exception。

(2)Runtime(非受检)异常:非受检异常指的是java.lang.RuntimeException和java.lang.Error类及其子类,所有其他的异常类都称为受检异常。使用throw语句可以随时抛出这种异常对象 throw new ArithmeticException(…);

1:编译时被检查的异常, 只要是 Exception 及其子类都是编译时被检测的异常。
2:运行时异常, 表示无法让程序恢复运行的异常,导致这种异常的原因通常是由于执行了错误操作,一旦出现了错误操作,建议终止程序,因为Javs编译器不检查这种异常。其中 Exception 有一个特殊的子类 RuntimeException,以及 RuntimeException的子类是运行异常,也就说这个异常是编译时不被检查的异常。

编译时被检查的异常和运行时异常的区别:
代码中的异常处理其实是对可检查异常的处理。
编译被检查的异常在方法内被抛出,方法必须要声明,否编译失败。
声明的原因:是需要调用者对该异常进行处理。
运行时异常如果在方法内被抛出,在方法上不需要声明。
不声明的原因:不需要调用者处理,运行时异常发生,已经无法再让程序继续运行,所以,不让
调用处理的,直接让程序停止,由调用者对代码进行修正。

定义异常处理时,什么时候定义 try,什么时候定义 throws 呢?
功能内部如果出现异常,如果内部可以处理,就用 try;
如果功能内部处理不了,就必须声明出来,让调用者处理。使用 throws 抛出,交给调用者处理。谁调用了这个功能谁就是调用者;

发生异常的原因

1、用户输入了非法数据。

2、要打开的文件不存在。

3、网络通信是连接中断

4、JVM内存溢出

throw 和 throws 关键字的区别:

throw 用于抛出异常对象,后面跟的是异常对象;throw 用在方法内。
throws 用于抛出异常类,后面跟的异常类名,可以跟多个,用逗号隔开。throws 用在方法上。
通常情况:方法内容如果有 throw,抛出异常对象,并没有进行处理,那么方法上一定要声明,否
则编译失败。但是也有特殊情况。

常见5个运行时异常:

算数异常
空指针异常
类型转换异常
数组越界
NumberFormateException (数字格式异常,转换文字失败,比如“a12”就会转换失败)

常见5个非运行时异常

IOException
SQLException
FileNotFoundException
NoSuchFileException
NoSuchMethodException

15、JAVA 锁

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。

自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量
线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;

10. JIT即时编译(不清楚)

just in time 的缩写,即时编译器技术。在运行时 JIT 会把翻译过的机器码保存起来,以备下次使用,使用JIT能够加速 Java 程序的执行速度。

12. 内存泄漏和内存溢出的区别

1、内存泄漏memory leak :
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

2、内存溢出 out of memory :
指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

3、二者的关系:
内存泄漏的堆积最终会导致内存溢出 内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

5、内存溢出的原因及解决方法:
(1) 内存溢出原因:
内存中加载的数据量过于庞大,如一次从数据库取出过多数据; 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; 代码中存在死循环或循环产生过多重复的对象实体; 使用的第三方软件中的BUG; 启动参数内存值设定的过小
(2)内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

15. 数据库中的锁有哪些种类?什么情况会引起表锁?

分类不同,答案不唯一。表锁、行锁、页面锁。表锁:索引失效;事务需要更新大部分数据;事务涉及多个表

数据库锁出现的目的: 处理并发问题

并发控制的主要采用的技术手段:乐观锁、悲观锁和时间戳。

锁分类
从数据库系统角度分为三种:排他锁、共享锁、更新锁。
从程序员角度分为两种:一种是悲观锁,一种乐观锁。

悲观锁(Pessimistic Lock)
顾名思义,很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人拿这个数据就会block(阻塞),直到它拿锁。

悲观锁(Pessimistic Lock):正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

传统的关系数据库里用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。

悲观锁按使用性质划分

共享锁(Share Lock)
S锁,也叫读锁,用于所有的只读数据操作。共享锁是非独占的,允许多个并发事务读取其锁定的资源。
性质

  1. 多个事务可封锁同一个共享页;
  2. 任何事务都不能修改该页;
  3. 通常是该页被读取完毕,S锁立即被释放。

在SQL Server中,默认情况下,数据被读取后,立即释放共享锁。
例如,执行查询语句“SELECT * FROM my_table”时,首先锁定第一页,读取之后,释放对第一页的锁定,然后锁定第二页。这样,就允许在读操作过程中,修改未被锁定的第一页。
例如,语句“SELECT * FROM my_table HOLDLOCK”就要求在整个查询过程中,保持对表的锁定,直到查询完成才释放锁定。

排他锁(Exclusive Lock)=
X锁,也叫写锁,表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。(某个顾客把试衣间从里面反锁了,其他顾客想要使用这个试衣间,就只有等待锁从里面打开了。)
性质

  1. 仅允许一个事务封锁此页;
  2. 其他任何事务必须等到X锁被释放才能对该页进行访问;
  3. X锁一直到事务结束才能被释放。

产生排他锁的SQL语句如下:select * from ad_plan for update;

更新锁
U锁,在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。

因为当使用共享锁时,修改数据的操作分为两步:

  1. 首先获得一个共享锁,读取数据,
  2. 然后将共享锁升级为排他锁,再执行修改操作。
    这样如果有两个或多个事务同时对一个事务申请了共享锁,在修改数据时,这些事务都要将共享锁升级为排他锁。这时,这些事务都不会释放共享锁,而是一直等待对方释放,这样就造成了死锁。
    如果一个数据在修改前直接申请更新锁,在数据修改时再升级为排他锁,就可以避免死锁。

性质

  1. 用来预定要对此页施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;
  2. 当被读取的页要被更新时,则升级为X锁;
  3. U锁一直到事务结束时才能被释放。

悲观锁按作用范围划分为:行锁、表锁。

行锁
锁的作用范围是行级别。

表锁
锁的作用范围是整张表。

数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。

举个例子,一个用户表user,有主键id和用户生日birthday。
当你使用update … where id=?这样的语句时,数据库明确知道会影响哪一行,它就会使用行锁;
当你使用update … where birthday=?这样的的语句时,因为事先不知道会影响哪些行就可能会使用表锁。

乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以,不会上锁。但是在更新的时候会判断一下在此期间别人有没有更新这个数据,可以使用版本号等机制。

乐观锁( Optimistic Locking ): 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。
悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。
乐观锁,大多是基于数据版本( Version )记录机制实现。
数据版本:为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

乐观锁的实现方式(这部分摘自https://www.jianshu.com/p/eb41df600775 )
版本号(version)
版本号(记为version):就是给数据增加一个版本标识,在数据库上就是表中增加一个version字段,每次更新把这个字段加1,读取数据的时候把version读出来,更新的时候比较version,如果还是开始读取的version就可以更新了,如果现在的version比老的version大,说明有其他事务更新了该数据,并增加了版本号,这时候得到一个无法更新的通知,用户自行根据这个通知来决定怎么处理,比如重新开始一遍。这里的关键是判断version和更新两个动作需要作为一个原子单元执行,否则在你判断可以更新以后正式更新之前有别的事务修改了version,这个时候你再去更新就可能会覆盖前一个事务做的更新,造成第二类丢失更新,所以你可以使用update … where … and version=”old version”这样的语句,根据返回结果是0还是非0来得到通知,如果是0说明更新没有成功,因为version被改了,如果返回非0说明更新成功。

时间戳(使用数据库服务器的时间戳)

时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳不能是业务系统的时间。

待更新字段

待更新字段:和版本号方式相似,只是不增加额外字段,直接使用有效数据字段做版本控制信息,因为有时候我们可能无法改变旧系统的数据库表结构。假设有个待更新字段叫count,先去读取这个count,更新的时候去比较数据库中count的值是不是我期望的值(即开始读的值),如果是就把我修改的count的值更新到该字段,否则更新失败。java的基本类型的原子类型对象如AtomicInteger就是这种思想。

所有字段
所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没变化才会执行更新。
乐观锁几种方式的区别(这部分摘自https://www.jianshu.com/p/eb41df600775 )
新系统设计可以使用version方式和timestamp方式,需要增加字段,应用范围是整条数据,不论那个字段修改都会更新version,也就是说两个事务更新同一条记录的两个不相关字段也是互斥的,不能同步进行。旧系统不能修改数据库表结构的时候使用数据字段作为版本控制信息,不需要新增字段,待更新字段方式只要其他事务修改的字段和当前事务修改的字段没有重叠就可以同步进行,并发性更高。

并发控制会造成两种锁
活锁
死锁
并发控制会造成活锁和死锁,就像操作系统那样,会因为互相等待而导致。

活锁
定义:指的是T1封锁了数据R,T2同时也请求封锁数据R,T3也请求封锁数据R,当T1释放了锁之后,T3会锁住R,T4也请求封锁R,则T2就会一直等待下去。
解决方法:采用“先来先服务”策略可以避免。

死锁
定义:就是我等你,你又等我,双方就会一直等待下去。比如:T1封锁了数据R1,正请求对R2封锁,而T2封住了R2,正请求封锁R1,这样就会导致死锁,死锁这种没有完全解决的方法,只能尽量预防。
预防方法:

  1. 一次封锁法,指的是一次性把所需要的数据全部封锁住,但是这样会扩大了封锁的范围,降低系统的并发度;
  2. 顺序封锁法,指的是事先对数据对象指定一个封锁顺序,要对数据进行封锁,只能按照规定的顺序来封锁,但是这个一般不大可能的。

系统判定死锁的方法:

超时法:如果某个事物的等待时间超过指定时限,则判定为出现死锁;
等待图法:如果事务等待图中出现了回路,则判断出现了死锁。
对于解决死锁的方法,只能是撤销一个处理死锁代价最小的事务,释放此事务持有的所有锁,同时对撤销的事务所执行的数据修改操作必须加以恢复。

20. 什么是Java的序列化,如何实现Java的序列化?列举在哪些程序中见过Java序列化?

Java中的序列化机制能够将一个实例对象(只序列化对象的属性值,而不会去序列化什么所谓的方法。)的状态信息写入到一个字节流中使其可以通过socket进行传输、或者持久化到存储数据库或文件系统中;然后在需要的时候通过字节流中的信息来重构一个相同的对象。一般而言,要使得一个类可以序列化,只需简单实现java.io.Serializable接口即可。
==对象的序列化主要有两种用途: ==
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2)在网络上传送对象的字节序列。

设计模式

解决问题最行之有效的思想。是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。
java 中有 23 种设计模式:
单例设计模式: ★★★★★
解决的问题:保证一个类在内存中的对象唯一性。
比如:多程序读取一个配置文件时,建议配置文件封装成对象。会方便操作其中数据,又要保证多个程序读到的是同一个配置文件对象,就需要该配置文件对象在内存中是唯一的。
Runtime()方法就是单例设计模式进行设计的。

//饿汉式
class Single{
private Single(){} //私有化构造方法。
private static Single s = new Single(); //创建私有并静态的本类对象。
public static Single getInstance(){ //定义公有并静态的方法,返回该对象。
return s;
}
}
---------------------------------------------
//懒汉式:延迟加载方式。
class Single2{
private Single2(){}
private static Single2 s = null;
public static Single2 getInstance(){
if(s==null)
s = new Single2();
return s;
}
}
----

8. treetLocal(不清楚)

听着不太对,是不是问的 ThreadLocal?ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,可以使⽤get和set⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题

Spring 原理

它是一个全面的、企业应用开发一站式的解决方案,贯穿表现层、业务层、持久层。但是 Spring仍然可以和其他的框架无缝整合。

Spring 特点

  1. 轻量级
  2. 控制反转
  3. 面向切面
  4. 容器
  5. 框架集合
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值