Skip to content

Latest commit

 

History

History
1185 lines (518 loc) · 44.9 KB

core-java-1-10th.md

File metadata and controls

1185 lines (518 loc) · 44.9 KB

《Java 核心技术 卷 1 基础知识 第十版》笔记

本文 GitHub 地址:https://github.com/AlanCheen/FullStackNotes

作者公众号:程序亦非猿,关注后回复「Java核心技术」即可获得 PDF 版本笔记


[TOC]

Core Java Volume 1-Fundamentals,基于 Java SE 8 更新。

一本值得每个学 Java 的同学学习的书。

该笔记是「程序亦非猿」所做,并且只是按个人的侧重点记录的笔记(感觉第四、六、八、九、十四章比较重要),所以内容相比原书会少很多,想看更多请购买原书。

第1 章 Java 程序设计概述

  • 简单性
  • 面向对象
  • 分布式
  • 健壮性
  • 安全性
  • 体系结构中立
  • 可移植性
  • 解释型
  • 高性能
  • 多线程
  • 动态性

第 3 章 Java 的基本程序设计结构

3.3 数据类型

8 种基本类型(primitive type),其中 4 种整形,2 种浮点类型、char、boolean。

类型 存储(字节) 取值范围
int 4
short 2
long 8
byte 1 -128~127
float 4
doubule 8
char
boolean

3.6 字符串

字符串 String,不可变。

3.10 数组

数组是一种数据结构,用来存储同一类型值的集合。通过一个整形下标可以访问数组中的每一个值。

在声明数组变量时,需要指出数组类型(数组元素类型紧跟[])和数组变量的名字 。如:int[]aint[]b = new int[]{1,2,3,4}

  1. foreach 循环语句的循环变量将会遍历数组中的每个元素,而不需要使用下标值;更加简洁、更不容易出错。
  2. Arrays.copy 来处理数组拷贝
  3. Arrays.sort来排序,它用的是优化过的快速排序算法

第 4 章 对象与类

传统的结构化程序设计通过设计一系列的过程 (即算法)来求解问题。 一旦确定了这些 过程, 就要开始考虑存储数据的方式。 这就是 Pascal 语言的设计者 Niklaus Wirth 将其著作命 名为《算法 + 数据结构 = 程序》( Algorithms + Data Structures = Programs, Prentice Hall, 1975 ) 的原因。 需要注意的是, 在 Wirth 命名的书名中, 算法是第一位的, 数据结构是第二位的,这就明确地表述了程序员的工作方式。 首先要确定如何操作数据, 然后再决定如何组织数 据, 以便于数据操作。 而 OOP 却调换了这个次序, 将数据放在第一位, 然后再考虑操作数 据的算法。

4.1.2类

类是构造对象的模板或蓝图。

对象中的数据成为实例域(instance field),操纵数据的过程称为方法(method)。

实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。仅通过方法与数据进行交互。

4.1.3对象

对象的三个主要特性:

  1. 对象的行为
  2. 对象的状态
  3. 对象的标识

4.1.4类之间的关系

最常见的关系有:

  1. 依赖,use-a,表示类 A 用到了 B,就可以说 A 依赖 B,比如 A 调用了 B 的方法;
  2. 聚合,has-a,表示类 A 的对象包含了 类B的对象;
  3. 继承,is-a

4.8 类路径

java -classpath 命令来设置类路径

或者通过设置 CLASSPATH 环境变量来完成。

4.9 文档注释

  1. @author
  2. @version
  3. @since
  4. @deprecated
  5. @see

注释文档抽取:

javadoc -d docDirectory nameOfPackage

4.10 类设计技巧

  1. 一定要保证数据私有,绝对不要破坏封装性。
  2. 一定要对数据初始化
  3. 不要在类中使用过多的基本类型,用类来替代多个相关的基本类型的使用会更加易于理解且易于修改;
  4. 不是所有的域都需要独立的域访问器和域更改器;
  5. 将职责过多的类进行分解;
  6. 类名和方法名要能够体现它们的职责;
  7. 优先使用不可变的类;

第 5 章 继承

5.7 反射

反射能够动态操纵 Java 代码的程序。

能够分析类能力的程序称为反射(reflective),反射机制的功能极其强大。

反射机制可以用来:

  • 在运行时分析类的能力;
  • 在运行时查看对象;
  • 实现通用的数组操作代码;
  • 利用 Method 对象,这个对象很像 C++ 中的函数指针;
Class 类

在运行期间 Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。

虚拟机为每个类型管理一个 Class 对象。因此可以利用==运算符实现两个类对象比较的操作。

class.newlnstance()创建新实例。

  • Field,描述类的域,
  • Method,类的方法,
  • Constructor,类的构造器,

第 6 章 接口、lambda 表达式与内部类

6.1 接口

接口(interface) 技术,这种技术主要用来描述类具有什么功能,而并不 给出每个功能的具体实现。一个类可以实多个接口。

接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

接口不能实例化。

在Java8 中允许在接口中增加静态方法,还可以为接口提供默认方法,用default修饰符标记。

接口与回调

回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。例如在按钮点击时应该采取什么行动。

6.3 lambda 表达式

lambda 表达式采用一种简介的语法定义代码块。

lambda 表达式是一个可以传递的代码块,可以再以后执行一次或多次。

语法:参数,箭头,表达式,比如:()->{}

在 Java 里传递代码块并不容易,因为 Java 面向对象,只能构造对象来传递并调用方法。

不需要指定 lambda 的返回类型,它可以自动推导得出。

函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式。这种接口称为函数式接口(functional interface)。

方法引用(::

从这些例子可以看出, 要用:: 操作符分隔方法名与对象或类名。 主要有 3 种情况:

  • object::instanceMethod
  • Class ::static Method
  • Class ::instanceMethod

表达式 System.out::println 是一个方法引用(method reference), 它等价于 lambda 表达式x 一> System.out.println(x)

构造器引用

构造器引用与方法引用很类似, 只不过方法名为 new。 例如, Person::new 是 Person 构造 器的一个引用。

变量作用域

lambda 表达式有 3 个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值(非参数而且不在代码中定义的变量)

注释:关于代码块以及自由变量值有一个术语:闭包(closure)。

lambda表达式可以捕获外围作用域中变量的值,并且它是不不会改变的变量。

lambda 表达式中捕获的变量必须实际上是最终变量 ( effectivelyfinal 。)实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。

lambda 表达式的体与嵌套块有相同的作用域。 这里同样适用命名冲突和遮蔽的有关规 则。 在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

在一个 lambda 表达式中使用 this 关键字时, 是指创建这个 lambda 表达式的方法的 this参数。例如在类 Foo 里创建了一个 lambda 表达式,那么在这个表达式里的 this 指的是 Foo 这个类。

使用 lambda 表达式的重点是延迟执行 deferred execution )

6.4 内部类

内部类 (inner class)是定义在另外一个类中的类。

为什么需要使用内部类?主要原因有三:

  1. 内部类方法可以访问该类定义所在的作用中的数据,包含私有的数据(内部类可以访问外部类(outer class)的数据);
  2. 内部类可以对同一个包中的其他类隐藏起来;
  3. 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷;

内部类是一种编译器现象, 与虚拟机无关。编译器将会把内部类翻译成用 $ ( 美元符号)分隔外部类名与内部类名的常规类文件, 而虚拟机则对此一无所知

局部内部类,在方法里定义的类。

匿名内部类

通常的语法格式为:

new SuperType(construction parameters){

inner class methods and data

}

匿名类不能有构造器

如果构造参数的闭小括号后面跟一个开大括号, 正在定义的就是匿名内部类。

双括号初始化”(double brace initialization):

new ArrayList<String>(){{add("A");add("B")}}
静态内部类

有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部, 并不需要内部类引用 外围类对象。为此,可以将内部类声明为static, 以便取消产生的引用。

注释: 在内部类不需要访问外围类对象的时候, 应该使用静态内部类。 有些程序员用嵌套类(nestedclass) 表示静态内部类。

注释: 与常规内部类不同, 静态内部类可以有静态域和方法。

注释: 声明在接口中的内部类自动成为 static 和 public 类。

6.5 代理

这里的代理指的是:java.lang.reflect.Proxy

利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。

何时使用代理

在编译时我们无法知道一个接口的确切类型,如果想要构造一个实现这个接口的类就做不到,需要在运行时定义一个新类。代理类可以在运行时创建全新的类。

例如:有系统的接口,我们在编写代码的时候没法直接用,这个时候就可以使用 Proxy 来处理。

创建代理
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
  • ClassLoader
  • Class<?>[]
  • InvocationHandler
代理类的特性

代理类是在程序运行过程中创建的,不过一旦被创建就变成了常规类,跟虚拟机中的任何其他类没有什么区别。

第 7 章 异常、断言和日志

所有异常都是由 Throwable 继承而来,但是分 Error 和 Exception。

7.2.4 finally

finally 子句不管是否有异常被捕获,finally 中的代码都会被执行

当 finally 句中包含 return 语句时,将会出现意想不到的结果。

假设利用 return 语句从 try 语句块中退出。在方法返回前,finally 语句中的内容将被执行。如果 finally 句中也有一个 return 语句,这个返回值将会覆盖原始的返回值

public static int f(int n) {
try {
	int r = n * n;
	return r; 
} finally{
	if (n = 2) return 0; }
}

如果调用 f(2), 那么 try 语句块的计算结果为 r = 4, 并执行 return 语 句 然 而, 在方法真 正返回前,还要执行finally子句。finally子句将使得方法返回0, 这个返回值覆盖了原 始的返回值 4。

7.2.5 带资源的 try 语句

try-catch-with-res 是 Java7 的特性。

注释: 带资源的 try 语句自身也可以有 catch 子句和一个 finally 子句。 这些子句会在 关闭资源之后执行。 不过在实际中, 一个 try语句中加入这么多内容可能不是一个好 主 意。

7.3 使用断言

断言机制运行在测试期间向代码中插入一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。

Java 引入了关键字 assert,使用方法有两种:

  • assert 条件
  • assert 条件:表达式

如果条件为 false,则会抛出一个 AssertionError 异常。

默认情况下断言是被禁用的,是利用 ClassLoader 来实现的。

第 8 章 泛型程序设计

使用泛型机制编写的程序代码要比那些杂乱地使用Object 变量, 然后再进行强制类型转换的代码具有更好的安全性和可读性。

8.1 为什么要使用泛型程序设计

泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。 例如, 我们并不希望为聚集 String 和 File 对象分别设计不同的类。

8.5 泛型代码和虚拟机

虚拟机没有泛型类型对象—所有对象都属于普通类。

8.5.1 类型擦除

无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型(raw type)。原始类型 的名字就是删去类型参数后的泛型类型名。 擦除 ( erased ) 类型变量 , 并替换为限定类型 (无限定的变量用 Object 。)

注释: 读者可能想要知道切换限定: class Interval<T extends Serializable & Comparable>会发生什么。如果这样做,原始类型用Serializable替换T, 而编译器在必要时要向Comparable 插入强制类型转换。 为了提高效率, 应该将标签(tagging) 接口 (即没有方法的接口)放在边界列表的末尾。

8.8 通配符类型

Pair<? extends Foo>表示它的类型参数是 Foo 的子类(限定子类)。

? super Foo,限定超类。

Pair<?>,没有限定。

8.9 反射和泛型

为了表达泛型类型声明, 使用 java.lang.reflect 包中提供的接口 Type。 这个接口包含下列子类型:

  • Class 类, 描述具体类型。
  • TypeVariable 接口, 描述类型变量(如 T extends Comparable<? super T> )
  • WildcardType 接口,描述通配符(如?super T)
  • ParameterizedType 接口,描述泛型类或 接口类型(如 Comparable<? super T>)
  • GenericArrayType 接口,描述泛型数组(如 T[])

第 9 章 集合

9.1

集合类将接口与实现分离。

队列,先进先出,通常有两种实现方式:

  1. 一种是使用循环数组,效率比链表更高,但是是个有界集合,容量有限;
  2. 另一种是使用链表,效率相对低,但容量无限。

Iterator.next 方法跟 remove 方法相互依赖,如果没有调用 next 就调用 remove 则会报错,也就是说调用 remove前必须调用 next,并且 remove 移除的就是上一次调用 next 方法时返回的元素。

image-20190725201419186

image-20190816192450962

9.2 具体的集合

省略了线程安全的集合:

  • ArrayList ,一种可以动态增长和缩减的索引序列
  • LinkedList ,一种可以在任何位置进行高效地插人和删除操作的有序序列
  • ArrayDeque ,一种用循环数组实现的双端队列
  • HashSet,一种没有重复元素的无序集合
  • TreeSet ,一种有序集
  • EnumSet ,一种包含枚举类型值的集
  • LinkedHashSet ,一种可以记住元素插人次序的集
  • PriorityQueue ,一种允许高效删除最小元素的集合
  • HashMap ,一种存储键 / 值关联的数据结构
  • TreeMap ,一种键值有序排列的映射表
  • EnumMap , 一种键值属于枚举类型的映射表
  • LinkedHashMap ,一种可以记住键 / 值项添加次序的映射表
  • WeakHashMap ,一种其值无用武之地后可以被垃圾回收器回收的映射表
  • IdentityHashMap,一种用 == 而不是用 equals 比较键值的映射表

image-20190816193114705

9.2.1 链表

数组和数组列表从中间位置删除一个元素要付出很大代价,因为后续的所有元素都需要往前移动,类似的插入操作则往后移动。这是个很大的缺点。

而链表则可以解决这个问题。

在 Java 程序设计语言中, 所有链表实际上都是双向链接的(doubly linked) —即每个结点还存放着指向前驱结点的引用。

image-20190816194353502

链表与泛型集合之间有一个重要的区别。链表是一个有序集合(orderedcollection), 每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部

9.2.3 散列表

数组和链表可以按照意愿排列元素的次序。

散列表(hash table),无序的,可以快速地查找所需要的对象。

散列表为每个对象计算一个整数,称为散列码(hash code)。

在 Java 中,散列表用链表数组实现,每个列表称为(bucket),要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,得到的结果就是保存这个元素的桶的索引。

image-20190817210520933

有时候会被遇到桶被占满的情况,这种现象被称为散列冲突(hash collision)。这时需要用新对象跟桶中所有对象进行比较,查看对象是否已经存在。

如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。

注释: 在 JavaSE 8 中, 桶满时会从链表变为平衡二叉树。如果选择的散列函数不当, 会 产生很多冲突, 或者如果有恶意代码试图在散列表中填充多个有相同散列码的值, 这样 就 能 提 高 性 能。

如果散列表太满,就需要再散列(rehashed),这需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。(耗性能)

如果大概知道最终会有多少个元素,可以设置桶数,通常将桶数设置为预计元素个数的 75%~150%。

装填因子(load factor)决定何时对散列表进行再散列。默认情况下,装填因子是 0.75,这意味着表中超过 75%的位置已经填入元素的话,就会进行再散列。

9.2.4 树集(TreeSet)

树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动按照排序后的顺序呈现。

TreeSet 排序是用树结构完成,到 Java8实现的是红黑树(red-black tree)。

每次讲一个元素添加到树中时,都被放置在正确的排序位置上。

添加一个元素到树中要比添加到散列表中慢。如果树中包含 n 个元素, 査找新元素的正确位置平 均需要 l0g2 n 次比较。

TreeSet、SortedSet、NavigableSet

9.2.5 队列与双端队列

队列可以让人们有效地在尾部添加一个元素, 在头部删除一个元素。有两个端头的队列, 即双端队列, 可以让人们有效地在头部和尾部同时添加或删除元素。

在 Java SE 6 中引人了 Deque 接口, 并由 ArrayDeque 和 LinkedList 类实现。这两个类都提供了双端队列, 而且在必要时可以增加队列的长度。

9.2.6 优先级队列

优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。

也就是说, 无论何时调用 remove 方法, 总会获得当前优先级队列中最小的元素。然而, 优先级队列并没有对所有的元素进行排序。

优先级队列使用了一个优雅且高效的数据结构, 称为(heap)。堆是一 个可以自我调整的二叉树, 对树执行添加(add) 和删除(remore) 操作, 可以让最小的元素 移动到根, 而不必花费时间对元素进行排序。

9.3 映射

映射(map)数据结构用来存放键/值对。

Java 类库为映射提供了两个通用的实现: HashMapTreeMap。这两个类都实现了 Map 接口。

散列映射对键进行散列, 树映射用键的整体顺序对元素进行排序, 并将其组织成搜索树。 散列或比较函数只能作用于键。 与键关联的值不能进行散列或比较。

应该选择散列映射还是树映射呢? 与集一样, 散列稍微快一些, 如果不需要按照排列顺序访问键, 就最好选择散列。

9.3.5 链接散列集与映射

LinkedHashSet 和 LinkedHashMap 类用来记住插人元素项的顺序。这样就可以避免在散歹IJ表 中的项从表面上看是随机排列的。

image-20190818002148871

EmimSet、IdentityHashMap、WeakHashMap。。。。

第 14 章 并发

并发执行的进程数目并不是由 CPU 数目制约的。操作系统将 CPU 的时间片 分配给每一个进程, 给人并行处理的感觉。

多线程程序在较低的层次上扩展了多任务的概念: 一个程序同时执行多个任务。通常,每一个任务称为一个线程(thread), 它是线程控制的简称。可以同时运行一个以上线程的程 序称为多线程程序 (multithreaded )。

多进程与多线程有哪些区别呢? 本质的区别在于每个进程拥有自己的一整套变量, 而线程则共享数据

警告: 不要调用 Thread 类或 Runnable 对象的 run 方法。 直接调用 run 方法, 只会执行同一个线程中的任务, 而不会启动新线程。应该调用 Thread.start 方法。这个方法将创建一个执行 ran 方法的新线程。

14.2 中断线程

没有可以强制线程终止的方法。 然而, interrupt 方法可以用来请求终止线程。

当对一个线程调用 interrupt 方法时, 线程的中断状态将被置位。这是每一个线程都具有 的 boolean 标志。 每个线程都应该不时地检査这个标志, 以判断线程是否被中断。

  • void interrupt(),向线程发送中断请求。线程的中断状态将被设置为 true。如果目前该线程被一个 sleep 调用阻塞, 那么, InterruptedException 异常被抛出。

  • static boolean interrupted(),测试当前线程(即正在执行这一命令的线程)是否被中断。注意, 这是一个静态方法。这一调用会产生副作用—它将当前线程的中断状态重置为 false。

  • boolean islnterrupted(),测试线程是否被终止。不像静态的中断方法, 这一调用不改变线程的中断状态。

14.3 线程状态

线程可以有如下 6 种状态:

  • New(新创建),用 new 创建线程时还处于新建状态,如 new Thread
  • Runnable(可运行),一旦调用 start 方法,线程就处于 runnable 状态,一个可运行的线程可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。(Java 的规范说明没有将它作为一个单独状态。 一个正在运行中的线程仍然处于可运行状态。)
  • Blocked(被阻塞),
  • Waiting(等待),
  • Timed waiting(计时等待),
  • Terminated(被终止),

要确定一个线程当前的状态,可以调用getState方法。

14.3.1 新创建线程

用 new 操作符创建一个新线程时, 如 newThread(r), 该线程还没有开始运行。这意味着它的状态是 new。 当一个线程处于新创建状态时, 程序还没有开始运行线程中的代码。 在线程运行之前还有一些基础工作要做。

14.3.2 可运行线程

一旦调用 start 方法, 线程处于 runnable 状态。一个可运行的线程可能正在运行也可能没有运行, 这取决于操作系统给线程提供运行的时间。(Java 的规范说明没有将它作为一个单独状态。 一个正在运行中的线程仍然处于可运行状态。)

一旦一个线程开始运行, 它不必始终保持运行。事实上, 运行中的线程被中断, 目的是为了让其他线程获得运行机会。 线程调度的细节依赖于操作系统提供的服务。 抢占式调度系 统给每一个可运行线程一个时间片来执行任务。当时间片用完, 操作系统剥夺该线程的运行权, 并给另一个线程运行机会。

记住, 在任何给定时刻, 一个可运行的线程可能正在运行也可能没有运行(这就是为什 么将这个状态称为可运行而不是运行 。)

14.3.3 被阻塞线程和等待线程

当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。

  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时, 它自己进入等待状态。在调用 Object.wait 方法或 Thread.join 方法, 或者是等待 java,util.concurrent 库中的 Lock 或 Condition 时, 就会出现这种情况。实际上, 被阻塞状态与等待状态是有很大不同的。
  • 有几个方法有一个超时参数。 调用它们导致线程进人计时等待(timed waiting) 状 态。 这一状态将一直保持到超时期满或者接收到适当的通知。 带有超时参数的方法有Thread.sleep 和 Object.wait、Thread.join、Lock,tryLock 以及 Condition.await 的计时版。
14.3.4 被终止的线程

线程因如下两个原因之一而被终止:

  • 因为run 方法正常退出而自然死亡;
  • 因为一个没有捕获的异常终止了 run 方法二意外死亡。

特别是, 可以调用线程的 stop 方法杀死一个线程。 该方法抛出 ThreadDeath 错误对象 ,

由此杀死线程。但是, stop方法已过时,不要在自己的代码中调用这个方法。

image-20190819172415097

14.4 线程属性

线程优先级、守护线程、线程组以及处理未捕获异常的处理器。

14.4.1 线程优先级

在Java 中每一个线程都有一个线程优先级。默认情况下一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低优先级。

可以将优先级设 置为在 MIN_PRIORITY ( 在 Thread 类中定义为 1 ) 与 MAX_PRIORITY ( 定义为 10 ) 之间的 任何值。NORM_PRIORITY 被定义为 5。

每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。 但是, 线程 优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时, Java 线程的优 先级被映射到宿主机平台的优先级上, 优先级个数也许更多, 也许更少。

不要将程序构建为功能的正确性依赖于优先级。(优先级不靠谱,不要依赖)

14.4.2 守护线程

可以通过调用t .setDaemon(true) ;

将线程转换为守护线程(daemon thread。) 这样一个线程没有什么神奇。守护线程的唯一用途 是为其他线程提供服务。计时线程就是一个例子, 它定时地发送“ 计时器嘀嗒” 信号给其他 线程或清空过时的高速缓存项的线程。当只剩下守护线程时, 虚拟机就退出了,由于如果只 剩下守护线程, 就没必要继续运行程序了。

守护线程有时会被初学者错误地使用,他们不打算考虑关机(shutdown) 动作。但是,这是很危险的。守护线程应该永远不去访问固有资源, 如文件、 数据库, 因为它会在任何时 候甚至在一个操作的中间发生中断。

14.4.3 未捕获异常处理器

线程的 run 方法不能抛出任何受查异常, 但是, 非受査异常会导致线程终止。 在这种情 况下, 线程就死亡了。

但是, 不需要任何 catch 子句来处理可以被传播的异常。 相反, 就在线程死亡之前, 异常被传递到一个用于未捕获异常的处理器。

该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。 这个接口只有—个方法。

void uncaughtException(Thread t, Throwable e)

可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。 也可以用 Thread类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。 替换处理器可以使用日志 API 发送未捕获异常的报告到日志文件。

如果不安装默认的处理器, 默认的处理器为空。 但是, 如果不为独立的线程安装处理器, 此时的处理器就是该线程的 ThreadGroup 对象。

14.5 同步

当多个线程需要共享对同一数据的存取,根据各线程访问数据的次序,可能会产生冲突。这种情况通常被称为 竞争条件(race condition)。

假定两个线程同时执行指令

accounts[to] += amount; 问题在于这不是原子操作。 该指令可能被处理如下:

1 ) 将 accounts[to] 加载到寄存器。 2) 增加amoun。t 3 ) 将结果写回 accounts[to]。

现在, 假定第 1 个线程执行步骤 1 和 2, 然后, 它被剥夺了运行权。假定第 2 个线程被唤醒并修改了 accounts 数组中的同一项。然后, 第 1 个线程被唤醒并完成其第 3 步。i个线程所做的更新 于是 总金额不再正确。

14.5.3 锁对象 ReentrantLock

有两种机制防止代码块受并发访问的干扰。 Java 语言提供一个 synchronized 关键字达 到这一目的, 并且 Java SE 5.0 引入了 ReentrantLock 类。

myLock.lockO; // a ReentrantLock object try
{
critical section
}
finally
{
myLock.unlockO;// make sure the lock is unlocked even if an exception is thrown }

这一结构确保任何时刻只有一个线程进人临界区。一旦一个线程封锁了锁对象, 其他任 何线程都无法通过 lock 语句。 当其他线程调用 lock 时, 它们被阻塞, 直到第一个线程释放 锁对象。

一个公平锁偏爱等待时间最长的线程。但是, 这一公平 的保证将大大降低性能。 所以, 默认情况下, 锁没有被强制为公平的。

14.5.4 条件对象 Condition
Lock lock = new ReentrantLock();
Condition co = lock.newCondition();

注意调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞, 以便这些线程可以在当前线程退出同步方法之后, 通过竞争实现对对象的访问。

警告: 当一个线程拥有某个条件的锁时, 它仅仅可以在该条件上调用 awai、t signalAll 或signal 方法。

14.5.5 synchronized 关键字

总结一下 有关锁和条件的关键之处:

  • 锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程。
  • 锁可以拥有一个或多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

从 1.0 版开始, Java中的每一个对象都有一个内部锁。 如果一个方法用 synchronized 关键字声明, 那么对象的锁将保护整个方法。也就是说, 要调用该方法, 线程必须获得内部的对象锁。

14.5.6 同步阻塞

线程可以通过调用同步方法获得锁,还有另外一种机制可以获得锁,通过进入一个同步阻塞。

synchronized(obj){
  
}

它就会获得 obj 的锁。

有时程序员使用一个对象的锁来实现额外的原子操作, 实际上称为客户端锁定(client-

side locking )

14.5.7 监视器概念

锁和条件是线程同步的强大工具,但是严格讲它们不是面向对象的。多年来, 研究人员努力寻找一种方法, 可以在不需要程序员考虑如何加锁的情况下, 就可以保证多线程 的安全性。最成功的解决方案之一是监视器(monitor), 这一概念最早是由 PerBrinchHansen和 Tony Hoare 在 20 世纪 70 年代提出的。 用 Java 的术语来讲, 监视器具有如下特性:

  • 监视器是只包含私有域的类。
  • 每个监视器类的对象有一个相关的锁。
  • 使用该锁对所有的方法进行加锁。换句话说, 如果客户端调用 obj.meth0d(), 那么 obj 对象的锁是在方法调用开始时自动获得, 并且当方法返回时自动释放该锁。 因为所有的域是私有的, 这样的安排可以确保一个线程在对对象操作时, 没有其他线程能访问该域。
  • 该锁可以有任意多个相关条件。
14.5.8 Volatile 域

有时, 仅仅为了读写一个或两个实例域就使用同步, 显得开销过大了。 毕竟, 什么地方能出错呢? 遗憾的是, 使用现代的处理器与编译器, 出错的可能性很大。

  • 多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。 结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值

  • 编译器可以改变指令执行的顺序以使吞吐量最大化。 这种顺序上的变化不会改变代码语义, 但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变!

**注释:**Brian Goetz给出了下述 “同步格言”:“如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取, 或者, 从一个变量读值, 而这个变量可能是之前被另一个线程写入的, 此时必须使用同步”。

volatile 关键字为实例域的同步访问提供了一种免锁机制。 如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的

警告: Volatile 变量不能提供原子性。例如, 方法public void flipDoneO { done = !done; } // not atomic

不能确保翻转域中的值。 不能保证读取、 翻转和写入不被中断。

14.5.9 final 变量

还有一种情况可以安全地访问一个共享域, 即这个域声明为 final 时。

考虑以下声明:final Map<String, Double〉 accounts = new HashKap<>();

其他线程会在构造函数完成构造之后才看到这个 accounts 变量。

14.5.10 原子性

AtomicXXX 类 compareAndSet

如果有大量线程要访问相同的原子值, 性能会大幅下降, 因为乐观更新需要太多次重 试。

14.5.11 死锁(deadlock)

两个线程互相等待,导致死锁。

14.5.12 线程局部变量

线程j间共享变量,要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。

14.5.13 锁测试与超时

myLock.tryLock(100,TimeUnit.MILLSECONDS)

14.5.14 读 / 写锁

我们已经讨论的ReentrantLock类和ReentrantReadWriteLock 类。 如果很多线程从一个数据结构读取数据而很少线程修改其中数 据的话, 后者是十分有用的。

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//得到一个可以被多个读操作共用的读锁, 但会排斥所有写操作。
private Lock readLock = rwl.readLock();
//得到一个写锁, 排斥所有其他的读操作和写操作。
private Lock writeLock = rwl.writeLock();
private int value = 1;
int get() {
    readLock.lock();
    try {
    } finally {
        readLock.unlock();
    }
    return value;
}
int write(int newV){
    writeLock.lock();
    try {
        value = newV;
    } finally {
        writeLock.unlock();
    }
    return value;
}
14.5.15 为什么弃用 stop 和 suspend 方法

stop、 suspend 和 resume 方法已经弃用。 stop 方法天生就不安全, 经验证明 suspend 方法 会经常导致死锁。

14.6 阻塞队列(blocking queue)

对于许多线程问题, 可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生 产者线程向队列插人元素, 消费者线程则取出它们。 使用队列, 可以安全地从一个线程向另 一个线程传递数据。

当试图向队列添加元素而队列已满, 或是想从队列移出元素而队列为空的时候, 阻塞队列(blocking queue ) 导致线程阻塞。 在协调多个线程之间的合作时, 阻塞队列是一个有用的 工具。 工作者线程可以周期性地将中间结果存储在阻塞队列中。 其他的工作者线程移出中间 结果并进一步加以修改。 队列会自动地平衡负载。 如果第一个线程集运行得比第二个慢, 第 二个线程集在等待结果时会阻塞。 如果第一个线程集运行得快, 它将等待第二个队列集赶上来。

image-20190820111855556

  • LinkedBlockingQueue,无界,可以指定最大容量,双端。

  • ArrayBlockingQueue,在构造时需要指定容量,并且可选指定是否需要公平性。

  • PriorityBlockingQueue,是一个带优先级的队列,而不是先进先出队列,容量无上限。

  • DelayQueue,

  • LinkedTransferQueue,

14.7 线程安全的集合

java.util.concurrent 包提供了映射、 有序集和队列的高效实现: ConcurrentHashMapConcurrentSkipListMap > ConcurrentSkipListSetConcurrentLinkedQueue

注释: 散列映射将有相同散列码的所有条目放在同一个“ 桶” 中。有些应用使用的散列函数不当, 以至于所有条目最后都放在很少的桶中, 这会严重降低性能。 即使是一般意义上 还算合理的散列函数,如String类的散列函数,也可能存在问题。例如,攻击者可能会制 造大量有相同散列值的字符串, 让程序速度减慢。 在 JavaSE 8 中, 并发散列映射将桶组织 为树, 而不是列表, 键类型实现了 Comparable, 从而可以保证性能为 0(log(8)。)

假设你想要的是一个大的线程安全的集而不是映射。 并没有一个 ConcurrentHashSet 类,Set<String> words = ConcurrentHashMap.<String>newKeySet();

CopyOnWriteArrayList 和 CopyOnWriteArraySet 是线程安全的集合, 其中所有的修改线 程对底层数组进行复制。

任何集合类都可以通过使用同步包装器(synchronization wrapper) 变成线程安全的:

List<E> synchArrayList = Collections,synchronizedList(new ArrayList<E>()); Map<K, V> synchHashMap = Col1ections.synchronizedMap(new HashMap<K, V>0);

14.8 Callable 和 Future

Runnable 封装一个异步运行的任务, 可以把它想象成为一个没有参数和返回值的异步方 法。Callable 与 Runnable 类似, 但是有返回值。Callable 接口是一个参数化的类型, 只有一 个方法 call。

Future 保存异步计算的结果。可以启动一个计算, 将 Future 对象交给某个线程, 然后忘 掉它。Future 对象的所有者在结果计算好之后就可以获得它。

第一个 get 方法的调用被阻塞, 直到计算完成。

FutureTask 包装器是一种非常便利的机制, 可将 Callable 转换成 Future 和 Runnable, 它 同时实现二者的接口。

14.9 执行器 Executor

构建一个新的线程是有一定代价的, 因为涉及与操作系统的交互。 如果程序中创建了大 量的生命期很短的线程, 应该使用线程池(thread pool )。 一个线程池中包含许多准备运行的 空闲线程。 将 Runnable 对象交给线程池, 就会有一个线程调用 run 方法。 当 run 方法退出 时,线程不会死亡, 而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由是减少并发线程的数目。 创建大量线程会大大降低性能甚至使 虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“ 固定的”线程池以 限制并发线程的总数。

执行器 (Executor) 类有许多静态工厂方法用来构建线程池, 表 14-2 中对这些方法进行 了汇总。

![image-20190820115111542](/Users/mingjue/Library/Application Support/typora-user-images/image-20190820115111542.png)

14.9.1 线程池
  • Executors.newCachedThreadPool(),对于每个任务如果有空闲的线程可用,则复用,如果没则创建一个新的;
  • Executors.newFixedThreadPool(int nThreads),构建一个具有固定大小的线程池,如果提交的任务多于空闲的线程数则会放到队列中,当其他任务运行完后再运行它们。
  • Executors.newSingleThreadExecutor(),构建一个大小为 1 的线程池;

下面总结了在使用连接池时应该做的事:

  • 1 ) 调用 Executors 类中静态的方法 newCachedThreadPool 或 newFixedThreadPool

  • 2 ) 调用 submit 提交 Runnable 或 Callable 对象。

  • 3 ) 如果想要取消一个任务, 或如果提交 Callable 对象, 那就要保存好返回的 Future对象。

  • 4 ) 当不再提交任何任务时, 调用 shutdown。

14.9.2 预定执行

ScheduledExecutorService 接口具有为预定执行(Scheduled Execution) 或重复执行任务而设计的方法。 它是一种允许使用线程池机制的 java.util.Timer 的泛化。 Executors 类的newScheduledThreadPool 和 newSingleThreadScheduledExecutor 方法将返回实现了 Scheduled¬ExecutorService 接口的对象。

可以预定 Runnable 或 Callable 在初始的延迟之后只运行一次。 也可以预定一个 Runnable对象周期性地运行。

14.9.4 Fork-Join 框架

在后台, fork-join 框架使用了一种有效的智能方法来平衡可用线程的工作负载, 这种方 法称为工作密取(work stealing)。

14.9.5 可完成 Future

处理非阻塞调用的传统方法是使用事件处理器, 程序员为任务完成之后要出现的动作注册一个处理器。 当然, 如果下一个动作也是异步的, 在它之后的下一个动作会在一个不同的事件处理器中。尽管程序员会认为“ 先做步骤 1, 然后是步骤 2, 再完成步骤 3”, 但实际上程序逻辑会分散到不同的处理器中。 如果必须增加错误处理, 情况会更糟糕。 假设步骤 2是“ 用户登录”。可能需要重复这个步骤, 因为用户输入凭据时可能会出错。要尝试在一组 事件处理器中实现这样一个控制流, 或者想要理解所实现的这样一组事件处理器, 会很有 难度。

JavaSE8的CompletableFuture类提供了一种候选方法。与事件处理器不同,“ 可完成future" 可以“ 组合”(composed)。

14.10 同步器

java.util.concurrent 包包含了几个能帮助人们管理相互合作的线程集的类见表 14-5。这些机制具有为线程之间的共用集结点模式(common rendezvous patterns) 提供的“ 预置功能”( canned functionality ) 0 如果有一个相互合作的线程集满足这些行为模式之一, 那么应该直接 重用合适的库类而不要试图提供手工的锁与条件的集合。

14.10.1 信号量 Semaphore

概念上讲, 一个信号量管理许多的许可证(permit)。 为了通过信号量, 线程通过调用acquire 请求许可。

14.10.2 倒计时门栓 CountDownLatch

一个倒计时门栓(CountDownLatch) 让一个线程集等待直到计数变为0。倒计时门栓是 一次性的。一旦计数为 0, 就不能再重用了。

举例来讲, 假定一个线程集需要一些初始的数据来完成工作。 工作器线程被启动并在门 外等候。另一个线程准备数据。当数据准备好的时候, 调用 cmmtDown, 所有工作器线程就 可以继续运行了。

14.10.3 障栅

CyclicBarrier 类实现了一个集结点(rendezvous) 称为障栅(barrier)。考虑大量线程运行 在一次计算的不同部分的情形。 当所有部分都准备好时, 需要把结果组合在一起。当一个线 程完成了它的那部分任务后, 我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销, 线程就可以继续运行。

障栅被称为是循环的(cyclic), 因为可以在所有等待线程被释放后被重用。在这一点上,

有别于 CountDownLatch, CountDownLatch 只能被使用一次。

14.10.4 交换器

当两个线程在同一个数据缓冲区的两个实例上工作的时候, 就可以使用交换器( Exchanger) 典型的情况是, 一个线程向缓冲区填人数据, 另一个线程消耗这些数据。当它

们都完成以后, 相互交换缓冲区。

14.10.5 同步队列

同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue的 put 方法时, 它会阻塞直到另一个线程调用 take 方法为止, 反之亦然。 与 Exchanger 的情况不同, 数据仅仅沿一个方向传递, 从生产者到消费者。

即使 SynchronousQueue 类实现了 BlockingQueue 接口, 概念上讲, 它依然不是一个队列。它没有包含任何元素, 它的 size 方法总是返回 0。

联系我