DomBro Studio

线程

2018/03/11

线程

如果你穿越到三十年前,网络并不普及,在Internet只有几百万用户而不是现在的数十亿时,你会发现网站比现在的更加拥堵。这个问题在于,当时大多数FTP服务器会为每个连接创建一个进程,意味着100个并发用户就要处理额外的100个进程。由于进程是相当重量级的,太多进程会很快让服务器吃不消。因此人们想到了一个解决方案——使用线程。

什么是线程

一个程序执行多个任务,每一个任务就是一个线程。它是线程控制的简称,可以同时运行一个以上线程的程序称作多线程程序。 ——《Java核心技术卷一》

  • 线程 VS 进程

回到最开始的例子,为什么使用线程替代进程就可以解决网站拥堵的问题呢?这得从他们各自的特点说起,每个进程都拥有自己的一块内存,拥有自己的一套变量(资源)。而线程则在一个进程中运行,在一块内存中共享变量(资源)。想象一下当使用多线程处理网站请求时,在一块内存中的线程处理不同的请求一定比为每个处理请求开辟新进程更加高效。

使用线程来代替进程,可以让你的服务器性能提升三倍。如果重用线程池,在同样的硬件和网络连接条件下,服务器的运行可以快九倍多。 ——《Java网络编程》

注:实际上在处理网站拥堵问题上,还有一种重用进程的解决方案,即在服务器启动时就创建固定数量的进程,处理请求不时在新建进程而重用那些处理完请求但未销毁的进程。

多线程编程

在 Java 中 Thread 是 java.lang.Thread 类的一个实例。Java 作为面向对象语言的大佬,用 Thread对象 来与虚拟机中的线程(thread)对应。这一节会你看到是Java如何启动线程,以及线程的运行。

Java 线程的启动

在初次接触线程时,感觉很奇怪(大概所有习惯单线程编程的程序员都会有些奇怪),有些茫然不知所措,不知道我写的线程究竟有没有运行。Java 线程的启动,要构造一个 Thread 实例,调用它的 start() 方法:

1
2
Thread t = new Thread();
t.start();

这个线程并没啥意思,因为他什么都没有做。要想让线程完成一些任务,可以继承Thread类覆盖其run()方法。也可以实现Runnable接口,将 Runnable 对象传递给Thread构造函数 。实际上 run()方法封装了线程的工作,线程结束在于run()方法是否完成。

派生Thread

要想让线程做一些任务,就一定要在run()方法中实现。下面介绍通过派生Thread类,重写run()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PrintThread extends Thread {

private String name;

public PrintThread(String name) {
this.name = name;
}

@Override
public void run() {
System.out.println(name);
}

public static void main(String[] args) {
for (String name:args){
PrintThread printThread = new PrintThread(name);
printThread.start();
}
}
}

上面这段代码,mian()方法从命令行读取参数,针对每个参数都会启动一个 PrintThread 线程,这个线程的工作实际上是在run()方法中完成的,即每个线程很简单的打印参数。请务必记住如果对Thread派生子类,就应当只覆盖run()方法。

并且由于run()方法签名是固定的,无法向其中传递参数和返回值。因此需要其他方法向线程传递信息和从中获取信息。传递信息最简单的方法是向构造构造函数中传递参数,这会设置Thread子类中的字段。 ——《Java网络编程》

实现Runnable接口

实现Runnable接口实际上就是在实现run()方法,实现这个接口的类都必须要提供这个方法,要启动执行Runnable任务的一个线程,可以把这个Runnable对象传入Thread构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PrintRunnable implements Runnable {

private String name;

public PrintRunnable(String name) {
this.name = name;
}

@Override
public void run() {
System.out.println(name);
}

public static void main(String[] args) {
for (String name:args){
PrintRunnable runnable = new PrintRunnable(name);]
Thread thread = new Thread(runnable);
thread.start();
}
}
}

虽然并不认为实现Runnable接口一定比派生Thread方式好,毕竟在不同使用场景下二者优势各有不同。但我更倾向于使用Runnable接口,因为这会更加清楚的将线程完成的任务和线程本身分开。

从线程返回信息

把这个部分单拿出来是因为,习惯传统单线程模型的程序员在转向多线程环境时,最难掌握的一点就是如何从线程返回信息(此处的信息一般是在线程结束或者快要结束时获取)。

从结束的线程获取而信息,这是多线程编程中最常被误解的方面之一。 ——《Java网络编程》

之所以出现上述情况是因为,无论是start()方法还是run()方法都不会返回任何值。无法从线程中直接获得返回信息,也就不知道这个线程是否执行完毕。

###一般错误方法

你可能会想到在线程对象中增加一个标识,在 run()方法中给标识赋值,在线程启动后,通过该对象获取这个标识就可以在从线程返回信息,但是这确实一个在多线程操作中大错而特错的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ErrorThread extends Thread{
//标识
String digest;

public void run(){
//在线程中为标识赋值
digest = "Just test thread";
}

//获取该标识
public String getDigest() {
return digest;
}

public static void main(String[] args) {
ErrorThread err = new ErrorThread();
err.start();
//通过线程获取该标识
System.out.println(err.getDigest());
}
}

上面的代码的返回结果

1
2
3
null

Process finished with exit code 0

可能会觉得奇怪,怎么会是 null ? 明明为 digest 赋值了。问题在于在单线程的程序中,线程的执行是按照代码顺序的,但是在多线程中,以上面代码为例,在主线程(即main方法)调用err.getDigest()方法之前,被调用的线程有可能还没有结束(即run方法还未执行完),所以获取到的标识就会是一个null。当然有时候可能在启动线程之前,err.getDigest()就已经执行结束了,那样就会报一个空指针异常。总之,这是一个单线程程序员很容易掉进的坑。

回调

回调可以有效的从线程中返回信息。

回调是通过调用主类(即启动这个线程的类)中的一个方法来做到的。这被称为回调(callback),因为线程在完成是反过来调用其创建者。这样一来,主程序就可以等待线程结束期间休息,而不会占用运行线程的时间。 ——《Java网络编程》

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
35
36
37
38
39
40
41
42
public class CallbackRunnable implements Runnable {

//一个被主类的引用
private CallbackUserInterface callback;


public CallbackRunnable(CallbackUserInterface callback) {
this.callback = callback;
}

@Override
public void run() {
String digest = "Test Callback";
//在线程要结束时,回调主类中方法
callback.receiveDigest(digest);
}
}

//调用线程即主类
public class CallbackUserInterface {

//标识
private String digest = "";
//接收标识
public void receiveDigest(String receive){
this.digest += "线程返回的信息是:"+receive;
System.out.println(this.digest);
}

public void calculateDigest(){
CallbackRunnable cb = new CallbackRunnable(this);
//启动线程
Thread thread = new Thread(cb);
thread.start();
}

public static void main(String[] args) {
CallbackUserInterface userInterface = new CallbackUserInterface();
userInterface.calculateDigest();
}

}

上面代码返回结果:

1
2
3
线程返回的信息是:Test Callback

Process finished with exit code 0

神奇吗?可以正常显示。不要觉得神奇,相比于有主程序询问每个线程来寻找答案,而是有每个线程告知主程序答案。就好像那句”上来自己动”,让人觉得舒服。至于为什么不会显示 null 或者空指针异常,也是一个很好理解的,在run方法中告知主线程这个标记,虽然调用了主类对象方法,但还是在线程的程序执行顺序中,所以无论主线程运行顺序如何,都不影响线程的正常执行完毕。唯一要注意的是调用主类方法一定要在线程快要结束工作时进行,否则回调的这个标记就没有意义了。

Future、Callable和Executor

如果觉得回调方式麻烦,Java 5 引入了更简单的处理回调的方式。

不再是直接创建一个线程,你要创建一个ExecutorService,他会根据需要为你创建线程。可以向ExecutorService提交Callable任务,对于每个Callable任务,会分别得到一个Future。可以向Future请求得到任务结果。如果结果已经准备就绪,就会立即得到这个结果。如果结果还没准备好,轮询线程会阻塞,知道结果准备就绪。 ——《Java网络编程》

Callable ——提交到——> ExecutorService ——得到——> Future ——查看——> Callable 结果

Callable 接口定义了一个call()方法,可以返回任意类型,而 Future 可以查看的 Callable 结果就是这个call返回的值。下面是使用多线程快速找到最大值的例子

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//Callable相当于一个任务
public class FindMax implements Callable<Integer>{

private int[]data;
private int start;
private int end;

public FindMax(int[] data, int start, int end) {
this.data = data;
this.start = start;
this.end = end;
}
//call 方法可以有任意的返回类型
@Override
public Integer call() throws Exception {
int max = Integer.MIN_VALUE;
for (int i = start; i < end; i++){
if (data[i] > max)
max = data[i];
}
return max;
}
}

public class MaxFinder {

public static int max(int[] data) throws IllegalArgumentException, ExecutionException, InterruptedException {
if (data.length == 1){
return data[0];
}else if (data.length == 0){
throw new IllegalArgumentException();
}

//将任务分解为两个部分
FindMax task1 = new FindMax(data,0,data.length/2);
FindMax task2 = new FindMax(data,data.length/2,data.length);

//创建两个线程
ExecutorService service = Executors.newFixedThreadPool(2);
//分别提交两个线程,并由Future启动
Future<Integer> future1 = service.submit(task1);
Future<Integer> future2 = service.submit(task2);
//通过Future.get()得到Callable的结果
return Math.max(future1.get(),future2.get());

}

}

上面例子,将一个数组分成两部分,利用两个线程分别找出这两个部分的最大值,再找出这两个值中最大的。这种办法几乎会同时搜索两个子数组,运行速度几乎可以达到原来的两倍。

尽管可以直接调用call()方法,但这并不是本来目的。而是要通过Future来启动线程并获取call()的返回值,来确定线程是否执行完毕

同步

在图书馆中的书,每个人都可以去借阅,这样可以省下自己的钱去买书。但如果你想看的书不幸被借走了,就只能申请这本书归还时为我保留。同时你也不是能在书上做标记。从图书馆借书而不是自己买,在时间和方便性会有很大的损失,但能够节约钱和存储时间。

线程就像图书馆的借阅者,它从一个中心资源池中借阅。线程通过共享内存、文件句柄、sokect和其他资源使得程序更高效。只要两个线程不同时使用相同资源,多线程程序就比多进程程序高效得多。 ——《Java网络编程》

多线程的缺点是,如果两个线程同时访问同一个资源,其中一个就必须等待另一个结束。如果其中一个没有等待,资源就有可能会被破坏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PrintRunnable implements Runnable {

private String name;

public PrintRunnable(String name) {
this.name = name;
}

@Override
public void run() {
name += ": dasdasfafasdsadasdasdsadas";
System.out.println(name);
}

public static void main(String[] args) {

String[] strings = {"hello","china","good","how"};
for (String string:strings){
PrintRunnable runnable = new PrintRunnable(string);
Thread t = new Thread(runnable);
t.start();
}
}
}

上面这段代码执行结果:

1
2
3
4
hello: dasdasfafasdsadasdasdsadas
china: dasdasfafasdsadasdasdsadas
good: dasdasfafasdsadasdasdsadas
how: dasdasfafasdsadasdasdsadas

根据执行结果可以看到,将 name 作为保存变量在线程打印时,这四个线程并行运行,每个线程会在控制台打印一行。但如果打印的不是保存结果的name,而是将中间结果可用时就直接打印在控制台

1
2
3
4
5
6
7
//只修改 run方法
@Override
public void run() {

System.out.print(name + ": dasdasfafasdsadasdasdsadas");
System.out.println();
}

执行结果:

1
2
3
4
5
hello: dasdasfafasdsadasdasdsadashow: dasdasfafasdsadasdasdsadaschina: dasdasfafasdsadasdasdsadas
//下面是两个空行


good: dasdasfafasdsadasdasdsadas

可以看到线程结果都混在一起了,造成这种现象的原因是 System.out 是由4个不同的线程共享。如果一个线程通过多个System.out语句向控制台输出,有可能他还没有完成所有写入,就有另一个线程插进来,开始他的输入。至于哪个线程会抢先于其他线程,具体顺序无法确定。

需要有一种办法能够指定一个共享资源只能由一个线程独占访问来执行一个特定的语句序列。在上面的例子中共享资源是 System.out 而需要独占访问的语句是:

1
2
System.out.print(name + ": dasdasfafasdsadasdasdsadas");
System.out.println();

同步块

为了指示这两行代码应当一起执行,要把它们包围在 sychronized 块中,他会对 System.out 对象同步,使用同步块的run方法

1
2
3
4
5
6
7
8
@Override
public void run() {
synchronized (System.out){
System.out.print(name + ": dasdasfafasdsadasdasdsadas");
System.out.println("");
}

}

执行结果:

1
2
3
4
hello: dasdasfafasdsadasdasdsadas
china: dasdasfafasdsadasdasdsadas
good: dasdasfafasdsadasdasdsadas
how: dasdasfafasdsadasdasdsadas

一旦线程开始打印这些值,所有其他线程在打印他们得知之前就必须停止,需要等待这个线程结束。同步要求在同一个对象上同步的所有代码要连续地运行,而不能并行运行。 ——《Java网络编程》

  • 注意

需要注意的是,对不用对象同步的代码或者根本不同步的代码仍然可以与这个代码并行运行。 Java并没有提供任何方法来组织其他线程使用共享资源。他只能防止对同一个对象同步的其他现线程使用这个共享资源。(这段话的意思是如果另一线程中使用了本线程中的同步对象,但是同步的对象与本线程中不一致,也不会影响另一个线程与该线程代码并行运行。)

只有当两个线程都拥有相同对象的引用时,同步才成为问题。 同步块就是将可能与其他线程共享的资源用锁包裹起来,这样其他线程在当前线程运行时,就不能对该资源操作。与同步块对应的还有同步方法,同步方法是对当前对象(this引用)同步整个方法。

举个例子

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//该类向文件中写入数据
public class WriterFile {

private Writer out;

public WriterFile(File file) throws IOException {
FileWriter writer = new FileWriter(file);
this.out = new BufferedWriter(writer);
}

public void writeEntry(String message) throws IOException {
out.write(message);
}

public void close() throws IOException {
out.flush();
out.close();
}
}

//线程类,要在线程中调用 WriterFile 的方法
public class WriteRunnable implements Runnable {

private WriterFile writerFile;

private String message;

//在构造方法中为 WriterFile域 赋值
public WriteRunnable(WriterFile writerFile,String messasge) {
this.writerFile = writerFile;
this.message = messasge;
}

@Override
public void run() {
try {
//在run()方法中会调用写入数据方法
writerFile.writeEntry(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

//主类
public class Test {

public static void main(String[] args) throws IOException {

WriterFile writerFile = new WriterFile(new File("C:\\Users\\18246\\Desktop\\thread.txt"));
//向两个线程中传入同一个WriterFile对象
WriteRunnable runnable1 = new WriteRunnable(writerFile,"我来自第一个线程");
WriteRunnable runnable2 = new WriteRunnable(writerFile,"我来自第二个线程");
Thread t1 = new Thread(runnable1);
Thread t2 = new Thread(runnable2);
t1.start();
t2.start();

}
}

上面例子中,在主类中向线程传入同一个WriterFile对象,这肯定是要出问题的,因为在run()方法中会调用WriterFile写入数据方法,一个线程在写入过程中另一个线程完全有可能会打断,这就会出现写入问题。有三种解决办法:

  • 第一种使用上面提到的同步块对Writer对象out同步
1
2
3
4
5
6
7
public void writeEntry(String message) throws IOException {
synchronized (out){
out.write(message);
out.write("\r\n");
}

}

这是由于使用这个WriterFile对象的线程也会使用属于这个WriterFile的同一个对象out。

  • 第二种是对WriterFile对象本身同步,这很简单只需要用到this关键字
1
2
3
4
5
6
7
public void writeEntry(String message) throws IOException {
synchronized (this){
out.write(message);
out.write("\r\n");
}

}
  • 第三种是Java提供的一个快捷方式,同步方法,即在方法声明添加修饰符
1
2
3
4
public synchronized void writeEntry(String message) throws IOException {
out.write(message);
out.write("\r\n");
}

同步这里说的比较多,比较啰嗦,是因为在平时写单线程代码很少遇到这种资源共享情况。但是同步也有一定弊端,比如降低性能,还会造成死锁,最关键的一点是同步可能并不会保护真正需要保护的对象。 上面例子中 out 是真正要被保护的对象,但如果其他与 WriterFile 完全不相关的类有 out 的引用,那么 out 也会写入失败。不过上面例子中,由于out是一个私有变量,由于没有提供这个对象的引用,所以其他对象也没有办法调用这个对象啦。

同步的替代方法

同步是为了保护某个可以共用的资源,那么避免同步就要想办法避免使用这种共用资源。下面有三种同步的替代方法。

  • 局部变量代替字段

局部变量不存在同步问题。每次进入一个方法时,虚拟机将为这个方法创建一组全新的局部变量。这些变量是外部不可见的,而且方法退出时将被撤销。因此一个局部变量不可能有两个不同的线程共享。 ——《Java网络编程》

所以上面的例子中 writeEntry() 方法可以写成

1
2
3
4
5
public void writeEntry(String message) throws IOException {
//out作为局部变量
Writer out = new BufferedWriter(new FileWriter(file));
out.write(message);
}
  • 在自己的类中利用不可变性

要使一个对象不可变,只要将其所有字段声明为 private 和 final ,而且不要编写任何能改变他们的方法。 ——《Java网络编程》

  • 将非线程安全的类用作为线程安全类的一个私有字段

只要包含类只以线程安全的方式访问这个非安全类,而且只要永远不让这个私有字段的引用泄露到另一个对象中,那么这个类就是安全的。 ——《Java网络编程》

死锁

上面说同步可能会导致一个问题:死锁。如果两个线程需要独占访问相同的资源集,而每个线程又分别有这些资源的不同子集的锁,就会发生死锁。这就好像大黄和小黄都要写毕设,他们都需要两本书《Java编程思想》和《前端开发》,而大黄借到了一本,小黄借到了第二本,同时他们有都不愿意放弃自己借到的书,那么都将无法完成毕设。这就是死锁问题。

糟糕的是死锁可能是偶发性 bug ,很难检测。死锁通常取决于不可预知的时间问题。(作者的意思是无法提前判断这个bug) ——《Java网络编程》

要防止死锁,最重要的技术就是要避免不必要的同步。同步应当是确保线程安全的最后一道防线,如果必须要同步,要保持同步块尽可能小,而且尽量不要一次同步多个对象。

线程调度

当多个线程可以同时运行时,必须考虑线程调度问题。要确保所有重要线程至少得到一些时间来运行,更重要的线程要得到更多的时间。同时你还要保证线程以合理的顺序执行。

线程优先级

不是每个线程创建时都可以均等的。每个线程都是有一个优先级,指定为一个从0到10的整数。在Java中,10是最高优先级,0是最低优先级。

在Thread类中指定了三个命名常量(1、5和10)分别代表三和优先级

1
2
3
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

线程的优先级可以用 Thread.setPriority()方法来改变

1
2
3
4
PrintThread printThread = new PrintThread(name);
//设置该线程优先级为8
printThread.setPriority(8);
printThread.start();

在优先级的设置上,与用户交互的线程应当获得非常高的优先级,这样就能感觉到响应非常快。另一方面,在后台完成计算的线程应当获得低优先级。很快技术的任务应当有高优先级,将花费很长时间的任务应当有低优先级,这样就不会妨碍其他任务。——《Java网络编程》

不过一般情况下要避免对线程实用太高优先级,因为这要冒一定风险,可能使其他低优先级线程陷入饥饿

抢占

每个虚拟机都有自己的线程调度器,确定在给定的时刻运行那个线程。线程的调度主要有两种:抢占式和协作式。抢占式线程调度器确定一个线程正常的轮到其cpu时间,会暂停这个线程,将cpu控制权交给另一个线程。协作式线程调度器再将CPU控制权交给其他线程前,会等待正在运行的线程自己暂停。

由此可见,抢占式线程不会像协作式线程那样容易陷入饥饿,协作式线程会让高级的线程独占cpu。因此所有Java虚拟机都确保在不同优先级之间使用抢占式线程调度

当一个低优先级线程正在运行,而一个高优先级线程准备运行时,虚拟机或早或晚会暂停这个低优先级进程,让高优先级进程运行。高优先级进程就抢占了低优先级进程。 ——《Java网络编程》

暂停线程

Java虚拟机使用抢占式来调度线程,让当前线程暂停,使其他线程有机会运行。那么如何具体暂停线程呢?大概有下面方法可以暂停线程或指示他准备暂停。

  • 对 I/O 阻塞

当线程必须停下来等待他没有的资源的时候,就会发生阻塞。

阻塞:当等待一个未到的资源时,程序会暂停,下面的代码不会执行,直到资源到达。

要让网络程序中的线程自动放弃CPU控制权,最常见的方式是对I/O的阻塞。 ——《Java网络编程》

很好理解的,比如当前线程要使用流读取一个文件时,在读取文件的那几毫秒就发生了阻塞,可偏偏就是这几毫秒就够其他线程完成一些重要任务。

  • 对同步对象阻塞

线程在进入一个同步方法或代码块时也会阻塞。如果这个线程没有所同步对象的锁,而其他线程拥有这个锁,这个线程就会暂停,直到锁被释放。

无论是I/O阻塞还是堆锁阻塞,都不会释放线程已经拥有的锁。

  • 放弃

要让线程显示的放弃控制权,线程可以通过调用 Thread.yeled() 静态方法。这将通知虚拟机,如果有另一个线程准备就绪,可以运行该线程。放弃不会释放这个线程拥有的锁, 因此在线程放弃时,不应该做任何同步。

一个线程放弃时,如果等待运行的其他线程都是因为这个线程的所拥有的同步资源而阻塞,那么这些线程将不能运行。 ——《Java网络编程》

在实际中让一个线程放弃非常简单。

1
2
3
4
public void run(){
//完成线程的工作
Thread.yield();
}

注意的是:放弃只会使其他有相同优先级的线程有机会运行!!所以在没有必要放弃的情况下,这种防范措施效果不太明显。

  • 休眠

休眠是更有力的放弃方式。放弃只是表示线程愿意暂停,其他相同优先级的线程有机会运行。而进入休眠的线程,不管有没有其他线程准备运行,休眠线程都会暂停。这样,不只是其他有相同优先级的线程的大机会,还会给更低优先级线程运行的机会。进入休眠的线程依然拥有这个线程的锁, 因此要避免在同步方法或块内让线程休眠。使用静态方法 Thread.sleep(),让线程休眠,在该方法中传入想让线程休眠的时间。

使让一个线程休眠也非常简单。

1
2
3
4
5
6
7
8
9
10
11
public void run(){
while(true){
//完成线程工作
try{
//休眠5分钟
Thread.sleep(300000);
//如果其他线程唤醒该线程,该线程会抛出InterruptedException
}catch(InterruptedException)
break;
}
}

向人一样,线程可以休眠,就可以被唤醒。如果在线程休眠时间内想让该线程提前继续运行,可以调用该线程的 interrup()方法将该线程唤醒。

一个线程休眠并不意味着其他醒着的线程不能处理这个相应线程的Thread对象,当另一个线程唤醒休眠线程后,会让休眠中的线程得到一个InterruptedException异常。休眠线程会被唤醒并并正常执行。

  • 连接线程

可以通过 Thread 对象的 join()方法,join()方法允许一个线程再继续执行之前等待另一个线程结束。但是该方法已经不常用。略过。

  • 等待一个对象

线程可以等待一个它锁定的对象。在等待时,它会释放这个对象的锁并暂停,直到他得到其他线程的通知。另一个线程以某种方式修改这个对象,通知等待对象的线程,然后继续执行。并不要求等待线程等待线程和通知线程在另一个线程继续前必须结束。

这个方式并不太出名,因为他并不涉及Thread类的任何方法。实际上,要等待某个特定的对象,希望暂停的线程首先必须使用synchronized获得这个对象的锁,然后调用是重载的 wait()方法。 ——《Java网络编程》

上面说的三个wait方法在 java.lang.Object中,也就是说任何类的任何对象都可以调用这个方法。当对象调用wait()方法时,调用该对象的线程会释放掉等待该对象的锁(但不是释放等待其他对象的锁),并进入休眠。线程会保持休眠直到:
1)时间到期 : 即 wait() 方法中时间参数到期,该进程会唤醒,线程会紧挨着wait()调用之后向下继续执行。
2)线程被中断 : 与 sleep() 工作方式相同,当其他线程调用这个线程额interrup()方法,将该进程手动唤醒。
3) 对象得到通知 : 这是个新方法。在其他线程在这个线程所等待的对象上调用notify()或notifyAll()方法时,就会发生通知,这两个方法都在java.lang.Object中。

这两个方法都必须在线程所等待的对象上调用,而不是在Thread本身调用。再通知一个对象之前,线程必须首先使用同步方法或同步块获得这个对象的锁。 notify() 会随机通知一个正在的等待该对象的线程,并唤醒它。notifyAll()唤醒等待指定对象的每一个线程。一旦线程得到通知,它就会试图重新获得所等待对象的锁。成功就继续顺着wait()向下执行,失败,他就会陷入阻塞,知道可以得到锁。——《Java网络编程》

当多线程希望可以等待同一个对象时,等待和通知会更常用。

  • 结束

    最后一个让线程暂停的方式,就是结束。即 run()方法结束。

线程池和Executor

想程序中添加多个线程会极大的提升性能,尤其是I/O首先程序,比如大多数网络程序。但是,线程自身也存在开销。线程需要虚拟机做大量工作,最后,虽然线程有利于更高效利用计算机有限CPU资源,但是资源毕竟是有限的!

如果并发线程数达到 4000 至 20000 时,大多数虚拟机可嗯呢过会由于内存耗尽而无法承受。不故意通过使用线程池而不是为每个连接生成新线程,服务器每分钟就可以用不到100个线程来处理数千个短连接。 ——《Java网络编程》

由此可见,线程池的使用场景一般是:
1)有大量 I/O 操作程序如网络程序。
2)每个线程的任务量很大,会消耗大量cpu资源。

遇到上面两种情况的多线程程序都可以考虑使用线程池。

  • 线程池的使用

利用 java.lang.concurrent 中的Executors类(executor是执行的意思),可以很容易的建立一个线程池。只需要将各个任务作为 Runnable 对象提交给这个线程池,你就会得到一个 Future 对象,可以用来检查任务进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//一个Runnable
public class FileRunnable implements Runnable{
public void run(){
....
}
}

public class TestPool{
public static void main(String[] args) {
//创建线程池,并设置线程数为 4
ExectorService pool = Exectors.newFixedThreadPool(4);
for(int i = 0;i<args.length;i++){
FileRunnable task = new FileRunnable();
//将任务提交
pool.submit(task);
}
pool.shutdown();
}
}
  • shutdown()

要说一下这个shutdown()方法,这个方法不是终止等待中的工作。他只是通知线程池已经更多的任务需要增加到它的内部队列了,而且一旦完成了所有等待工作,就应当关闭。像上面程序中,可以这样关闭是因为他有一个终点,即只处理args.lenth个任务,所以可以使用pool.shutdown()。

  • shutdownNow()

而在一些不知道确定任务终点的线程池,如果想在运行时终止线程池的任务。可以使用 shutdownNow() 方法 。该方法会终止线程池中正在执行的任务,并忽略所有等待任务。

1
pool.shutdownNow();

总结

1.线程比进程轻量级,线程在进程中运行,共用一块内存、以及变量等资源。

2.你可以根据需要使用继承Thread类和实现Runnable接口作为参数传给Thread构造方法两种方案创建进程。

3.线程通过 Thread 对象的start()方法开启,线程的任务封装在 run()方法中。扩展Thread类时尽量只重写其run()方法,其他方法不要动。

4.若想从线程返回信息可以使用回调的方式。回调就是被调用线程调用主类(调用该线程的程序)中的方法,回调返回信息尽量在run()方法快执行完时返回。

5.使用Callable、Executors和Future也可以从线程返回信息,Callable 中的 call()方法可以返回任意类型,将 Callable 对象提交给 ExectorService 线程池可以执行该任务,并得到一个Future对象,该对象可以得到对应任务对象的 call()方法返回值。

6.同步是为了让线程中的共享对象(资源)可以得到保护的一种机制,即当一个线程操作共享对象(资源)时另一个线程不可以对该对象进行操作。同步的策略有同步块、同步方法、同步对象三种。在编写代码时要避免不必要的同步。

7.当两个线程都需要同一个资源集,但有都不愿意放弃各自手中的资源时,两个线程就会陷入等待程序无法正常运行,从而形成死锁。所以死锁可以理解为线程得不到需要的资源(被占用的资源)。要避免死锁解决方案还是要避免不必要的同步。

8.线程要按照一定的规则轮番运行,这个规则就是线程的优先级,多个线程同时运行时,虚拟机通常只运行优先级最高的。

9.线程优先级分为10各等级,Java 中 依次按 0 -10 优先级递增。使用 Thread 对象的setPriority(优先级)为线程设置优先级。 合理分配优先级,一般不要给一个线程过高的优先级,这会让优先级较低的线程陷入饥饿。

10.在虚拟机的线程调度器中,按照抢占式的线程调度对线程进行调度。抢占式调度在某线程到了运行是时间会暂停当前运行线程,将cpu控制权交给另外的线程。

11.Java中主要有四种让线程暂停,是其他线程获得运行机会的方式。

12.对I/O阻塞或对同步对象阻塞,这种可以算作是一种,他们都会在等待资源(文件、对象)陷入阻塞时让另外的线程得到运行的机会。对I/O阻塞或对同步对象阻塞都不会放弃已经拥有的锁,即对同步对象阻塞可能会引起死锁。

13.放弃,让线程放弃控制权。使用Thread.yield()方法,会通知虚拟机如果另一个优先级相同的线程准备就绪就可以运行。放弃也不会释放已经拥有的锁,所以在放弃时不应该做任何同步。

14.休眠,为更有力的方式。与放弃的区别是休眠可以让较低线程优先级的线程运行。使用Thread.sleep()方法使线程休眠,也还以设置休眠时间。休眠同样不会释放他的锁。

15.使用休眠线程对象的 interrupt() 方法可以唤醒休眠线程,被唤醒的线程会抛出一个InterruptedException 异常,所以在使用 sleep()方法时,如果要对该线程唤醒,就要捕获sleep()方法的InterruptedException。

16.等待对象,等待一个被锁的对象。任何同步对象都可以被等待。一个线程如果需要另一个线程对同步对象做一些改动,该线程会释放这个对象的锁并暂停,在改动之后该线程继续执行。值得一提的是等待对象的方法是在Object.wait(),这会让该线程暂停,而对该对象改动的线程可以使用 Object对象的notify()方法,通知等待线程等待结束,可以继续运行。

17.线程池可以减少过多线程对虚拟机性能的影响,在遇到过多I/O操作和每个线程的任务量很大,会消耗大量cpu资源的情况可以使用线程池。将 Runnable 对象提交给Executors 会得到 Future 对象用来获得线程执行结果。

18.Executors对象的shutdown()方法不是停止等待的任务,而是告诉连接池全部任务已提交完毕,当所有任务完成后就关闭连接池;shutdownNow()方法则是关闭所有目前进行的任务,并忽略所有等待的任务。

CATALOG
  1. 1. 线程
    1. 1.1. 什么是线程
    2. 1.2. 多线程编程
      1. 1.2.1. Java 线程的启动
      2. 1.2.2. 派生Thread
      3. 1.2.3. 实现Runnable接口
  2. 2. 从线程返回信息
    1. 2.1. 回调
    2. 2.2. Future、Callable和Executor
  3. 3. 同步
    1. 3.1. 同步块
      1. 3.1.1. 举个例子
    2. 3.2. 同步的替代方法
  4. 4. 死锁
  5. 5. 线程调度
    1. 5.1. 线程优先级
  6. 6. 抢占
    1. 6.1. 暂停线程
  7. 7. 线程池和Executor
  8. 8. 总结