1. 线程状态

  线程共包括以下5种状态。

  1. 新建状态(New):线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
  • 等待阻塞——通过调用线程的wait()方法,让线程等待某工作的完成。
  • 同步阻塞——线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  • 其他阻塞——通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
    这5种状态涉及到的内容包括Object类, Thread和synchronized关键字。这些内容我们会在后面的章节中逐个进行学习。

Object类,定义了wait()、 notify()、notifyAll()等休眠/唤醒函数。

Thread类,定义了一些列的线程操作函数。例如,sleep()休眠函数, interrupt()中断函数, getName()获取线程名称等。

synchronized,是关键字;它区分为synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁。

在后面详细介绍wait(),notify()等方法时,我们会分析为什么“wait(), notify()等方法要定义在Object类,而不是Thread类中”。

2. 实现多线程的两种方式

2.1 Thread和Runnable简介

  Thread 是一个类。Thread本身就实现了Runnable接口。它的声明如下:

1
public class Thread implements Runnable {}

  Thread的作用,实现多线程。
  Runnable 是一个接口,该接口中只包含了一个run()方法。它的定义如下:

1
2
3
public interface Runnable {
public abstract void run();
}

  Runnable的作用,实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。

2.2 Thread的多线程示例

  下面通过示例更好的理解Thread和Runnable,借鉴网上一个例子比较具有说服性的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyThread extends Thread{  
private int ticket=10;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(this.getName()+" 卖票:ticket"+this.ticket--);
}
}
}
};
public class ThreadTest {
public static void main(String[] args) {
// 启动3个线程t1,t2,t3;每个线程各卖10张票!
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}

输出结果一部分:

1
2
3
4
5
6
7
8
9
10
11
Thread-2买票:Ticket100
Thread-2买票:Ticket99
Thread-2买票:Ticket98
Thread-2买票:Ticket97
Thread-0买票:Ticket100
Thread-0买票:Ticket99
Thread-0买票:Ticket98
Thread-1买票:Ticket100
Thread-2买票:Ticket96
Thread-0买票:Ticket97
....

结果说明

  • MyThread继承于Thread,它是自定义个线程。每个MyThread都会卖出10张票。
  • 主线程main创建并启动3个MyThread子线程。每个子线程都各自卖出了10张票。

    2..2 Runnable的多线程示例

      下面,我们对上面的程序进行修改。通过Runnable实现一个接口,从而实现多线程。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class MyThread implements Runnable{  
    private int ticket=10;
    public void run(){
    for(int i=0;i<20;i++){
    if(this.ticket>0){
    System.out.println(Thread.currentThread().getName()+" 卖票:ticket"+this.ticket--);
    }
    }
    }
    };
    public class RunnableTest {
    public static void main(String[] args) {
    MyThread mt=new MyThread();
    // 启动3个线程t1,t2,t3(它们共用一个Runnable对象),这3个线程一共卖10张票!
    Thread t1=new Thread(mt);
    Thread t2=new Thread(mt);
    Thread t3=new Thread(mt);
    t1.start();
    t2.start();
    t3.start();
    }
    }
    输出结果一部分:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    hread-2买票:Ticket0
    Thread-0买票:Ticket0
    Thread-1买票:Ticket0
    Thread-0买票:Ticket1
    Thread-2买票:Ticket1
    Thread-0买票:Ticket2
    Thread-1买票:Ticket1
    Thread-0买票:Ticket3
    Thread-2买票:Ticket2
    Thread-0买票:Ticket4
    Thread-1买票:Ticket2
    Thread-0买票:Ticket5
    Thread-2买票:Ticket3
    Thread-0买票:Ticket6
    ....
  • 结果说明*
  • 和上面“MyThread继承于Thread”不同;这里的MyThread实现了Thread接口。
  • 主线程main创建并启动3个子线程,而且这3个子线程都是基于“mt这个Runnable对象”而创建的。运行结果是这3个子线程一共卖出了10张票。这说明它们是共享了MyThread接口的。

    2.3 Thread和Runnable的异同点:

  • 相同点:*
    ​ 都是“多线程的实现方式”。
  • 不同点:*
    ​ Thread 是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。我们知道“一个类只能有一个父类,但是却能实现多个接口”,因此Runnable具有更好的扩展性。
    ​ 此外,Runnable还可以用于“资源的共享”。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。通常,建议通过“Runnable”实现多线程!

    2.4 start() 和 run()的区别说明

    start():它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。  
    run():run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

下面,通过一个简单示例演示它们之间的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread{  
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
public class Demo {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+" call mythread.run()");
mythread.run();
System.out.println(Thread.currentThread().getName()+" call mythread.start()");
mythread.start();
}
}

运行结果:

1
2
3
4
main call myThread.run()
main
main call myThread.start()
Thread-0

结果说明

  • Thread.currentThread().getName()是用于获取“当前线程”的名字。当前线程是指正在cpu中调度执行的线程。
  • myThread.run()是在“主线程main”中调用的,该run()方法直接运行在“主线程main”上。
  • myRhread.start()会启动“线程myThread”,“线程myThread”启动之后,会调用run()方法;此时的run()方法是运行在“线程myThread”上。

    3. synchronized——同步锁

    3.1 synchronized原理

  在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。
  不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

3.2 synchronized基本规则

  • 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
  • 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块
  • 当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

1)当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.start();
t2.start();
}
}

class MyRunnable implements Runnable{
@Override
public void run() {
synchronized (this) {
System.out.println(this.getClass().getName());
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
thread.MyRunnable
Thread-0 , 0
Thread-0 , 1
Thread-0 , 2
Thread-0 , 3
...
thread.MyRunnable
Thread-1 , 0
Thread-1 , 1
Thread-1 , 2
Thread-1 , 3
...

2)当一个线程访问“某对象”的“synchronized方法”或者“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
34
35
36
37
38
39
40
41
42
43
44
45
46
public class synchronizedDemo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Count count = new Count();
count.isSynchronous();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
try {
Count count = new Count();
count.unSynchronous();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

class Count {
//同步方法
public void isSynchronous() throws InterruptedException {
synchronized (this) {
for (int i = 0; i < 100; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
}
//非同步方法
public void unSynchronous() throws InterruptedException {
for (int i = 0; i < 100; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
}

输出结果一部分:

1
2
3
4
5
6
7
8
9
10
11
Thread-0 , 0
Thread-1 , 0
Thread-0 , 1
Thread-1 , 1
Thread-0 , 2
Thread-1 , 2
Thread-0 , 3
Thread-1 , 3
Thread-0 , 4
Thread-1 , 4
...

3)当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class synchronizedDemo {
public static void main(String[] args) {
Count count = new Count();
Thread t1 = new Thread(){
@Override
public void run() {
try {
count.isSynchronous1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
// Count count = new Count();
//如果每个线程中的实例对象不同,那么就是两个锁对象,两个线程能并发运行
try {
count.isSynchronous2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
}
}

class Count {
public void isSynchronous1() throws InterruptedException {
synchronized (this) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}

}
Object object = new Object();
public void isSynchronous2() throws InterruptedException {
//如果将this换成另一个对象,那么两个线程还是能并发运行的
synchronized (this) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread-0 , 0
Thread-0 , 1
Thread-0 , 2
Thread-0 , 3
Thread-0 , 4
Thread-0 , 5
...
Thread-1 , 0
Thread-1 , 1
Thread-1 , 2
Thread-1 , 3
Thread-1 , 4
Thread-1 , 5
...

3. synchronized方法 和 synchronized代码块

  “synchronized方法”是用synchronized修饰方法,而 “synchronized代码块”则是用synchronized修饰代码块。
synchronized方法

1
2
3
public synchronized void foo1() {
System.out.println("synchronized methoed");
}

synchronized代码块

1
2
3
4
5
public void foo2() {
synchronized (this) {
System.out.println("synchronized methoed");
}
}

  synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。
  synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。

4. 实例锁 和 全局锁

实例锁——锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。实例锁对应的就是synchronized关键字。

全局锁——该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

关于“实例锁”和“全局锁”有一个很形象的例子:

1
2
3
4
5
6
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}

假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况。

(01) x.isSyncA()与x.isSyncB()

(02) x.isSyncA()与y.isSyncA()

(03) x.cSyncA()与y.cSyncB()

(04) x.isSyncA()与Something.cSyncA()

(01) 不能被同时访问。因为isSyncA()和isSyncB()都是访问同一个对象(对象x)的同步锁!

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
class Something {
public synchronized void isSyncA(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
public synchronized void isSyncB(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
public static synchronized void staSyncA(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
public static synchronized void staSyncB(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " , " + i);
}
}
}

public class SynDemo {

static Something s1 = new Something();
static Something s2 = new Something();

public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
s1.isSyncA();
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
s1.isSyncB();
}
}).start();
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread-0 , 0
Thread-0 , 1
Thread-0 , 2
Thread-0 , 3
Thread-0 , 4
Thread-0 , 5
...
Thread-1 , 0
Thread-2 , 1
Thread-3 , 2
Thread-4 , 3
Thread-5 , 4
Thread-6 , 5
...

(02) 可以同时被访问。因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
s1.isSyncA();
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
s2.isSyncB();
}
}).start();

输出结果:

1
2
3
4
5
6
7
8
9
Thread-1 , 0
Thread-0 , 0
Thread-1 , 1
Thread-0 , 1
Thread-1 , 2
Thread-0 , 2
Thread-1 , 3
Thread-0 , 3
...

(03) 不能被同时访问。因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LockTest3 {
static Something s1 = new Something();
static Something s2 = new Something();

public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
s1.staSyncA();
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
s2.staSyncB();
}
}).start();
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread-0 , 0
Thread-0 , 1
Thread-0 , 2
Thread-0 , 3
Thread-0 , 4
Thread-0 , 5
...
Thread-1 , 0
Thread-2 , 1
Thread-3 , 2
Thread-4 , 3
Thread-5 , 4
Thread-6 , 5
...

(04) 可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LockTest4 {
static Something s1 = new Something();
static Something s2 = new Something();

public static void main(String[] args){
new Thread(new Runnable() {
@Override
public void run() {
s2.isSyncA();
}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
s2.staSyncB();
}
}).start();
}
}

输出结果:

1
2
3
4
5
6
7
8
9
Thread-1 , 0
Thread-0 , 0
Thread-1 , 1
Thread-0 , 1
Thread-1 , 2
Thread-0 , 2
Thread-1 , 3
Thread-0 , 3
...

4. 线程等待与唤醒

4.1 wait(), notify(), notifyAll()等方法介绍

 在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
​ Object类中关于等待/唤醒的API详细信息如下:
| 方法 | 作用 |
| —————————– | ———————————————————— |
| notify() | 唤醒在此对象监视器上等待的单个线程 |
| notifyAll() | 唤醒在此对象监视器上等待的所有线程 |
| wait() | 让当前线程处于“等待(阻塞)状态”,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,当前线程被唤醒(进入“就绪状态”) |
| wait(long timeout) | 让当前线程处于“等待(阻塞)状态”,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量,当前线程被唤醒(进入“就绪状态”) |
| wait(long timeout, int nanos) | 让当前线程处于“等待(阻塞)状态”,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量,当前线程被唤醒(进入“就绪状态”) |

4.2 wait(long timeout)和notify()

  wait(long timeout)会让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
下面的示例就是演示wait(long timeout)在超时情况下,线程被唤醒的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WaitTest {
public static void main(String[] args) throws InterruptedException {
MyThread1 myThread = new MyThread1();
synchronized(myThread) {
System.out.println(Thread.currentThread().getName() + " start thread");
myThread.start();
System.out.println(Thread.currentThread().getName() + " call wait");
myThread.wait(30000);
System.out.println(Thread.currentThread().getName() + " continue");
}
}
}

class MyThread1 extends Thread {
@Override
public void run() {
for(int i = 1; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " run");
}
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
main  start thread
main call wait
Thread-0 run
Thread-0 run
Thread-0 run
Thread-0 run
Thread-0 run
Thread-0 run
Thread-0 run
Thread-0 run
Thread-0 run
main continue

结果说明:
(01) 对于synchronized里面的方法,如果里面的方法如果不全是锁启动的方法,那么里面的方法还是会CPU切换运行
(02) 对于让一个线程在运行过程中wait,那么需要使用flag来判断

4.4 wait() 和 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
public class NotifyAllTest {
static Object object = new Object();
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
Thread t3 = new Thread(myRun);
//开启三个线程
t1.start();
t2.start();
t3.start();

try {
System.out.println(Thread.currentThread().getName() + " , " + "sleep(3000)");
//让主线程休眠3000ms
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

synchronized (object) {
System.out.println(Thread.currentThread().getName() + " , " + "notifyAll");
//让object锁唤醒所有线程
object.notifyAll();
}
}

static class MyRun implements Runnable {
@Override
public void run() {
synchronized (object) {
try {
System.out.println(Thread.currentThread().getName() + " , " + "wait");
//让t1、t2、t3运行到这儿时进入等待状态,等待object锁唤醒
object.wait();
System.out.println(Thread.currentThread().getName() + " , " + "continue");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

输出结果:

1
2
3
4
5
6
7
8
main , sleep(3000)
Thread-0 , wait
Thread-2 , wait
Thread-1 , wait
main , notifyAll
Thread-0 , continue
Thread-1 , continue
Thread-2 , continue

参考下面的流程图。

(01) 主线程中新建并且启动了3个线程”t1”, “t2”和”t3”。
(02) 主线程通过sleep(3000)休眠3秒。在主线程休眠3秒的过程中,我们假设”t1”, “t2”和”t3”这3个线程都运行了。以”t1”为例,当它运行的时候,它会执行obj.wait()等待其它线程通过notify()或额nofityAll()来唤醒它;相同的道理,”t2”和”t3”也会等待其它线程通过nofity()或nofityAll()来唤醒它们。
(03) 主线程休眠3秒之后,接着运行。执行 obj.notifyAll() 唤醒obj上的等待线程,即唤醒”t1”, “t2”和”t3”这3个线程。 紧接着,主线程的synchronized(obj)运行完毕之后,主线程释放“obj锁”。这样,”t1”, “t2”和”t3”就可以获取“obj锁”而继续运行了!

4.5 为什么notify(), wait()等函数定义在Object中,而不是Thread中

 Object中的wait(),notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!
​ OK,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据对象的同步
​ 总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

其他注意问题

5.1 sleep() 与 wait()的比较

  我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。

  但是,wait()会释放对象的同步锁,而sleep()则不会释放锁。

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
// SleepLockTest.java的源码
public class SleepLockTest{

private static Object obj = new Object();

public static void main(String[] args){
ThreadA t1 = new ThreadA("t1");
ThreadA t2 = new ThreadA("t2");
t1.start();
t2.start();
}

static class ThreadA extends Thread{
public ThreadA(String name){
super(name);
}
public void run(){
// 获取obj对象的同步锁
synchronized (obj) {
try {
for(int i=0; i <10; i++){
System.out.printf("%s: %d\n", this.getName(), i);
// i能被4整除时,休眠100毫秒
if (i%4 == 0)
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

结果说明:
  主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(obj)。在t1运行过程中,虽然它会调用Thread.sleep(100);但是,t2是不会获取cpu执行权的。因为,t1并没有释放“obj所持有的同步锁”!
  注意,若我们注释掉synchronized (obj)后再次执行该程序,t1和t2是可以相互切换的。

5.2 join()介绍

  join() 定义在Thread.java中。

  join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 主线程
public class Father extends Thread {
public void run() {
Son s = new Son();
s.start();
s.join();
...
}
}
// 子线程
public class Son extends Thread {
public void run() {
...
}
}

  上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。

  在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {

Thread1 t1 = new Thread1();
t1.start();
  //当cpu切换到join的时候,主线程会进入堵塞状态,等待t1运行结束再运行
  t1.join();
for (int i = 0; i < 100; i++) {
System.out.println("mian " + i);
}
}

static class Thread1 extends Thread {
@Override
public void run() {
System.out.println("start " + Thread.currentThread().getName());
for (int i = 0; i < 100; i++) {
System.out.println("t1 " + i);
}
System.out.println("finish " + Thread.currentThread().getName());
}
}
}

运行流程如图

输出结果:

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
没有使用join的时候:
...
mian 44
start Thread-0
mian 45
mian 46
mian 47
mian 48
mian 49
mian 50
mian 51
mian 52
mian 53
t1 0
mian 54
...
使用join的时候:
start Thread-0
t1 0
t1 1
t1 2
t1 3
t1 4
...
finish Thread-0
mian 0
mian 1
mian 2
mian 3
mian 4
...

(01) 在“主线程main”中通过 new ThreadA(“t1”) 新建“线程t1”。 接着,通过 t1.start() 启动“线程t1”,并执行t1.join()。
(02) 执行t1.join()之后,“主线程main”会进入“阻塞状态”等待t1运行结束。“子线程t1”结束之后,会唤醒“主线程main”,“主线程”重新获取cpu执行权,继续运行。

5.3 终止线程的方式

 Thread中的stop()和suspend()方法,由于固有的不安全性,已经建议不再使用!
 线程在“阻塞状态”和“运行状态”的终止方式

5.3.1 终止处于“阻塞状态”的线程

通常,我们通过“中断”方式终止处于“阻塞状态”的线程。
当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程,形式如下:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void run() {
try {
while (true) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 由于产生InterruptedException异常,退出while(true)循环,线程终止!
}
}
​`

说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!
注意:对InterruptedException的捕获务一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。否则,InterruptedException在while(true)循环体之内,就需要额外的添加退出处理。形式如下:
​``` Java
@Override
public void run() {
while (true) {
try {
// 执行任务…
} catch (InterruptedException ie) {
// InterruptedException在while(true)循环体内。
// 当线程产生了InterruptedException异常时,while(true)仍能继续运行!需要手动退出
break;
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
说明:上面的InterruptedException异常的捕获在whle(true)之内。当产生InterruptedException异常时,被catch处理之外,仍然在while(true)循环体内;要退出while(true)循环体,需要额外的执行退出while(true)的操作。

#### 5.3.2 终止处于“运行状态”的线程
通常,我们通过“标记”方式终止处于“运行状态”的线程。其中,包括“中断标记”和“额外添加标记”。
(01) 通过“中断标记”终止线程。
形式如下:

``` Java
@Override
public void run() {
while (!isInterrupted()) {
// 执行任务...
}
}

说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。
注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

(02) 通过“额外添加标记”。
形式如下:

1
2
3
4
5
6
7
8
9
10
11
private volatile boolean flag= true;
protected void stopTask() {
flag = false;
}

@Override
public void run() {
while (flag) {
// 执行任务...
}
}

说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。
综合线程处于“阻塞状态”和“运行状态”的终止方式,比较通用的终止线程的形式如下:

1
2
3
4
5
6
7
8
9
10
11
@Override
public void run() {
try {
// 1. isInterrupted()保证,只要中断标记为true就终止线程。
while (!isInterrupted()) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
}
}

线程只有在循环调用某段代码才会用while。外部如果达到一定条件,就会调用interrupt方法或设置flag标记为false让线程跳出while循环。
这才是中断的用处所在。如果线程只是顺序执行一段代码,等于是昙花一现,根本就没必要中断(但是try catch必须要,防止sleep等方法执行时中断)。

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
// Demo1.java的源码
class MyThread extends Thread {

public MyThread(String name) {
super(name);
}

@Override
public void run() {
try {
int i=0;
while (!isInterrupted()) {
Thread.sleep(100); // 休眠100ms
i++;
System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
}
}
}

public class Demo1 {

public static void main(String[] args) {
try {
Thread t1 = new MyThread("t1"); // 新建“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is new.");

t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started.");

// 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

// 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

结果说明:
(01) 主线程main中通过new MyThread(“t1”)创建线程t1,之后通过t1.start()启动线程t1。
(02) t1启动之后,会不断的检查它的中断标记,如果中断标记为“false”;则休眠100ms。
(03) t1休眠之后,会切换到主线程main;主线程再次运行时,会执行t1.interrupt()中断线程t1。t1收到中断指令之后,会将t1的中断标记设置“false”,而且会抛出InterruptedException异常。在t1的run()方法中,是在循环体while之外捕获的异常;因此循环被终止。

我们对上面的结果进行小小的修改,将run()方法中捕获InterruptedException异常的代码块移到while循环体内。

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
// Demo2.java的源码
class MyThread extends Thread {

public MyThread(String name) {
super(name);
}

@Override
public void run() {
int i=0;
while (!isInterrupted()) {
try {
Thread.sleep(100); // 休眠100ms
} catch (InterruptedException ie) {
System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
}
i++;
System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
}
}
}

public class Demo2 {

public static void main(String[] args) {
try {
Thread t1 = new MyThread("t1"); // 新建“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is new.");

t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started.");

// 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

// 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

程序进入了死循环!
为什么会这样呢?这是因为,t1在“等待(阻塞)状态”时,被interrupt()中断;此时,会清除中断标记[即isInterrupted()会返回false],而且会抛出InterruptedException异常[该异常在while循环体内被捕获]。因此,t1理所当然的会进入死循环了。
解决该问题,需要我们在捕获异常时,额外的进行退出while循环的处理。例如,在MyThread的catch(InterruptedException)中添加break 或 return就能解决该问题。
下面是通过“额外添加标记”的方式终止“状态状态”的线程的示例:

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
// Demo3.java的源码
class MyThread extends Thread {

private volatile boolean flag= true;
public void stopTask() {
flag = false;
}

public MyThread(String name) {
super(name);
}

@Override
public void run() {
synchronized(this) {
try {
int i=0;
while (flag) {
Thread.sleep(100); // 休眠100ms
i++;
System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);
}
} catch (InterruptedException ie) {
System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");
}
}
}
}

public class Demo3 {

public static void main(String[] args) {
try {
MyThread t1 = new MyThread("t1"); // 新建“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is new.");

t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started.");

// 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.stopTask();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");

// 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
联系我

评论