本章是整理知识内容,为强化知识长期更新。
面向对象思想
- 面向对象是一个思想,时间万物皆可以被看做一个对象。
封装
- 隐藏对象的属性和实现的具体细节,只对外暴露公共访问方式。
继承
- 当多个类出现相同代码逻辑时,我们通常将相同的代码重构到一个类中,如果是绑定关系就可以使用继承。
- Java中类是单继承。多继承会导致棱形问题。
- 继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。
- 可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。
多态
- 一个事物的的多种状态,比如女人、男人都是人的性别。人的性别就分为女人、男人。
抽象
- 在逻辑上看似相关的,想要把他们联系起来。这样可以提高效率。矩形、圆形,都可以具有周长和面积两个方法,但是计算的方式完全不同,矩形和圆形之间肯定不能构成子父类的关系,那么只能是同时去继承一个父类。这时,就引出了抽象的概念。
总结
- 封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。它需要编程语言提供权限访问控制语法来支持,例如 Java 中的 private、protected、public 关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。
- 抽象可以通过接口类或者抽象类来实现,但也并不需要特殊的语法机制来支持。抽象存在的意义,一方面是提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。
- 继承是用来表示类之间的 is-a 关系,主要是用来解决代码复用的问题。
- 多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也需要编程语言提供特殊的语法机制来实现。主要解决扩展性问题。
接口与抽象类
如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类;如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。
接口:它是一种自上而下的设计思路。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。
- 实际开发过程中,容易过度使用比如给每个类都定义接口。
- 基于接口而非实现编程”这条原则的英文描述是:“Program to an interface, not an implementation”
- 从本质上来看,“接口”就是一组“协议”或者“约定”,是功能提供者提供给使用者的一个“功能列表”。“接口”在不同的应用场景下会有不同的解读,比如服务端与客户端之间的“接口”,类库提供的“接口”,甚至是一组通信的协议都可以叫作“接口”。
抽象类 :抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)
抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。接口不能包含属性,只能声明方法,方法不能包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。
每个优秀的程序员都知道,不应该定义一个attackBaghdad() ‘袭击巴格达‘ 的方法,而是应该把城市作为函数的参数 attack(city)。
重写和重载
- 重载:同一个类中,方法名相同,参数个数或者类型不相同,返回类型可以不相同。
- 重写:类的继承关系中体现,子类重写父类的方法。
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
重写(override)
- 存在父类和子类之间。
- 方法名、参数、返回值相同。
- 方法被
final
修饰不能被重写。 - 子类重写父类方法后,不能抛出比父类方法的异常。子类不能缩写父类的方法
访问权限
。
重载(overload)
- 参数类型、个数、顺序至少有一个不相同。
- 不能重载只有返回值不同的方法名。
- 存在于父类和子类、同类中。
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。
介绍下 Java 基本数据类型
- Java中存在8个原生数据类型,同时又分成四种:整形、浮点型、char、Boolean。它们之间存在自动类型转换,规则是从小到大。并且都存在自动装箱拆箱特性,但是这种操作是隐式操作而且在某些情况会导致CG压力增大。
类型 | 存储需求 | 取值范围 |
---|---|---|
int | 4字节 | -2 147 483 638 ~ 2 147 483 637 |
short | 2字节 | -32 768 ~ 32 767 |
long | 8字节 | -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807 |
byte | 1字节 | - 128 ~ 127 |
- 整型的范围与运行Java运行的硬件没有关系,所有的数据类型所占的字节数量与平台无关。
类型 | 存储需求 | 取值范围 |
---|---|---|
float | 4字节 | 大约 $\pm$ 3.402 823 37F + 38F (有效位数为7~8位) |
double | 8字节 | 大约 $\pm$ 1.797 693 134 862 315 70E + 308 (有效位数为15位) |
double这种类型的精度是float的两倍。
所有浮点数值计算都遵循IEEE 754规范,下面是溢出和出错的情况的三种特殊的浮点数值。
- 正无穷大
- 负无穷大
- NaN ( 不是一个数字 )
- 一个整整数除以0的结果为正无穷大,计算0/0或者负数的平方根结果为NaN。
char类型
- char类型表示单个字符,属于Unicode编码表。因为历史原因,不建议在程序中使用。除非确实要对UTF-16代码单元进行操作。
- char字节大小
- Java中无论是汉字还是英文字母都是用Unicode编码来表示的,一个Unicode是16位,每字节是8位,所以一个Unicode码占两字节。但是英文字母比较特殊,源自于8位(1字节)的ASCII吗,于是在Unicode码仅使用了低8位(1字节)就可以表示。
boolean类型
- 布尔类型,只有两个值false、true。基本用于判定条件。
- boolean字节大小
- Java规范中并未明确规定boolean类型的大小。
自动类型转换
整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。
转换从低级到高级。
1
byte,short,char—> int —> long—> float —> double
不能对boolean进行类型转换、不能把对象类型转换成不相关的对象、把大容量的对象转换成小容量对象时需要强制类型转换、转换过程中间可能出现精度损失。
装箱和拆箱boxing or unboxing
装箱:将基础类型用它们对应类型包装起来
拆箱:将包装类型转换成基本数据类型
原语 | 对应的JDK类 |
---|---|
int | java.lang.Integer |
short | java.lang.Short |
long | java.lang.Long |
byte | java.lang.Byte |
char | java.lang.Character |
double | java.lang.Double |
float | java.lang.Float |
boolean | java.lang.Boolean |
Java中只有原生数据类型是特殊的,它们不是对象。其它的都是对象。那么就一个尴尬的问题,集合类都是存放的对象,JDK5之后考虑到这个问题就自动进行逆行拆箱装箱的操作。
1
2
3//比如所在泛型中是不能存放原生数据类型的,如要要存放原生数据类型的数据,需要装箱。
Collection<int> c = new ArrayList<int>(); //这是无法编译成功的。
Collection<Integer> cc = new ArrayList<Integer>(); //这样才行。每个 JDK 类都提供了相应方法来解析它的内部表示,并将其转换为相应的原语类型。
但是注意装箱拆箱操作其实是非常消耗内存的举动,在该过程中可能会生成生成无用对象增加GC压力。所以尽量避免这中操作。
1
2
3
4Integer sum = 0;
for(int index = 1000; index < 5000; index ++){
sum+=index;
}比如这种,每次sum都需要自动拆箱。
默认情况下整数的类型都是int、浮点型的数都是double。
1
float d = 1.1f; //在后面添加f,大小写不区分。隐式强制类型转换
int 和 Integer 有什么区别。
- Integer是int的包装类,它有一个int类型字段存储数据,并提供了基础的操作。关于Integer的缓存值,jdk5之后引入了一个静态工厂方法
valueOf
,在调用它的时候会有明显的性能提升,内部是一个缓存机制IntegerCache
默认的长度是-128~127之间。- 这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
- 有 ByteCache 用于缓存 Byte 对象。
- 有 ShortCache 用于缓存 Short 对象。
- 有 LongCache 用于缓存 Long 对象。
- 有 CharacterCache 用于缓存 Character 对象。
- Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。
- 这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
Java中访问类型
访问类型 | 同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | |
default | Y | Y | ||
private | Y |
instanceof 关键字
- instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
1 | boolean result = obj instanceof Class |
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
1 | int i = 0; |
声明一个Double变量赋值 0.001会编译错误吗 ?
- 不会,double 数据类型是双精度、64 位、标准的浮点数。
Java是纯粹的面向对象语言吗?
- Java不是存粹的面向对象语言,因为它包含了原生数据类型,比如int 、double。
String、StringBuffer、StringBuilder的区别
- 可变性
- String 是不可变的,StringBuffer、StringBuilder是可变的。
- String 类中使用 final 关键字字符数组保存字符串,
private final char value[]
,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的。
- 线程安全方面
- String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder
是StringBuilder
与StringBuffer
的公共父类,但是StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
- String 中的对象是不可变的,也就可以理解为常量,线程安全。
- 性能
- 操作少量的数据 = String
- 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
为什么 String 类型要用 final 修饰?
使用 final 修饰的第一个好处是安全;第二个好处是高效,以 JVM 中的字符串常量池来举例,如下两个变量:
1 | public class Test { |
- 只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率,如下图所示:
- 工作原理
当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。
为什么 char 数组比 Java 中的 String 更适合存储密码
- 字符串在 Java 中是不可变的,如果你将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它. 并且为了可重用性,会存在 String 在字符串池中, 它很可能会保留在内存中持续很长时间,从而构成安全威胁。
- 由于字符串是不可变的,所以不能更改字符串的内容,因为任何更改都会产生新的字符串,而如果你使用char[],你就可以将所有元素设置为空白或零。因此,在字符数组中存储密码可以明显降低窃取密码的安全风险。
== 和 equals 与 compareTo
- == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
- equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
- compareTo() 方法用于比较两个字符串,返回的结果为 int 类型的值。
1 | public int compareTo(String anotherString) { |
equals() 可以接收一个 Object 类型的参数,而 compareTo() 只能接收一个 String 类型的参数;
equals() 返回值为 Boolean,而 compareTo() 的返回值则为 int。
它们都可以用于两个字符串的比较,当 equals() 方法返回 true 时,或者是 compareTo() 方法返回 0 时,则表示两个字符串完全相同。
为什么要重写equals和hashcode方法
- equals是Object的成员方法,默认不重写(override)情况下判断等价性。
- 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
1 | public class test { |
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。
- 集合中使用场景。
- 将对象放入到集合中时,首先判断要放入对象的 hashCode 值与集合中的任意一个元素的 hashCode 值是否相等,如果不相等直接将该对象放入集合中。
- 如果 hashCode 值相等,然后再通过 equals 方法判断要放入对象与集合中的任意一个对象是否相等,如果 equals 判断不相等,直接将该元素放入到集合中,否则不放入。
- 集合中使用场景。
hashCode()与equals()的相关规定
- 如果两个对象相等,则hashcode一定也是相同的。
- 两个对象相等,对两个对象分别调用equals方法都返回true。
- 两个对象有相同的hashcode值,它们也不一定是相等的。
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
总结
- 如果在 Java 运行期间对同一个对象调用 hashCode 方法后,无论调用多少次,都应该返回相同的 hashCode,但是在不同的 Java 程序中,执行 hashCode 方法返回的值可能不一致。
- 如果两个对象的 equals 相等,那么 hashCode 必须相同。
- 如果两个对象 equals 不相等,那么 hashCode 也有可能相同,所以需要重写 hashCode 方法,因为你不知道 hashCode 的底层构造(反正我是不知道,有大牛可以传授传授),所以你需要重写 hashCode 方法,来为不同的对象生成不同的 hashCode 值,这样能够提高不同对象的访问速度。
- hashCode 通常是将地址转换为整数来实现的。
i++和++i有什么区别
- i++ 是在程序执行完毕后进行自增,而 ++i 是在程序开始执行前进行自增。
- i++ 的操作分三步
- 栈中取出 i
- i 自增 1
- 将 i 存到栈
- 三个阶段:内存到寄存器,寄存器自增,写回内存(这三个阶段中间都可以被中断分离开)
- 所以 i++ 不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致 i 的值不正确自增。
- ++i
- 在多核的机器上,CPU 在读取内存 i 时也会可能发生同时读取到同一值,这就导致两次自增,实际只增加了一次。
- i++ 和 ++i 都不是原子操作。
- 原子性:指的是一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程打断。
- i++ 的操作分三步
如何保证多线程下i++ 结果正确
- 使用循环CAS,比如AtomicInteger
- 使用锁机制
- 使用synchronized
Java值传递和引用传递的区别
Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。按值调用(call by value)表示方法接收的是调用者提供的值
其实只有传值
- 值传递(call by value),对于基本型变量,传递的是该变量的副本,改变副本不影响变量。
- 传递引用(call by reference),对于对象型变量,传递的该对象的地址的一个副本,并不是原对象本身。
Java的四种引用,强弱软虚
强引用
- 强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
1
2String str = new String("str");
// 强引用 str != null;- 当内存空间不足时,
Java
虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,需要弱化从而使GC
能够回收。
1
str = null;
- 显示的将str对象设置成null,这样就脱离的对象的生命周期,具体什么时候取决当前的gc。
软引用
- 软引用在程序内存不足时,会被回收,使用方式:
1
2
3
4// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
// 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。弱引用
- 弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
1
2
3
4WeakReference<String> wrf = new WeakReference<String>(str);
// Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用
// 不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作
// 弱引用 wrf = null; 无法在获取对象虚引用
虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用。
1
2
3PhantomReference<String> prf = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());
// 对象销毁前的一些操作,比如说资源释放等。Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效
// prf = null ; 无法在获取对象Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用
类型 | 回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软饮用 | 当内存不足时 | 对象缓存 | 内存不足时终止 |
弱饮用 | 正常垃圾回收时 | 对象缓存 | 垃圾回收后终止 |
虚饮用 | 正常垃圾回收时 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
描述下final 与 finally、finalize的区别。
final
,是修饰符关键字。- 如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在new一个对象时初始化(即只能在声明变量或构造器或代码块内初始化),而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能覆盖(重写)。
父类的private成员方法是不能被子类覆盖的,因为被private修饰的方法默认是final类型的。
final类
- final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会载被扩展,那么就设计为final类。
final方法
- 如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法。
final变量(常量)
- 用final修饰的成员变量表示常量,值一旦给定就无法改变!
- final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
- final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。类中的final数据成员就可以根据依赖对象而有所不同,并保持其恒定不变的特征。
final参数
- 当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值。
注意final 不是immutable的
- 如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在new一个对象时初始化(即只能在声明变量或构造器或代码块内初始化),而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能覆盖(重写)。
finally
在异常处理时提供finally
块来执行任何清除操作。如果抛出一个异常,那么相匹配的catch
子句就会执行,然后控制就会进入finally
块(如果有的话)。- 在异常处理时提供 finally 块来执行任何清除操作。只有在与 finally 相对应的 try 语句块得到执行的情况下,finally 语句块才会执行。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
- finally 语句块可能是要执行的。
- 当try流程中出现程序中断情况是不会在执行finally语句的。也就是说一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。
finalize
,是方法名。- Java 允许使用
#finalize()
方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。
- Java 允许使用
finally对应的try catch语句流程
1 | public static void main(String[] args) { |
从结果看finally语句会在return之前执行。
1 | public static void main(String[] args) { |
从结果看finally 语句块在 catch 语句块中的 return 语句之前执行。
1 | public static void main(String[] args) { |
从结果看 finally 语句块中如果出现return那么该流程就结束了。其实是finally块中的return语句会覆盖try块中的return返回。
1 | public static void main(String[] args) { |
从结果看:weary:为什么不返回200!为什么finally里面的修改没有效果?因为finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变。那什么情况下会改变呢?
1 | public static void main(String[] args) { |
从结果看finally里面的修改启效果了,因为抛出了异常所以没有执行try代码块里面的return。
1 | public static void main(String[] args) { |
- finally块的语句在try或catch中的return语句执行之后返回之前执行,finally里的修改语句可能影响也可能不影响try或catch中 return已经确定的返回值,若finally里也有return语句则覆盖try或catch中的return语句直接返回。没有进入try代码块就不会执行finally代码块。
static,this,super 关键字总结
static 关键字
static 关键字主要有以下四种使用场景
修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:
类名.静态变量名
类名.静态方法名()
静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:
import static
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
修饰成员变量和成员方法(常用)
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
调用格式:
类名.静态变量名
类名.静态方法名()
如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。
1 | public class StaticBean { |
静态代码块
- 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
1 | static { |
- 一个类中的静态代码块可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。
静态内部类
- 静态内部类与非静态内部类之间存在一个最大的区别,我们知道非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:
- 它的创建是不需要依赖外围类的创建。
- 它不能使用任何外围类的非static成员变量和方法。
1 | public class Singleton { |
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用
getUniqueInstance()
方法从而触发SingletonHolder.INSTANCE
时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
静态导包
格式为:import static
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法
1 | //将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用 |
静态方法与非静态方法
- 静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
1 | class Foo { |
- 你可以像这样调用静态方法:
Foo.method1()
。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:Foo bar = new Foo(1);bar.method2();
总结:
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
static{}
静态代码块与{}
非静态代码块(构造代码块)
相同点: 都是在JVM加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些static变量进行赋值。
不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次,之后不再执行,而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
一般情况下,如果有些代码比如一些项目最常用的变量或对象必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的。如果我们想要设计不需要创建对象就可以调用类中的方法,例如:Arrays类,Character类,String类等,就需要使用静态方法, 两者的区别是 静态代码块是自动执行的而静态方法是被调用的时候才执行的.
1 | public class Test { |
- 上述代码输出:
1 | 静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!-- |
- 当只执行
Test.test();
时输出:
1 | 静态代码块!--静态方法中的内容! --静态方法中的代码块!-- |
- 当只执行
Test test = new Test();
时输出:
1 | 静态代码块!--非静态代码块!--默认构造方法!-- |
- 非静态代码块与构造函数的区别是: 非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
this 关键字
this关键字用于引用类的当前实例。 例如:
1 | class Manager { |
在上面的示例中,this关键字用于两个地方:
- this.employees.length:访问类Manager的当前实例的变量。
- this.report():调用类Manager的当前实例的方法。
此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。
super 关键字
super关键字用于从子类访问父类的变量和方法。 例如:
1 | public class Super { |
在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 showNumber()
方法。
使用 this 和 super 要注意的问题
- 在构造器中使用
super()
调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 - this、super不能用在static方法中。
简单解释一下:
- 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西。
Java程序初始化的顺序是怎么样的
在 Java 语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建象。
初始化一般遵循3个原则:
- 静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
- 父类优先于子类进行初始化;
- 按照成员变量的定义顺序进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;
加载顺序
- 父类(静态变量、静态语句块)
- 子类(静态变量、静态语句块)
- 父类(实例变量、普通语句块)
- 父类(构造函数)
- 子类(实例变量、普通语句块)
- 子类(构造函数)
实例
1 | class Base { |
结果是:
1 | Base static block! |
介绍下异常类型
- Throwable
- Error
- Exception
- RuntimeException
- IOException
- 超类Throwable ,有两个子类Error和Exception,分别表示错误和异常。
- Error是程序无法处理的错误,比如OutOfMemoryError等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
- Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。
- 运行时异常(RuntimeException)和非运行时异常也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception),这两种异常有很大的区别。
- RuntimeException(运行时异常),表示无法让程序恢复的异常,导致的原因通常是因为执行了错误的操作,建议终止逻辑,因此,编译器不检查这些异常。
- CheckedException(受检查异常),是表示程序可以处理的异常,也即表示程序可以修复(由程序自己接受异常并且做出处理),所以称之为受检查异常。
- 常见的异常
- NullPointerException: 空指针异常。
- NoSuchMethodException:找不到方法。
- IllegalArgumentException:不合法的参数异常。
- IndexOutOfBoundException: 数组下标越界异常。
- IOException:由于文件未找到、未打开或者 I/O 操作不能进行而引起异常。
- ClassNotFoundException :找不到文件所抛出的异常。
- NumberFormatException: 字符的 UTF 代码数据格式有错引起异常。
- InterruptedException: 线程中断抛出的异常。
Throw 和 throws 的区别
throw
,用于在程序中显式地抛出一个异常。throws
,用于指出在该方法中没有处理的异常。每个方法必须显式指明哪些异常没有处理,以便该方法的调用者可以预防可能发生的异常。最后,多个异常用逗号分隔。位置不同
- throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
功能不同
- throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。
- throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行 throw 则一定抛出了某种异常对象。
常见的异常处理方式
- 使用System.out.println是高代价的,这这做会降低系统吞吐量。
- 在生成环境中避免使用printStackTrace()方法,printStackTrace默认会把调用的堆栈打印到控制台上,在生产环境中访问控制台是不现实的。
- 如果不能处理异常,就不要捕获该异常。
- 如果要捕获异常,应在最近的地方捕获它。
- 不要吃掉你的捕捉的异常信息,就是捕获了啥也不做,建议LOG记录。
- 不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调用者为了正常的控制流而使用异常)。
- 对可以恢复的情况使用受检异常,对编程错误使用运行时异常。
- 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)。
- 优先使用标准的异常。
- 每个方法抛出的异常都要有文档。
- 保持异常的原子性
- 不要在
catch
中忽略掉捕获到的异常。
如何正确的在一个循环中删除ArrayList中的元素。
如果使用普通for循环直接删除会出现IndexOutOfBoundsException异常,非法索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35ArrayList<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
list.add("2");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
// 这样做肯定抛异常,非法访问数据越界
int len = list.size();
for (int i = 0; i < len; i++) {
if("1".equals(list.get(i))){
list.remove(i);
}
}
//Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 4, Size: 4
//改进后,删除元素后更新List长度,更新循环下标。但是这样的可读性很差。而且不适用于多线程场景
int len = list.size();
for (int i = 0; i < len; i++) {
if("1".equals(list.get(i))){
list.remove(i);
--len;
--i;
}
}
//另外一种方式,这种看起来好读多了。。。
Iterator<String> sListIterator = list.iterator();
while(sListIterator.hasNext()){
String e = sListIterator.next(); //注意了
if(e.equals("1")){
sListIterator.remove();
}
}
Java序列化
- 序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。
- 反序列化的过程,则是和序列化相反的过程。
- 对于不想进行序列化的变量,使用transient关键字修饰。
- transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
Java反射
反射(Reflection)是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。
通过反射,可以在程序运行时访问Java对象的成员变量、方法、构造方法。
反射的缺点
- 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。
- 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
- 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。
反射的实现方式:
第一步:获取Class对象,有4中方法:
- 1)Class.forName(“类的路径”);
- 2)类名.class
- 3)对象名.getClass()
- 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
Java多线程
Java中的主线程
Java为多线程编程提供内置支持。多线程程序包含两个或多个可以并发运行的部分。这样的程序的每个部分称为线程,每个线程定义一个单独的执行路径。当Java程序启动时,一个线程立即开始运行。这通常被称为我们程序的主线程,因为它是我们程序开始时执行的那个。
- 它是生产启动其它
子
线程的线程。 - 它负责程序结束时候的收尾工作。
上面简单画图出一个主线程和子线程以及后台线程的关系。
在主线程中,这可以通过调用Thread类中的currentThread()方法来完成。此方法返回对其调用的线程的引用。主线程的默认优先级为5,对于所有剩余的用户线程,优先级将从父级继承到子级。同时虚拟机会启动其它的线程,比如垃圾回收器GC。
1 | package cn.z201.java.test.thread; |
1 | package cn.z201.java.test.thread; |
主线程默认优先级是5经过修改为10,启动的子线程就继承了主线程的级别。
什么是进程
进程是程序运行和资源分配的基本单位,一个程序至少一个进程,一个进程至少一个线程。
多个进程的*
内部数据和状态是完全独立的
*,而多个线程是共享一个内存空间和一组系统资源,有可能互相影响
。
什么是线程
- 线程是程序内部的控制流,
只能使用分配给线程 的资源和环境
。 线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈
,所以线程的切换比进程切换负担要小。- 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小能独立运行的基本单位。
多线程编程的目的
充分的利用 CPU 和 I/O 的利用率
- 多线程编程的目的,就是 最大限度地利用CPU资源,当某一线程的处理不需要占用CPC而和I/O等资源打交道时,让需要占用CPU资源的其他线程有机会获得CPU资源,从根本上,这就是多线程编程的最终目的。
- 线程(Thread)也是程序的最小单元,它依托进程而存在。
- 多个线程可以共享一块内存空间和一组系统资源因此线程之前的切换更加节省资源、更加轻量化,也因此被称为轻量级的进程。
多线程概念介绍
- 一个进程可以包含多个线程。
- 一个程序实现多个代码同时交替运行就需要产生多个线程。
- CPU随机的抽出时间,让我们的程序一会做这件事情,一会做另外一件事情。
- 同期其他大多数编程语言不同,Java内置支持多线程编程(multithreaded progranmming)。多线程程序包包含两个或两条以上并发运作的部分,吧程序中每个这种的部分都叫做一个线程(thread),每个线程都独立的执行路经,因此多线程是多任务处理的一种特殊形式。
- 多任务处理被所有的现代操作系统所支持。然后,多任务处理有两种截然不同的类型,基于进程和基于线程。
- 基于进程
- 基于进程的多任务处理是更熟悉的形式,进程(process)本质上是一个执行的程序。因此基于进程的多线程任务处理的特点是允许你的计算机同时运行两个或更多的程序。
- 基于线程
- 基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以执行两个或则多个任务的功能。
- 多线程程序比多进程程序需要更少的管理资源。
- 进程是重量级的任务,需要分配给他们的独立空间地址。进程之间通信是昂贵和受限的。进程之间的转换也是很需要花费的。另一方面,线程是轻量级的选手,它们共享相同的地址空间并且共享同一进程,线程之间通信是轻量级的,线程的转换也是轻量级的。
- 线程的实现
- 1.两种方法均需要执行线程start方法作为线程分配必须的系统资源、调度线程运行并执行线程的run方法。
- 2.在具体应用中,采用哪种方法来构造线程体要试情况而定。通常,当一个线程继承了另一个类,应该应该使用第二种方法,即使实现runnable接口。
- 3.线程的消亡不能通过调用一个Trread.stop方法,而是让线程自然消亡。
- 基于进程
线程的生命周期
一个线程的消亡过程。
新建(New)
- 创建后尚未启动。
可运行(Runnable)
- 可能正在运行,也可能正在等待 CPU 时间片。
- 包含了操作系统线程状态中的 运行(Running ) 和 就绪(Ready)。
无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 退出方法 没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll() 没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕 LockSupport.park() 方法 -
限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用 “使一个线程睡眠” 进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用 “挂起一个线程” 进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
进入方法 退出方法 Thread.sleep() 方法 时间结束 设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll() 设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕 LockSupport.parkNanos() 方法 - LockSupport.parkUntil() 方法 -
阻塞(Blocking)
- 这个状态下,是在多个线程有同步操作的场景,比如正在等待另一个线程的 synchronized 块的执行释放,或者可重入的 synchronized 块里别人调用 wait() 方法,也就是线程在等待进入临界区。
- 阻塞可以分为:等待阻塞,同步阻塞,其他阻塞
死亡(Terminated)
- 线程因为 run 方法正常退出而自然死亡。
- 因为一个没有捕获的异常终止了 run 方法而意外死亡。
线程的基本方法
wait线程等待
- 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
sleep线程睡眠
- sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。
yield线程让步
- yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
interrupt线程中断
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。
join等待其它线程终止
- join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
- 很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
notify线程唤醒
- Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
其它方法
- isAlive(): 判断一个线程是否存活。
- activeCount(): 程序中活跃的线程数。
- enumerate(): 枚举程序中的线程。
- currentThread(): 得到当前线程。
- isDaemon(): 一个线程是否为守护线程。
- setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
- setName(): 为线程设置一个名称。
- setPriority(): 设置一个线程的优先级。
- getPriority()::获得一个线程的优先级。
线程优先级
设置优先级是为了在多线程环境中便于系统对线程调度,优先级高的线程将优先执行。
- 一个线程的优先级遵从以下原则。
- 线程创建时,子继承父的优先级。
- 线程创建后,可用过调用setPriority()方法改变优先级。
- 线程的优先级是1-10之间的正整数。
- 1 - MIN_PRIORITY
- 10 - MAX_PRIORITY
- 5 - NORM_PRIORITY
- 一个线程的优先级遵从以下原则。
线程的优先级策略
- 线程调度器选择优先级最高的线程运行.但是,如果发生以下情况,就会终止线程的运行.
- 线程体中调用了yieid()方法,让出了对CPU的占用权。
- 线程体中调用sleep()方法,使线程进入了睡眠状态。
- 线程由I/O操作而受阻塞。
- 另一个更高优先级的线程出现。
- 在支持时间片段系统中,该线程的时间片用完。
- 线程调度器选择优先级最高的线程运行.但是,如果发生以下情况,就会终止线程的运行.
上下文切换
是指某一时间点 CPU 寄存器和程序计数器的内容。
- 寄存器
- 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
- 程序计数器
- 是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
- 上下文切换的活动
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。
- 引起线程上下文切换的原因
- 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务。
- 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务。
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务。
- 用户代码挂起当前任务,让出 CPU 时间。
- 硬件中断。
- 寄存器
线程的同步
- 在多线程环境中,可能会出现两个甚至多个线程试图同时访问一个有限的资源。必须对这种潜在的资源冲突进行预防。
- 解决方案:在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其上锁以后,其他线程比便不能在使用那个资源。除非被解锁。
- 在线程环境中,关于成员变量与局部变量;如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时候,他们对该成员变量是彼此影响到(也就是说一个线程对成员变量的改变会影响另一个到另一线程)。
- 不能依靠线程优先级来决定线程的执行。
- 同步到实现方式
- synchronized 关键字;当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
- Java中方锁。
- java中每个对象都有一个锁(lock)或者叫监视器(monitor),当访问一个对象synchronized方法时,表示将该对象上锁,此时其他任何线程在去访问该synchronized方法了,直到之前那个线程执行方法完毕后(或者抛出异常),那么该对象的锁释放掉。其他线程才有可能再去访问synchronized方法。
- 如果一个对象中有多个synchronized方法,某一时刻某个线程进入了该对象中的synchronized方法,那么在该方法没有执行完成之前或者抛出异常之前,其他线程是无法访问该对象的任何synchronized方法。
- 被synchronized保护的变量应该是private修饰的。
- 如果某个synchronized方法被static修饰的,那么当线程访问该方法时候,它锁定并不是对象(实例),而是synchronized方法所以在对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象都会对应唯一一个Class对象,因此当线程分别访问一个类的两个对象的两个static synchronized方法时,他们的执行顺序是顺序的,也就是说一个线程先执行方法,执行完毕后另一个线程才开始执行。
- synchronized方法是一个粗粒度的控制,某一个时刻只能有一个方法执行synchronized方法;sysnchronized块则是一种细粒度的控制方;只会将代码块同步。位于方法内、synchronized块之外的代码是可以被多个线程同时访问的。
线程如何工作的?
线程的状态在枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态:
- NEW,新建状态,线程被创建出来,但尚未启动时的线程状态;
- RUNNABLE,就绪状态,表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;
- BLOCKED,阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法;
- WAITING,等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如,一个线程调用了 Object.wait() 方法,那它就在等待另一个线程调用 Object.notify() 或 Object.notifyAll() 方法;
- TIMED_WAITING,计时等待状态,和等待状态(WAITING)类似,它只是多了超时时间,比如调用了有超时时间设置的方法 Object.wait(long timeout) 和 Thread.join(long timeout) 等这些方法时,它才会进入此状态;
- TERMINATED,终止状态,表示线程已经执行完成。
线程的执行流程
- 首先先要创建线程并指定线程需要执行的业务方法,然后再调用线程的 start() 方法,此时线程就从 NEW(新建)状态变成了 RUNNABLE(就绪)状态,此时线程会判断要执行的方法中有没有 synchronized 同步代码块,如果有并且其他线程也在使用此锁,那么线程就会变为 BLOCKED(阻塞等待)状态,当其他线程使用完此锁之后,线程会继续执行剩余的方法。当遇到 Object.wait() 或 Thread.join() 方法时,线程会变为 WAITING(等待状态)状态,如果是带了超时时间的等待方法,那么线程会进入 TIMED_WAITING(计时等待)状态,当有其他线程执行了 notify() 或 notifyAll() 方法之后,线程被唤醒继续执行剩余的业务方法,直到方法执行完成为止,此时整个线程的流程就执行完了
Java实现多线程的方式
有三种使用线程的方法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
1.实现 Runnable 接口
需要实现 run() 方法。
通过 Thread 调用 start() 方法来启动线程。
1 | public class MyRunnable implements Runnable { |
2.实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
1 | public class MyCallable implements Callable<Integer> { |
3.继承 Thread 类
同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
1 | public class MyThread extends Thread { |
4.Runnable VS Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
5.三种方式的区别
- 实现 Runnable 接口可以避免 Java 单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程区处理同一资源的情况。
- 继承 Thread 类和实现 Runnable 方法启动线程都是使用 start() 方法,然后 JVM 虚拟机将此线程放到就绪队列中,如果有处理机可用,则执行 run() 方法。
- 实现 Callable 接口要实现 call() 方法,并且线程执行完毕后会有返回值。其他的两种都是重写 run() 方法,没有返回值。
BLOCKED(阻塞等待)和 WAITING(等待)有什么区别?
- 虽然 BLOCKED 和 WAITING 都有等待的含义,但二者有着本质的区别,首先它们状态形成的调用方法不同,其次 BLOCKED 可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源;而 WAITING 则是因为自身调用了 Object.wait() 或着是 Thread.join() 又或者是 LockSupport.park() 而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒,比如当线程因为调用了 Object.wait() 而进入 WAITING 状态之后,则需要等待另一个线程执行 Object.notify() 或 Object.notifyAll() 才能被唤醒。
start() 方法和 run() 方法有什么区别?
Thread 源码来看,start() 方法属于 Thread 自身的方法,并且使用了 synchronized 来保证线程安全,源码如下
1 | public synchronized void start() { |
run() 方法为 Runnable 的抽象方法,必须由调用类重写此方法,重写的 run() 方法其实就是此线程要执行的业务方法,源码如下:
1 | public class Thread implements Runnable { |
从执行的效果来说,start() 方法可以开启多线程,让线程从 NEW 状态转换成 RUNNABLE 状态,而 run() 方法只是一个普通的方法。
其次,它们可调用的次数不同,start() 方法不能被多次调用,否则会抛出 java.lang.IllegalStateException;而 run() 方法可以进行多次调用,因为它只是一个普通的方法而已。
线程的优先级有什么用?该如何设置?
在 Thread 源码中和线程优先级相关的属性有 3 个:
1 | // 线程可以拥有的最小优先级 |
线程的优先级可以理解为线程抢占 CPU 时间片的概率,优先级越高的线程优先执行的概率就越大,但并不能保证优先级高的线程一定先执行。
在程序中我们可以通过 Thread.setPriority() 来设置优先级,setPriority() 源码如下:
1 | public final void setPriority(int newPriority) { |
sleep() 和wait()的区别
sleep是Thread的成员方法,睡眠时保持对象锁,仍然占有该锁。
- sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait是Object的成员方法睡眠时,释放对象锁。
- wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
两者最主要的区别在于:sleep ⽅法没有释放锁,⽽ wait ⽅法释放了锁 。
wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者notifyAll() ⽅法。sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout)超时后线程会⾃动苏醒。
wait只能在synchronize代码块中,sleep不需要。
为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤run() ⽅法?
- new ⼀个 Thread,线程进⼊了新建状态;调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏run() ⽅法的内容,这是真正的多线程⼯作。 ⽽直接执⾏ run() ⽅法,会把 run ⽅法当成⼀个 main线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
- 总结: 调⽤ start ⽅法⽅可启动线程并使线程进⼊就绪状态,⽽ run ⽅法只是 thread 的⼀个普通⽅法调⽤,还是在主线程⾥执⾏。
notify与notifyAll的区别
尽量使用notifyAll。
- 调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
- notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
- 两者最⼤的区别:
- notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,⼀旦该对象被解锁,他们就会去竞争。
- notify他只是选择⼀个wait状态线程进⾏通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第⼀个线程运⾏完毕以后释放对象上的锁,此时如果该对象没有再次使⽤notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出⼀个notify或notifyAll,它们等待的是被notify或notifyAll,⽽不是锁。
有三个线程T1,T2,T3,如何保证顺序执行?
在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个
线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用
T2,T2调用T1),这样T1就会先完成而T3最后完成。
1 | public class JoinTest2 { |
在多线程中,什么是上下文切换(context-switching)?
- 上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
- 多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
- 概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的 CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。
- Linux 相⽐与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少。
为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?
- Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法。
为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?
- 当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。
为什么Thread类的sleep()和yield()方法是静态的?
- Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
interrupted和isInterrupted⽅法的区别?
interrupted() :会将中断状态清除,Java多线程的中断机制是⽤内部标识来实现的,调⽤Thread.interrupt()来中断⼀个线程就会设置中断标识为true。当中断线程调⽤静态⽅法Thread.interrupted()来检查中断状态时,中断状态会被清零。
isInterruptedd : 不会将中断状态清除,⾮静态⽅法isInterrupted()⽤来查询其它线程的中断状态且不会改变中断状态标识。
任何抛出InterruptedException异常的⽅法都会将中断状态清零。⽆论如何,⼀个线程的中断状态有有可能被其它线程调⽤中断来改变。
synchronized关键字
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。
- synchronized作用于「方法」或者「代码块」,保证被修饰的代码在同一时间只能被一个线程访问。
- synchronized修饰代码块时,JVM采用「monitorenter、monitorexit」两个指令来实现同步
- synchronized修饰同步方法时,JVM采用「ACC_SYNCHRONIZED」标记符来实现同步
- monitorenter、monitorexit或者ACC_SYNCHRONIZED都是「基于Monitor实现」的
- 实例对象里有对象头,对象头里面有Mark Word,Mark Word指针指向了「monitor」
- Monitor其实是一种「同步工具」,也可以说是一种「同步机制」。
- 在Java虚拟机(HotSpot)中,Monitor是由「ObjectMonitor实现」的。ObjectMonitor体现出Monitor的工作原理~
1 | ObjectMonitor() { |
- synchronized 关键字加到 static 静态⽅法和 synchronized(class)代码块上都是是给 Class类上锁。
- synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
常用场景
同步一个代码块
1 | public void func() { |
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
1 | public class SynchronizedExample { |
对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
1 | public static void main(String[] args) { |
同步一个方法
1 | public synchronized void func () { |
它和同步代码块一样,作用于同一个对象。
同步一个类
1 | public void func() { |
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
1 | public class SynchronizedExample { |
同步一个静态方法
1 | public synchronized static void fun() { |
作用于整个类。
synchronized 关键字和 volatile 关键字的区别
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定⽐synchronized关键字要好。但是volatile关键字只能⽤于变量⽽synchronized关键字可以修饰⽅法以及代码块。synchronized关键字在JavaSE1.6之后进⾏了主要包括为了减少获得锁和释放锁带来的性能消耗⽽引⼊的偏向锁和轻量级锁以及其它各种优化之后执⾏效率有了显著提升,实际开发中使⽤synchronized 关键字的场景还是更多⼀些。
多线程访问volatile关键字不会发⽣阻塞,⽽synchronized关键字可能会发⽣阻塞。
volatile关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized关键字两者都能保证。
volatile关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽ synchronized关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
主要解决每个线程绑定自己的值,存储每个线程的私有变量。
- ThreadLocal用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择ThreadLocal变量。
- 每个线程都会拥有他们自己的Thread变量,它们可以使用get()set()方法去获取他们的默认值或者在线程内部改变他们的值。ThreadLocal实例通常是希望它们同线程状态关联起来是private static属性。
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
ThreadLocal为什么会内存泄漏
ThreadLocal为每一线程创建一块小的堆工作空间
- ThreadLocal中含有一个叫做ThreadLocalMap的内部类,该类为一个采用线性探测法实现的HashMap。它的key为ThreadLocal对象而且还使用了WeakReference,ThreadLocalMap正是用来存储变量副本的。
1 | /** |
- 如果ThreadLocal被设置为null后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。但是对应的value不会被回收掉直到线程结束才会被回收。如果当前线程一直处于运行中,那么这些Entry对象中的value就可能一直无法回收,就会发生内存泄露。为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove()来清理无用的Entry。
ThreadLocal与Synchronized
ThreadLocal模式与Synchronized关键字都用于处理多线程并发访问变量的问题,处理问题的角度和思路不同。
名称 | 原理 |
---|---|
ThreadLocal | 空间换时间,为每一份线程提供一份变量副本,完成线程私有变量访问,多线程之间数据互相隔离。 |
Synchronized | 时间换空间,只提供一份变量副本,让线程排队访问。多线程之间访问数据同步。 |
volatile关键字
volatile 关键字的主要作⽤就是保证变量的可⻅性然后还有⼀个作⽤是防⽌指令重排序。
特性
原⼦性 : ⼀个的操作或者多次操作,要么所有的操作全部都得到执⾏并且不会收到任何因素的⼲扰⽽中断,要么所有的操作都执⾏,要么都不执⾏。 synchronized 可以保证代码⽚段的原⼦性。
可⻅性 :当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新值。 volatile 关键字可以保证共享变量的可⻅性。
有序性 :代码在执⾏的过程中的先后顺序,Java 在编译器以及运⾏期间的优化,代码的执⾏顺序未必就是编写代码时候的顺序。 volatile 关键字可以禁⽌指令进⾏重排序优化。
内存屏蔽:加入volatile会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(也称内存栅栏)。
禁止重排序:volatile 禁止了指令重排。
使用场景
读写锁
- 如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronize同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。
状态位
用于做状态位标志,如果多个线程去需要根据一个状态位来执行一些操作,使用volatile修饰可以保证内存可见性。
用于单例模式用于保证内存可见性,以及防止指令重排序。
线程池
为避免线程频繁创建和销毁带来的性能问题,而池化的一种方案。
利用线程池管理并复用线程、控制最大并发数等。
实现任务线程队列缓存策略和拒绝机制。
实现某些与时间相关的功能,如定时执行、周期执行等。
隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池将较慢的交易服务与搜索服务隔离开,避免各服务线程相互影响。
这里直接参考阿里巴巴的手册。线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。
ExecutorService
阿里开发手册建议使用ThreadPoolExecutor 这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
- Executors.newSingleThreadPool()
- newFixedThreadPool()
- newcachedTheadPool()
- newScheduledThreadPool()
Executors返回的线程池对象的弊端
查看 Executors 的源码会发现,Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor() 和 Executors.newCachedThreadPool() 等方法的底层都是通过 ThreadPoolExecutor 实现的。
- FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
ThreadPoolExecutor的核心参数
1 | public ThreadPoolExecutor(int corePoolSize, |
- 第 1 个参数:corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程(创建和销毁的原因会在本课时的下半部分讲到);如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。
- 第 2 个参数:maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到。
- 第 3 个参数:keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
- 第 4 个参数:unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
- 第 5 个参数:workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
- 第 6 个参数:threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程。
- 第 7 个参数:RejectedExecutionHandler 表示指定c,当线程池的任务已经在缓存队列 workQueue 中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。
- 原理
- 如果当前池⼤⼩ poolSize < corePoolSize,则创建新线程执⾏任务。
- 如果当前池⼤⼩ poolSize > corePoolSize,且等待队列未满,则进⼊等待队列。
- 如果当前池⼤⼩ poolSize > corePoolSize 且⼩于 maximumPoolSize ,且等待队列已满,则创建新线程执⾏任务。
- 如果当前池⼤⼩ poolSize > corePoolSize 且⼤于 maximumPoolSize ,且等待队列已满,则调⽤拒绝策略来处理该任务。
- 线程池⾥的每个线程执⾏完任务后不会⽴刻退出,⽽是会去检查下等待队列⾥是否还有线程任务需要执⾏,如果在 keepAliveTime ⾥等不到新的任务了,那么线程就会退出。
ThreadPoolExecutor
3 个最重要的参数:corePoolSize
: 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
线程池任务执行的主要流程
execute() VS submit()
execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。
1 | ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10L, |
线程池的拒绝策略
当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。
Java 自带的拒绝策略有 4 种:
- AbortPolicy,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略。
- CallerRunsPolicy,把任务交给当前线程来执行。
- DiscardPolicy,忽略此任务(最新的任务)。
- DiscardOldestPolicy,忽略最早的任务(最先加入队列的任务)。
演示AbortPolicy
1 | ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10, |
结果
1 | pool-1-thread-1 |
- 第 6 个任务来的时候,线程池则执行了 AbortPolicy 拒绝策略,抛出了异常。因为队列最多存储 2 个任务,最大可以创建 3 个线程来执行任务(2+3=5),所以当第 6 个任务来的时候,此线程池就“忙”不过来了。
workQueue
BlockingQueue
ArrayBlockingQueue
⼀个由数组结构组成的有界阻塞队列。
- 用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。
LinkedBlockingQueue
⼀个由链表结构组成的有界阻塞队列。
- 基于链表的阻塞队列,同 ArrayListBlockingQueue 类似,此队列按照先进先出(FIFO)的原则对元素进行排序。而 LinkedBlockingQueue 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE)。
PriorityBlockingQueue
⼀个⽀持优先级排序的⽆界阻塞队列。
- 是一个支持优先级的无界队列。默认情况下元素采取自然顺序升序排列。可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
DelayQueue
⼀个使⽤优先级队列实现的⽆界阻塞队列。
- 是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
SynchronousQueue
⼀个不存储元素的阻塞队列。
- 是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用, SynchronousQueue 的吞吐量高于LinkedBlockingQueue 和ArrayBlockingQueue。
LinkedTransferQueue
⼀个由链表结构组成的⽆界阻塞队列。
- 是一个由链表结构组成的无界阻塞 TransferQueue 队列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
- transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
- tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。
- 对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回 true。
LinkedBlockingDeque
⼀个由链表结构组成的双向阻塞队列。
- 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
BlockingDeque
LinkedBlockingDeque
TransferQueue
如果你提交任务时,线程池队列已满。会时发会⽣什么?
- ⼀个任务不能被调度执⾏那么ThreadPoolExecutor.submit()⽅法将会抛出⼀个RejectedExecutionException异常。
shutdown()
与shutdownNow()
shutdown()
:关闭线程池,线程池的状态变为SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕。shutdownNow()
:关闭线程池,线程的状态变为STOP
。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
isTerminated()
与 isShutdown()
isShutDown
当调用shutdown()
方法后返回为 true。isTerminated
当调用shutdown()
方法后,并且所有提交的任务完成后返回为 true
如何合理的配置java线程池?
线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程。
如CPU密集型的任务,基本线程池应该配置多⼤?IO密集型的任务,基本线程池应该配置多⼤?⽤有界队列好还是⽆界队列好?任务⾮常多的时候,使⽤什么阻塞队列能获取最好的吞吐量?
- 配置线程池时CPU密集型任务可以少配置线程数,⼤概和机器的cpu核数相当,可以使得每个线程都在执⾏任务。
- IO密集型时,⼤部分线程都阻塞,故需要多配置线程数,2*cpu核数
- 有界队列和⽆界队列的配置需区分业务场景,⼀般情况下配置有界队列,在⼀些可能会有爆发性增⻓的情况下使⽤⽆界队列。
- 任务⾮常多时,使⽤⾮阻塞队列使⽤CAS操作替代锁可以获得好的吞吐量。
- 有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
- 如何判断是 CPU 密集任务还是 IO 密集任务?
- CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
计算公式
CPU密集任务
- 理论上
线程数量 = CPU 核数(逻辑)
就可以了,但是实际上,数量一般会设置为CPU 核数(逻辑)+ 1
- 理论上
IO密集任务
- 最佳线程数 =
(1/CPU利用率)
=1 + (I/O耗时/CPU耗时)
- 最佳线程数 =
CPU核心数
*(1/CPU利用率)
=CPU核心数
*(1 + (I/O耗时/CPU耗时))
- 最佳线程数 =
Lock
Lock对象也可以实现同步,在使用上更加方便。
Java多线程中,可以使用synchronized关键字来实现线程之间同步互斥,但是再jdk1.5以后增加了ReentrantLock类。并且功能更加强大、灵活。
锁名称 | 应用 |
---|---|
乐观锁 | CAS |
自旋锁 | CAS |
悲观锁 | synchronized、vector、hashtable |
可重入锁 | synchronized、Reentrantlock、Lock |
读写锁 | ReentrantReadWriteLock,CopyOnWriteArrayList、CopyOnWriteArraySet |
公平锁 | Reentrantlock(true) |
非公平锁 | synchronized、reentrantlock(false) |
共享锁 | ReentrantReadWriteLock中读锁 |
独占锁 | synchronized、vector、hashtable、ReentrantReadWriteLock中写锁 |
重量级锁 | synchronized |
互斥锁 | synchronized |
同步锁 | synchronized |
分段锁 | concurrentHashMap |
死锁 | 相互请求对方的资源 |
锁粗化 | 锁优化技术 |
锁消除 | 锁优化技术 |
轻量级锁 | 锁优化技术 |
偏向锁 | 锁优化技术 |
AQS-AbstractQueuedSynchronizer
AQS:AbstractQueuedSynchronizer 抽象队列同步器
内部实现了一个FIFO列队来管理锁,线程会首先尝试获取锁,如果失败。则将当前线程以及等待信息包装成一个node节点放入同步列队中柱塞,当有锁的线程释放锁的时候,则会唤醒列队中的等待的线程。
AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成abstract ,是因为独占模式下只用实现 tryAcquire-tryRelease ,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等)。
AQS定义类一个
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语。
CountDownLatch
1 CounttDownLatch 是 java.util.concurrent(JUC)中的锁,闭锁。利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
案例
1 |
|
输出
1 | 14:22:53.775 [main] INFO cn.z201.java.test.aqs.CountDownLatchTest - await |
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。和synchronized也是互斥锁,同时可以实现公平和非公平锁。
构造方法
1 |
|
案例
1 | public class LockExample { |
1 | public class ReentrantLockDemo { |
输出结果
1 | ThreadName Thread-0 number 0 |
- 当前的线程执行完成之后才能执行其他线程,但是其他线程执行的顺序是随机的。
ReentrantReadWriteLock
ReentrantReadWriteLock锁 实际也是继承了AQS类来实现锁的功能的
案例
1 |
|
输出
1 | 00:25:27.321 [main] INFO cn.z201.java.test.lock.ReentrantReadWriteLockTest - 写锁 |
Semaphore
java.util.concurrent(JUC) ,Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
案例
这里防止主线程终止,先睡个10s。
1 |
|
输出
1 | 14:10:08.873 [Thread-6] INFO cn.z201.java.test.aqs.SemaphoreTest - Thread-6 acquire |
CyclicBarrier
CyclicBarrier 是 java.util.concurrent(JUC)中的锁,栅栏锁。它可以实现让一组线程等待至某个状态之后再全部同时执行。
案例
1 |
|
输出
1 | 15:06:17.233 [pool-1-thread-4] INFO cn.z201.java.test.aqs.CyclicBarrierTest - await |
Condition
synchronized 关键字,它配合 Object 的 wait()、notify() 系列方法可以实现等待/通知模式。对于 Lock,通过 Condition 也可以实现等待/通知模式。Condition 是一个接口。Condition 接口的实现类是 Lock(AQS)中的 ConditionObject。Lock 接口中有个 newCondition() 方法,通过这个方法可以获得 Condition 对象(其实就是 ConditionObject )。因此,通过 Lock 对象可以获得 Condition 对象。
1 | Lock lock = new ReentrantLock(); |
CountDownLatch与CyclicBarrier
- CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待
- CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier
CAS Compare-And-Swap
CAS的全称是Compare-And-Swap,它是CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。其原理是利用
sun.misc.Unsafe.java
类通过JNI来调用硬件级别的原子操作来实现CAS(即CAS是借助C来调用CPU底层指令实现的)。
CAS = 比较并交换 + 乐观版本锁 + 锁自旋
CAS具备原子性,它是cpu硬件指令实现保证原子性。通过JNI调用Native方法由c++编写的硬件级别指令。JDK中通过Unsafe类执行操作。
- 非阻塞算法
- 非独占锁
ABA问题
CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
- 比如说一个线程 t1 从内存位置 V 中取出 A,这时候另一个线程 t2 也从内存中取出 A,并且t2 进行了一些操作变成了 B,然后 t2 又将 V 位置的数据变成 A,这时候线程 t1 进行 CAS 操作发现内存中仍然是 A,然后 t1 操作成功。此时CAS比较值相等,自旋成功。
- 部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
synchronized和ReentrantLock 的区别
两者都是可重⼊锁
- 两者都是可重⼊锁。“可重⼊锁”概念是:⾃⼰可以再次获取⾃⼰的内部锁。⽐如⼀个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重⼊的话,就会造成死锁。同⼀个线程每次获取锁,锁的计数器都⾃增1,所以要等到锁的计数器下降为0时才能释放锁。
synchronized 依赖于 JVM ⽽ ReentrantLock 依赖于 API
- synchronized 是依赖于 JVM 实现的,前⾯我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进⾏了很多优化,但是这些优化都是在虚拟机层⾯实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层⾯实现的(也就是 API 层⾯,需要 lock() 和 unlock() ⽅法配合try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
synchronized的方式加锁,会让线程在 BLOCKED 状态和 RUNNABLE 状态之间切换,在操作系统上,就会造成用户态和内核态的频繁切换,效率就比较低。
相⽐synchronized,ReentrantLock增加了⼀些⾼级功能
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
等待可中断
- ReentrantLock提供了⼀种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
实现公平锁
- ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是⾮公平的,可以通过 ReentrantLock类的 ReentrantLock(boolean fair) 构造⽅法来制定是否是公平的。
可实现选择性通知
- ReentrantLock类实现等待/通知机制,需要借助于Condition接⼝与newCondition() ⽅法。Condition是JDK1.5之后才有的,它具有很好的灵活性,⽐如可以实现多路通知功能也就是在⼀个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活。 在使⽤notify()/notifyAll()⽅法进⾏通知时,被通知的线程是由 JVM 选择的,⽤ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能⾮常重要,⽽且是Condition接⼝默认提供的。⽽synchronized关键字就相当于整个Lock对象中只有⼀个Condition实例,所有的线程都注册在它⼀个身上。如果执⾏notifyAll()⽅法的话就会通知所有处于等待状态的线程这样会造成很⼤的效率问题,⽽Condition实例的signalAll()⽅法 只会唤醒注册在该Condition实例中的所有等待线程。
公平锁/非公平锁
公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
- 公平锁的含义是线程需要按照请求的顺序来获得锁;而非公平锁则允许“插队”的情况存在,所谓的“插队”指的是,线程在发送请求的同时该锁的状态恰好变成了可用,那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。
- 而公平锁由于有挂起和恢复所以存在一定的开销,因此性能不如非公平锁,所以 ReentrantLock 和 synchronized 默认都是非公平锁的实现方式。
可重入锁
- 可重入锁又名递归锁,是指在同一个线程,在外层方法获取锁的时候,在进入内层方法会自动获取锁。
- 可重入锁的一个好处是可一定程度避免死锁。
共享锁/独占锁
只能被单线程持有的锁叫独占锁,可以被多线程持有的锁叫共享锁。
- 独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 synchronized 就是独占锁,而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。
- 独占锁可以理解为悲观锁,当每次访问资源时都要加上互斥锁,而共享锁可以理解为乐观锁,它放宽了加锁的条件,允许多线程同时访问该资源。
共享锁
是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。- 在 JDK 中
ReentrantReadWriteLock
就是一种共享锁。
- 在 JDK 中
互斥锁/读写锁
- 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
- 互斥锁在Java中的具体实现就是
ReentrantLock
- 读写锁在Java中的具体实现就是
ReadWriteLock
- 互斥锁在Java中的具体实现就是
分段锁
- 分段锁其实是一种锁的设计,并不是具体的一种锁,对于
ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
锁升级(无锁|偏向锁|轻量级锁|重量级锁)
无锁
状态其实就是上面讲的乐观锁。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01” (可偏向),即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
- 当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”,不可偏向)或 轻量级锁定(标志位为“00”)的状态,后续的同步操作就进入轻量级锁的流程。
进入到轻量级锁说明不止一个线程尝试获取锁,这个阶段会通过自适应自旋CAS方式获取锁。如果获取失败,则进行锁膨胀,进入重量级锁流程,线程阻塞。
重量级锁是通过系统的线程互斥锁来实现的,代价最昂贵
自旋锁
自旋锁
是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。- 如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。
互斥锁与自旋锁
- 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
- 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
- 对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。
- 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
- 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
- 自旋锁是通过 CPU 提供的
CAS
函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。 - 一般加锁的过程,包含两个步骤:
- 第一步,查看锁的状态,如果锁是空闲的,则执行第二步;
- 第二步,将锁设置为当前线程持有;
- CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
- 使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。
- 自旋锁是通过 CPU 提供的
锁优化技术(锁粗化|锁消除)
锁粗化
就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。锁消除
是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。
死锁(Deadlock)?如何分析和避免死锁?
- 死锁是指两个以上的线程永远阻塞的情况,这种情况产生至少需要两个以上的线程和两个以上的资源。
- 分析死锁,我们需要查看Java应用程序的线程转储。我们需要找出那些状态为BLOCKED的线程和他们等待的资源。每个资源都有一个唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁。
- 避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。
案例
1 |
|
如何避免线程死锁?
破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 :⼀次性申请所有的资源。
破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
Java中常见的线程同步方式
- Object中wait\notify\notifyAll完成对monitor之间的同步。
- synchronized对方法\对象代码块同步。
- volatile关键字修饰字段实现变量的可见性。
- atomic自增自减。
- ThreadLoacal线程局部变量。
- juc包下工具类\LinkedBlockingQueue。
- Thread中join\await方法实现任务顺序执行。
相关规范
- JVM规范
- Java语言规范、Java虚拟机规范、Java内存模型规范。
- JSR规范。
- IETF规范。