重新认识java——线程(Thread)

多线程编程是开发者必须要掌握的基本技能,线程(Thread)是基础和核心。只有深刻地理解Java线程,才能写出合理、高效的多线程代码。本文将研究Java中的线程,同时会捎带部分操作系统相关内容。主要的内容如下:

  1. 进程与线程
  2. Java线程(线程创建、Thread中的主要方法、线程通信)

进程与线程

进程与线程,是操作系统中的重要概念,稍微有点计算机基础的开发者都会听说过、接触过。

进程(Process):计算机中的程序关于某数据集合上的一次运行活动,是操作系统结构的基础(来自百度百科)。狭义上来说,进程是正在运行的程序实例。进程和程序间的关系很微妙,用一个比喻(来自《现代操作系统》)来说明。有一位拥有一手好厨艺计算机科学家正在为女儿烘制蛋糕,他有制作蛋糕的食谱,还有一些原料(面粉、鸡蛋等)。在这个例子中,食谱就是程序(用适当形式描述的算法),科学家就是CPU,原料就是各种输入数据。那么,进程就是食谱、原料以及烘制蛋糕的系列动作的总和。

线程(Thread):有时也被称为轻量级进程,是程序调度和执行的最小单元。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个进程可以包含多个线程,而一个线程只能属于唯一的进程。

已经有了进程,为什么还会需要线程呢?主要原因如下:

  1. 许多应用程序中,同时发生着多个活动。将这些应用程序分解成多个准并行的线程,程序设计的模型会变成更加简单。
  2. 由于线程比进程进行更加轻量,创建和取消更加容易。
  3. 如果程序是IO密集型,那么多线程执行能够加快程序的执行速度。(如果是CPU密集型,则没有这个优势)
  4. 在多CPU系统中,多线程是可以真正并行执行的。

Java线程

线程状态

java-thread-state

  1. 新建状态(New):线程对象已经创建,还没有在其上调用start()方法。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

创建和启动线程

Java中有两种方法定义线程:

1、继承java.lang.Thread类。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 进程Thread类定义线程。
* @author xialei
* @version 1.0 2016年8月1日下午9:22:37
*/
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello");
}
}

上面的代码继承了java.lang.Thread类,然后重写了run()方法。该方法的方法体内是线程需要完成的任务,称为线程执行体。下面的代码启动这个线程:

1
2
MyThread myThread = new MyThread();
myThread.start();

通过调用Thread类中的start()方法启动线程,这方法最终会调用start0()native方法启动线程。调用start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。

2、实现java.lang.Runnable接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 实现Runnable接口定义线程。
* @author xialei
* @version 1.0 2016年8月1日下午9:28:28
*/
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello");
}
}

调用下面的代码可以启动线程。

1
2
Thread thread = new Thread(new MyRunnable());
thread.start();

MyRunnable类本身并不能启动线程,而是需要实例化一个Thread类来启动线程。可以看到,不管是哪种方法,都是调用Threadstart()方法来启动的。Thread类和Runnable接口到底是什么关系呢?实际上,Thread类本身就实现了Runnable接口,Thread中的run()就是实现了Runnable中的run()方法,其代码如下:

1
2
3
4
5
public void run() {
if (target != null) {
target.run();
}
}

通过Thread(Runnable)构造方法可传入Runnable对象,然后直接调用其run()方法。

线程类型

Java中的线程有用户(User)线程和守护(Daemon)线程两类。我们默认创建的线程是用户(User)线程,守护线程–也称“服务线程”,在没有用户线程可服务时会自动离开。只要当前JVM实例中尚存在任何存货的用户线程,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

下面来分别演示一下这两类线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserThreadTest {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i=" + i);
}
}
};
t.start();
}
}

上面创建一个默认线程(用户线程),在任务是打印出0~10,上面的运行结果如下:

1
2
3
4
5
6
7
8
9
10
i=0
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserThreadTest {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i=" + i);
}
}
};
t.setDaemon(true); // 将线程设置为守护线程
t.start();
}
}

上面创建一个守护线程,与上面的程序执行同样的任务。最终的结果(可能每台机器会有差异)是程序结束时什么都没打印。main函数是Java程序的入口,在启动程序时会同时启动一个main线程来执行main函数中的代码。main线程是一个用户线程,当执行完t.start()代码后,main就已经运行完毕了。此时,该程序不存在任何存活的用户线程,在t还没来得及运行时,程序就直接终止了。

Java垃圾回收线程就是一个典型的守护线程,当我们的程序中不再有任何运行中的用户线程时,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,Java虚拟机会自动离开。

线程通信

每个Java线程独享执行栈、程序计数器等资源。在多核处理器中,多线程可以真正的并行运行,相较单线程程序,其运行效率大大提高。理想情况下,每个线程应该是独立运行的,多个线程之间不会牵扯。但是,在实际应用中,存在线程依赖或排斥等需求,如消费者线程和生产者线程。这时候就需要线程通信机制,来保证多线程程序“安全”运行。下面介绍Java中的主要线程通信方式:线程同步和wait/notify机制。

线程同步

同步中的“同”应为协同、协助、互相配合之意。所谓线程同步,是指多个线程协同完成一个任务。Java中的可以实现同步的方法包括synchronized以及其他的锁(如ReetrantLock),其中synchronized是Java中的关键字。本节所指的线程同步特指使用synchronized关键字实现的线程同步。

下面的例子是2个线程使用同一个变量count进行计数,每个线程均计1000次。如果正常的话,可以预期count变量最终的值应该是2000。在运行多次之后,count最终的值出现了1998、1999、2000等情况。出现这种结果的原因是++count操作不是原子的,会编译成多条指令。

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
public class ThreadCommunication1 {
private static int count = 0;
public static void main(String[] args) {
// 开两个线程,同时对count变量进行累加1000次。正常情况下,最后count应该等于2000。
// 但是,实际运行情况是,多次运行之后,最后会出现count=1998、1999、2000等情况。
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("thread:" + this.getName() + ",count=" + (++count));
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("thread:" + this.getName() + ",count=" + (++count));
}
}
}.start();
}
}

这种情况就需要多个线程协同执行,线程之间商量好,我执行的时候其他线程都等着,等我执行完了之大家再继续执行。下面的代码加上了synchronized关键字。

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
public class ThreadCommunication1 {
private static int count = 0;
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (ThreadCommunication1.class) { // 这里加了同步代码
System.out.println("thread:" + this.getName() + ",count=" + (++count));
}
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (ThreadCommunication1.class) { // 这里加了同步代码
System.out.println("thread:" + this.getName() + ",count=" + (++count));
}
}
}
}.start();
}
}

上面的代码将++count操作包裹synchronized程序块内,同一时间只能有一个线程对象(ThreadCommunication1.class)的锁。在上面的代码无论运行多少次,count最终的结果都是2000。Java中的synchronized包含了互斥的语义,无论什么操作,同一时间内,只有一个线程可以获得同步锁并执行。使用synchronized只能实现排他锁,无法直接实现共享锁。

wait/notify机制

Java中的继承关系是一种树形结构,树的根节点就是Object类,其他所有的类(或接口)都是从这个类派生出来的。Object类中定义了一些与线程通信相关的方法:

  • wait():当前线程放弃自己所持的锁,并进入等待状态。
  • notify():唤醒正在处理等待(wait)状态的某一个线程。
  • notifyAll():唤醒正在处于等待(wait)状态的所有线程。

上面的方法是本地(native)方法,或者间接调用本地方法。在使用这些线程通信方法时有些要特别注意的点。

(一)在调用这些方法之前,当前线程必须持有对象的锁。从源代码层面上来说,这些方法必须包含在synchronized代码块内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WaitWithoutSync {
public void dothing() {
try {
this.wait(); // 直接调用wait方法。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WaitWithoutSync waitWithoutSync = new WaitWithoutSync();
waitWithoutSync.dothing(); // 抛出java.lang.IllegalMonitorStateException
}
}

上面是直接调用wait()方法的一段代码,运行后抛出java.lang.IllegalMonitorStateException异常。正确的调用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WaitWithoutSync {
public void dothing() {
synchronized (this) { // 将wait方法由对应的同步块包裹起来
try {
this.wait(); // 直接调用wait方法。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
WaitWithoutSync waitWithoutSync = new WaitWithoutSync();
waitWithoutSync.dothing(); // 抛出java.lang.IllegalMonitorStateException
}
}

(二)当调用notify()或者notifyAll()方法后并不会立刻释放锁,而是要等到同步块内部的代码全部运行完毕后才会释放锁并唤醒等待线程。

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
public class NotifyTest {
private Object lock = new Object();
private void await() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait完毕 ");
}
}
private void anotifyAll() {
synchronized (lock) {
lock.notifyAll();
System.out.println("notify完毕 ");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡眠5秒钟....");
}
}
public static void main(String[] args) {
final NotifyTest notifyTest = new NotifyTest();
new Thread() { // 启用一个线程,调用等待方法
@Override
public void run() {
notifyTest.await();
}
}.start();
new Thread() { // 启用另外一个线程,调用唤醒方法
@Override
public void run() {
notifyTest.anotifyAll();
}
}.start();
}
}

上面代码的运行结果如下:

1
2
3
notify完毕
睡眠5秒钟....
wait完毕

也就是说,在调用lock.notifyAll();方法之后并没有立刻唤醒等待线程,而是待同步块内的逻辑全部运行完毕之后,才会真正地唤醒等待线程。

(三)notify和notifyAll的区别

notifyAll:使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify:只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

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
61
62
63
64
65
public class NotifyTest2 {
private void anotify() {
while (true) {
try {
Thread.sleep(4000);
System.out.println("等待4秒结束");
synchronized (this) {
this.notify();
System.out.println("notify完毕");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void await() {
synchronized (this) {
try {
System.out.println("开始wait");
this.wait();
System.out.println("wait完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
final NotifyTest2 notifyTest2 = new NotifyTest2();
new Thread() { // 创建一个notify线程
@Override
public void run() {
notifyTest2.anotify();
}
}.start();
new Thread() { // 创建一个wait线程
@Override
public void run() {
notifyTest2.await();
}
}.start();
new Thread() { // 创建一个wait线程
@Override
public void run() {
notifyTest2.await();
}
}.start();
new Thread() { // 创建一个wait线程
@Override
public void run() {
notifyTest2.await();
}
}.start();
}
}

上面的anotify()方法循环调用notify方法,每次调用完毕后,休眠4秒钟。主线程中调起一个notify线程和3个wait线程,由于休眠了4秒钟后才会第1次执行notify方法,所以在这之前,3个wait线程都会执行完wait()方法并阻塞当前线程。当执行notify()方法后,唤醒一个wait线程。所以最终的执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
开始wait
开始wait
开始wait
等待4秒结束
notify完毕
wait完毕
等待4秒结束
notify完毕
wait完毕
等待4秒结束
notify完毕
wait完毕

下面将notify()方法改为notifyAll()方法,其他代码均不变。

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
61
62
63
64
65
public class NotifyTest2 {
private void anotify() {
while (true) {
try {
Thread.sleep(4000);
System.out.println("等待4秒结束");
synchronized (this) {
this.notifyAll(); // 这里改为调用notifyAll方法
System.out.println("notify完毕");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void await() {
synchronized (this) {
try {
System.out.println("开始wait");
this.wait();
System.out.println("wait完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
final NotifyTest2 notifyTest2 = new NotifyTest2();
new Thread() { // 创建一个notify线程
@Override
public void run() {
notifyTest2.anotify();
}
}.start();
new Thread() { // 创建一个wait线程
@Override
public void run() {
notifyTest2.await();
}
}.start();
new Thread() { // 创建一个wait线程
@Override
public void run() {
notifyTest2.await();
}
}.start();
new Thread() { // 创建一个wait线程
@Override
public void run() {
notifyTest2.await();
}
}.start();
}
}

由于第一次调用notifyAll()方法后,3个wait线程均已经被唤醒,此时只需争用到锁即可以继续执行,而无需再次被唤醒。上面的代码的执行结果如下:

1
2
3
4
5
6
7
8
开始wait
开始wait
开始wait
等待4秒结束
notify完毕
wait完毕
wait完毕
wait完毕

Thread中的重要方法

Thread.sleep(time):放弃CPU机会,等待time时间后,线程变成可运行状态。值得注意的是,如果当前线程持有锁,执行这个方法并不会释放锁。
Thread.yield():让出CPU,使得有相同优先级的线程有机会执行。大多数情况下,将线程从运行状态转为可运行状态,然而,下次仍有可能运行这个线程。
threadInstance.join():将线程threadInstance加入当前运行线程,只有当threadInstance运行完毕之后,当前线程才能继续往下运行。


本文由xialei原创,转载请说明出处http://hinylover.space/2016/09/17/relearn-java-thread/