10.0 Thread, Lambda
Multi Processing
多进程是操作系统的一个核心特性,它允许你同时运行多个程序 。
- 如果你的计算机只有一个核心 (single core),操作系统会调度进程,使它们看起来像是并发执行的
- 如果计算机有多个核心 (multiple cores),操作系统会将这些进程分配到不同的核心上执行 。
实现的简单方法可能是同时运行几个 Java 程序,但是这种方法有限制,进程之间无法通信
Multi Threading
并发:一个单独的进程也可以拥有多个执行线程,用处如下:
- Responsiveness
我们正在编写读取大文件的代码。如果为了等待文件加载而不得不暂停整个程序,那将是很糟糕的。相反,我们可以让一个线程读取文件,而另一个线程更新图形用户界面(GUI)或执行其他操作。
- Efficiency
现代计算机通常有很多核心,我们将希望编写能够利用所有这些核心的代码,以最大限度地提高效率。Java支持多线程代码 。这是通过 Thread
类实现的 。每个 Thread
类的实例代表一个新的执行线程 。如果我们创建多个线程,我们就可以让代码的多个部分同时执行 。通常,我们会想要扩展这个类并重写其 run()
方法 。
Create a Tread
Ex1
class MyThread extends Thread {
private int number;
public MyThread(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println("MyThread (" + number + ") running");
}
}
public class ThreadExample {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyThread t = new MyThread(i);
t.start();
}
}
}
定义一个类继承 Thread 然后构造函数初始化,重载 run 函数,在 ThreadExample 类的 main 方法中创建了一个循环,循环十次。在每次循环中,会创建一个 MyThread
类的实例 t
,并传入当前的循环变量 i
作为线程编号 。然后,调用 t.start()
方法 。一旦调用 start()
方法,run()
方法中的代码就会在一个新的线程中并发执行 。注意:因为线程调度的不确定性 ,运行这个程序会注意到线程的执行顺序并非严格按照 0 到 9 的顺序。
Ex2
class MyRunnable implements Runnable {
private int number;
public MyRunnable(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println("MyThread (" + number + ") running");
}
}
public class RunnableExample {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new MyRunnable(i));
t.start();
}
}
}
Runnable
接口也可以用来创建线程,定义后使用构造函数用于接受和储存整数,重载 run
方法后在 main
中用 Thread
其中的一个构造函数可接收 Runnable
对象,然后启动。一般来说这种方法更受推荐因为可以多重实现,第一个方法不支持多重继承。
Join
一旦我们启动了一个线程,有时需要等待它执行完毕才能继续主程序的后续步骤,特别是当后续步骤依赖于该线程的执行结果时 。例如,如果你创建了两个线程分别从两个不同的数据库读取数据,你可能需要等待两个线程都返回结果后才能继续程序的下一步操作 。
- Java 提供了
join()
方法:一个线程完成了run()
方法执行后调用join()
就会使当前线程等待直到线程执行完毕
public class JoinExample {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new MyThread(i);
threads[i].start();
}
for (int i = 0; i < 10; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {}
}
System.out.println("All threads finished");
}
}
十个线程全部执行完毕后才会被打印出来。join()
方法会抛出一个 InterruptedException
(检查型异常) 。这种情况会在一个线程的执行以非预期的方式结束时发生
Race condition
Race Condition: 当两个或多个线程试图同时对共享数据进行操作,并且操作的执行顺序会影响最终结果时,就发生了竞态条件。每次运行程序结果都不同因此一不小心就变成了 bug。
Ex1
class SpecialInt {
private int value;
public SpecialInt(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void increment() {
value++;
}
}
class AddThread extends Thread {
private SpecialInt specialInt;
public AddThread(SpecialInt specialInt) {
this.specialInt = specialInt;
}
public void run() {
for (int i = 0; i < 1000000; i++) {
specialInt.increment();
}
}
}
public class RaceCondition {
public static void main(String[] args) {
SpecialInt specialInt = new SpecialInt(0);
Thread t1 = new AddThread(specialInt);
Thread t2 = new AddThread(specialInt);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {}
System.out.println(specialInt.getValue());
}
}
这个程序会导致大量的竞态条件 。如果你运行这个程序,会发现输出结果通常不是2,000,000 。这是因为 specialInt.increment()
操作(即 value++
)不是原子性的。它实际上包含三个步骤:读取 value
的当前值,将该值加1,然后将新值写回 value
。多个线程的这三个步骤可能会交错执行,导致某些增加操作丢失。
- Good: Thread A reads 0, A writes 1, B reads 1, B writes 2
- Bad: Thread A reads 0, B reads 0, A writes 1, B writes 1
Synchronized
Java 提供同步语句,当一个线程进入这个代码块时,它会尝试获取 object
对象上的锁。
- 如果该对象已经被其他线程锁定,那么当前线程将会等待,直到该对象被释放
- 线程锁定对象、执行代码块代码、被解锁
该字段有效避免了竞态
class AddThread extends Thread {
private SpecialInt specialInt;
public AddThread(SpecialInt specialInt) {
this.specialInt = specialInt;
}
public void run() {
for (int i = 0; i < 1000000; i++) {
synchronized (specialInt) {
specialInt.increment();
}
}
}
}
以上这个方法是在调用期间给 object
临时设置同步,直接在原类直接设置 method 为同步一样有效
class SpecialInt {
private int value;
public SpecialInt(int value) {
this.value = value;
}
public int getValue() {
return value; //
}
public synchronized void increment() { //
value++;
}
}
两者同样安全有效
Deadlock
由于 Java 的同步关键字只针对单个对象:线程 A 持有对象 1 的锁,想要获取对象 2 的锁;同时 B 持有对象 2 的锁并且想要获取对象 1 的锁。这种情况会永远等待下去导致死锁,就像两个礼貌的人永远互相谦让。
synchronized (specialInt1) { // 先获取 specialInt1 的锁
// ...
synchronized (specialInt2) { // 再获取 specialInt2 的锁
// ... 对两者进行操作 ...
}
}
因此如果在执行的时候实现安全锁,只能通过这种方式逐个获取锁
Thread t1 = new AddBothThread(specialInt1, specialInt2); //
Thread t2 = new AddBothThread(specialInt2, specialInt1); // 注意参数顺序
t1.start();
t2.start();
由于传递参数顺序的不同导致了死锁:线程 t1 等待 t2 释放,线程 t2 等待 t1 释放。这种情况会永远等待下去导致死锁,就像两个礼貌的人永远互相谦让。修复方法就是 t2 传参顺序改为与 t1 一致。
Inter-thread communication
假设我们有一个线程,它需要等待某个对象被更新后才能继续执行 。这个线程需要释放它持有的锁,以便其他线程可以更新该对象 。
wait()
wait()
方法用于将当前线程置于“等待”状态 。- 其必须在
synchronized
代码块或同步方法内部 - 一旦调用了
wait()
,线程会释放它在该对象上持有的锁,并进入等待状态,直到另一个线程在该对象上调用notify()
。
notify() and notifyAll()
notify()
: 唤醒单个正在等待该对象锁的线程 。如果有多个线程在等待,会任意选择一个进行唤醒。notifyAll()
: 唤醒所有正在等待该对象锁的线程 。优先级最高的线程会首先获得锁 。- 同样也必须在同步上下文中调用
ProduceConsume
这个例子演示了生产者消费者模式,一个线程生成数据,另外一个消费数据
class ProduceConsume {
private String sharedResource; // 共享资源
// 生产者方法
public synchronized void produce(String value) throws InterruptedException { //
while (sharedResource != null) {
// 等待消费者消费资源
wait();
}
sharedResource = value;
System.out.println("Produced: " + value);
// 通知消费者资源已准备好
notify();
}
// 消费者方法
public synchronized void consume() throws InterruptedException {
while (sharedResource == null) {
// 等待生产者生产资源
wait();
}
System.out.println("Consumed: " + sharedResource);
sharedResource = null;
// 通知生产者资源已被消费
notify();
}
}
那么这样一来线程在调用这类方法的时候,在不同类里生产十次和消费十次,调用的时候就可以确保线程同步的同时数据是正确被消费,有了生产才有消费。
Anonymous Class
class MyStringComparator implements Comparator<String> {
private static int countOccurences (String s, char c)
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) {
count++;
}
}
return count;
}
@Override
public int compare (String s1, String s2) {
return countOccurences (s1, 'e') - countOccurences (s2, 'e');
}
}
public class Sorting {
public static void main(String[] args) {
String[] strings = {"elevated", "banana", "elephant", "early"};
Arrays.sort(strings, new MyStringComparator());
System.out.println(Arrays.toString(strings));
}
}
有时候你只需要一个一次性的对象,匿名类允许在不显式声明一个新类的情况下创建一个自定义对象 。这在完整类定义会显得繁琐的情况下特别有用。Arrays.sort(array, comparator)
方法使用 Comparator<T>
的实例来对数组进行排序 。这个接口只有一个需要实现的方法,即 compare(a, b)
。只是为了实现这个方法可以看到就要创建一个新的 MyStringComparator
。
public class AnonymousSorting {
private static int countOccurences (String s, char c) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) count++;
}
return count;
}
public static void main(String[] args) {
String[] strings = {"elevated", "banana", "elephant", "early"};
Arrays.sort(strings, new Comparator<String>() {
public int compare (String s1, String s2) {
return countOccurences (s1, 'e') - countOccurences (s2, 'e');
}
});
System.out.println(Arrays.toString(strings));
}
}
可以看到语法是以下这样非常简洁,甚至可以 extends a class
Lambda Expression
本质上是匿名方法或函数,现在犹豫其更方便,经常取代匿名类
直接创建一个 Comparator
来根据字符串的长度比较两个字符串,省略类型让其自动推断
public class LambdaSorting {
private static int countOccurences (String s, char c) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) count++;
}
return count;
}
public static void main(String[] args) {
String[] strings = {"elevated", "banana", "elephant", "early"};
Arrays.sort(strings, (s1, s2) -> countOccurences(s1, 'e') - countOccurences(s2, 'e'));
System.out.println(Arrays.toString(strings));
}
}
因此可以看到这个比较版本的过载是最简单的非常紧凑,方法里的参数甚至不用写类型自动推断
Details
可以不接受参数、多个 expressions,但是如果是多个参数中的 expressions 有 return 语句必须显示
public class LambdaRunnable {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println("Hello, world!");
return 1;
};
r.run();
}
}
Javadoc
Javadoc 是一个可以从 Java 源代码直接自动生成 HTML 格式 API 文档的工具 。你需要将 Javadoc 注释放在类、字段和方法声明的正前方,格式如下:
其注释可以包含标签 (tags) 以提供关于代码更具体的信息,下述为 method 版本注释
/**
* 这个方法做某事。
*
* @param param 参数的描述
* @return 返回值的描述
* @throws ExceptionType 何时会抛出该异常
*/
public ReturnType methodName (Type param) throws ExceptionType {
// 方法体
}
这个版本是类的:
/**
* 这是一个类上的 Javadoc 注释示例。
*/
public class Javadoc {
/**
* 如果给定的整数是偶数,此方法返回 true,否则返回 false。
* @param x 要检查的整数
* @return 如果 x 是偶数则返回 true,否则返回 false
*/
public boolean isEven (int x) {
return x % 2 == 0; //
}
}
要从 Java 文件中直接生成 HTML 文档,官方文档就是如此生成:
mydocs
是文件夹名