Java—多线程编程

时间:2021-10-13 20:38:00

一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。

进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守候线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用CPU的目的。

一 生命周期

Java—多线程编程

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

二 线程的创建与启动

Java中线程的创建常见有如三种基本形式

1.继承Thread类,重写该类的run()方法。

创建一个新的类,该类继承Thread类,然后创建一个该类的实例。

继承类必须重写run()方法,该方法是新线程的入口点。它也必须调用start()方法才能执行。

Java—多线程编程
 1 class MyThread extends Thread {
2
3 private int i = 0;
4
5 @Override
6 public void run() {
7 for (i = 0; i < 100; i++) {
8 System.out.println(Thread.currentThread().getName() + " " + i);
9 }
10 }
11 }
Java—多线程编程
Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 for (int i = 0; i < 100; i++) {
5 System.out.println(Thread.currentThread().getName() + " " + i);
6 if (i == 30) {
7 Thread myThread1 = new MyThread(); // 创建一个新的线程 myThread1 此线程进入新建状态
8 Thread myThread2 = new MyThread(); // 创建一个新的线程 myThread2 此线程进入新建状态
9 myThread1.start(); // 调用start()方法使得线程进入就绪状态
10 myThread2.start(); // 调用start()方法使得线程进入就绪状态
11 }
12 }
13 }
14 }
Java—多线程编程

输出结果:(多线程执行)存在交叉性。

如上所示,继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。

2.实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

创建一个线程,最简单的方法是创建一个实现Runnable接口的类。为了实现Runnable,一个类只需要执行一个方法调用run(),

Java—多线程编程
 1 class MyRunnable implements Runnable {
2 private int i = 0;
3
4 @Override
5 public void run() {
6 for (i = 0; i < 100; i++) {
7 System.out.println(Thread.currentThread().getName() + " " + i);
8 }
9 }
10 }
Java—多线程编程
Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 for (int i = 0; i < 100; i++) {
5 System.out.println(Thread.currentThread().getName() + " " + i);
6 if (i == 30) {
7 Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
8 Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
9 Thread thread2 = new Thread(myRunnable);
10 thread1.start(); // 调用start()方法使得线程进入就绪状态
11 thread2.start();
12 }
13 }
14 }
15 }
Java—多线程编程

相信以上两种创建新线程的方式大家都很熟悉了,那么Thread和Runnable之间到底是什么关系呢?我们首先来看一下下面这个例子。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 for (int i = 0; i < 100; i++) {
5 System.out.println(Thread.currentThread().getName() + " " + i);
6 if (i == 30) {
7 Runnable myRunnable = new MyRunnable();
8 Thread thread = new MyThread(myRunnable);
9 thread.start();
10 }
11 }
12 }
13 }
14
15 class MyRunnable implements Runnable {
16 private int i = 0;
17
18 @Override
19 public void run() {
20 System.out.println("in MyRunnable run");
21 for (i = 0; i < 100; i++) {
22 System.out.println(Thread.currentThread().getName() + " " + i);
23 }
24 }
25 }
26
27 class MyThread extends Thread {
28
29 private int i = 0;
30
31 public MyThread(Runnable runnable){
32 super(runnable);
33 }
34
35 @Override
36 public void run() {
37 System.out.println("in MyThread run");
38 for (i = 0; i < 100; i++) {
39 System.out.println(Thread.currentThread().getName() + " " + i);
40 }
41 }
42 }
Java—多线程编程

同样的,与实现Runnable接口创建线程方式相似,不同的地方在于

1 Thread thread = new MyThread(myRunnable);

那么这种方式可以顺利创建出一个新的线程么?答案是肯定的。至于此时的线程执行体到底是MyRunnable接口中的run()方法还是MyThread类中的run()方法呢?通过输出我们知道线程执行体是MyThread类中的run()方法。其实原因很简单,因为Thread类本身也是实现了Runnable接口,而run()方法最先是在Runnable接口中定义的方法。

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

我们看一下Thread类中对Runnable接口中run()方法的实现:

Java—多线程编程
  @Override
public void run() {
if (target != null) {
target.run();
}
}
Java—多线程编程

也就是说,当执行到Thread类中的run()方法时,会首先判断target是否存在,存在则执行target中的run()方法,也就是实现了Runnable接口并重写了run()方法的类中的run()方法。但是上述给到的列子中,由于多态的存在,根本就没有执行到Thread类中的run()方法,而是直接先执行了运行时类型即MyThread类中的run()方法。

3.使用Callable和Future接口创建线程。具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

看着好像有点复杂,直接来看一个例子就清晰了。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4
5 Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象
6 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象
7
8 for (int i = 0; i < 100; i++) {
9 System.out.println(Thread.currentThread().getName() + " " + i);
10 if (i == 30) {
11 Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
12 thread.start(); //线程进入到就绪状态
13 }
14 }
15
16 System.out.println("主线程for循环执行完毕..");
17
18 try {
19 int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
20 System.out.println("sum = " + sum);
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 } catch (ExecutionException e) {
24 e.printStackTrace();
25 }
26
27 }
28 }
29
30
31 class MyCallable implements Callable<Integer> {
32 private int i = 0;
33
34 // 与run()方法不同的是,call()方法具有返回值
35 @Override
36 public Integer call() {
37 int sum = 0;
38 for (; i < 100; i++) {
39 System.out.println(Thread.currentThread().getName() + " " + i);
40 sum += i;
41 }
42 return sum;
43 }
44
45 }
Java—多线程编程

首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:

1 public class FutureTask<V> implements RunnableFuture<V> {
2
3 //....
4
5 }
1 public interface RunnableFuture<V> extends Runnable, Future<V> {
2
3 void run();
4
5 }

于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。

执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕..”则很可能是在子线程循环中间输出。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕..”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?

原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。

上述主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。

三. Java多线程的就绪、运行和死亡状态

就绪状态转换为运行状态:当此线程得到处理器资源;

运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。

运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。

此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。

由于实际的业务需要,常常会遇到需要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的做法是设置一boolean型的变量,当条件满足时,使线程执行体快速执行完毕。如:

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4
5 MyRunnable myRunnable = new MyRunnable();
6 Thread thread = new Thread(myRunnable);
7
8 for (int i = 0; i < 100; i++) {
9 System.out.println(Thread.currentThread().getName() + " " + i);
10 if (i == 30) {
11 thread.start();
12 }
13 if(i == 40){
14 myRunnable.stopThread();
15 }
16 }
17 }
18 }
19
20 class MyRunnable implements Runnable {
21
22 private boolean stop;
23
24 @Override
25 public void run() {
26 for (int i = 0; i < 100 && !stop; i++) {
27 System.out.println(Thread.currentThread().getName() + " " + i);
28 }
29 }
30
31 public void stopThread() {
32 this.stop = true;
33 }
34
35 }
Java—多线程编程

四.Java多线程的阻塞状态与线程控制

上文已经提到Java阻塞的几种具体类型。下面分别看下引起Java线程阻塞的主要方法。

1.join()

join —— 让一个线程等待另一个线程完成才继续执行。如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,知道B线程执行完为止,A才能得以继续执行。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4
5 MyRunnable myRunnable = new MyRunnable();
6 Thread thread = new Thread(myRunnable);
7
8 for (int i = 0; i < 100; i++) {
9 System.out.println(Thread.currentThread().getName() + " " + i);
10 if (i == 30) {
11 thread.start();
12 try {
13 thread.join(); // main线程需要等待thread线程执行完后才能继续执行
14 } catch (InterruptedException e) {
15 e.printStackTrace();
16 }
17 }
18 }
19 }
20 }
21
22 class MyRunnable implements Runnable {
23
24 @Override
25 public void run() {
26 for (int i = 0; i < 100; i++) {
27 System.out.println(Thread.currentThread().getName() + " " + i);
28 }
29 }
30 }
Java—多线程编程

2.sleep()

sleep —— 让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,出于sleep()中的线程也不会执行。因此sleep()方法常用来暂停线程执行。

前面有讲到,当调用了新建的线程的start()方法后,线程进入到就绪状态,可能会在接下来的某个时间获取CPU时间片得以执行,如果希望这个新线程必然性的立即执行,直接调用原来线程的sleep(1)即可。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4
5 MyRunnable myRunnable = new MyRunnable();
6 Thread thread = new Thread(myRunnable);
7
8 for (int i = 0; i < 100; i++) {
9 System.out.println(Thread.currentThread().getName() + " " + i);
10 if (i == 30) {
11 thread.start();
12 try {
13 Thread.sleep(1); // 使得thread必然能够马上得以执行
14 } catch (InterruptedException e) {
15 e.printStackTrace();
16 }
17 }
18 }
19 }
20 }
21
22 class MyRunnable implements Runnable {
23
24 @Override
25 public void run() {
26 for (int i = 0; i < 100; i++) {
27 System.out.println(Thread.currentThread().getName() + " " + i);
28 }
29 }
30 }
Java—多线程编程

注:睡一个毫秒级够了,因为CPU不会空闲,会切换到新建的线程。

3.后台线程(Daemon Thread)

概念/目的:后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。如JVM中的垃圾回收线程。

生命周期:后台线程的生命周期与前台线程生命周期有一定关联。主要体现在:当所有的前台线程都进入死亡状态时,后台线程会自动死亡(其实这个也很好理解,因为后台线程存在的目的在于为前台线程服务的,既然所有的前台线程都死亡了,那它自己还留着有什么用...伟大啊 ! !)。

设置后台线程:调用Thread对象的setDaemon(true)方法可以将指定的线程设置为后台线程。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 Thread myThread = new MyThread();
5 for (int i = 0; i < 100; i++) {
6 System.out.println("main thread i = " + i);
7 if (i == 20) {
8 myThread.setDaemon(true);
9 myThread.start();
10 }
11 }
12 }
13
14 }
15
16 class MyThread extends Thread {
17
18 public void run() {
19 for (int i = 0; i < 100; i++) {
20 System.out.println("i = " + i);
21 try {
22 Thread.sleep(1);
23 } catch (InterruptedException e) {
24 // TODO Auto-generated catch block
25 e.printStackTrace();
26 }
27 }
28 }
29 }
Java—多线程编程

判断线程是否是后台线程:调用thread对象的isDeamon()方法。

注:main线程默认是前台线程,前台线程创建中创建的子线程默认是前台线程,后台线程中创建的线程默认是后台线程。调用setDeamon(true)方法将前台线程设置为后台线程时,需要在start()方法调用之前。前天线程都死亡后,JVM通知后台线程死亡,但从接收指令到作出响应,需要一定的时间。

4.改变线程的优先级/setPriority():

每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。main线程默认具有普通优先级。

设置线程优先级:setPriority(int priorityLevel)。参数priorityLevel范围在1-10之间,常用的有如下三个静态常量值:

MAX_PRIORITY:10

MIN_PRIORITY:1

NORM_PRIORITY:5

获取线程优先级:getPriority()。

注:具有较高线程优先级的线程对象仅表示此线程具有较多的执行机会,而非优先执行。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 Thread myThread = new MyThread();
5 for (int i = 0; i < 100; i++) {
6 System.out.println("main thread i = " + i);
7 if (i == 20) {
8 myThread.setPriority(Thread.MAX_PRIORITY);
9 myThread.start();
10 }
11 }
12 }
13
14 }
15
16 class MyThread extends Thread {
17
18 public void run() {
19 for (int i = 0; i < 100; i++) {
20 System.out.println("i = " + i);
21 }
22 }
23 }
Java—多线程编程

本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题。

一.一个典型的Java线程安全例子

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 Account account = new Account("123456", 1000);
5 DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);
6 Thread myThread1 = new Thread(drawMoneyRunnable);
7 Thread myThread2 = new Thread(drawMoneyRunnable);
8 myThread1.start();
9 myThread2.start();
10 }
11
12 }
13
14 class DrawMoneyRunnable implements Runnable {
15
16 private Account account;
17 private double drawAmount;
18
19 public DrawMoneyRunnable(Account account, double drawAmount) {
20 super();
21 this.account = account;
22 this.drawAmount = drawAmount;
23 }
24
25 public void run() {
26 if (account.getBalance() >= drawAmount) { //1
27 System.out.println("取钱成功, 取出钱数为:" + drawAmount);
28 double balance = account.getBalance() - drawAmount;
29 account.setBalance(balance);
30 System.out.println("余额为:" + balance);
31 }
32 }
33 }
34
35 class Account {
36
37 private String accountNo;
38 private double balance;
39
40 public Account() {
41
42 }
43
44 public Account(String accountNo, double balance) {
45 this.accountNo = accountNo;
46 this.balance = balance;
47 }
48
49 public String getAccountNo() {
50 return accountNo;
51 }
52
53 public void setAccountNo(String accountNo) {
54 this.accountNo = accountNo;
55 }
56
57 public double getBalance() {
58 return balance;
59 }
60
61 public void setBalance(double balance) {
62 this.balance = balance;
63 }
64
65 }
Java—多线程编程

上面例子很容易理解,有一张银行卡,里面有1000的余额,程序模拟你和你老婆同时在取款机进行取钱操作的场景。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:

1 取钱成功, 取出钱数为:700.0
2 余额为:300.0
3 取钱成功, 取出钱数为:700.0
4 余额为:-400.0

也就是说,对于一张只有1000余额的银行卡,你们一共可以取出1400,这显然是有问题的。

经过分析,问题在于Java多线程环境下的执行的不确定性。CPU可能随机的在多个处于就绪状态中的线程中进行切换,因此,很有可能出现如下情况:当thread1执行到//1处代码时,判断条件为true,此时CPU切换到thread2,执行//1处代码,发现依然为真,然后执行完thread2,接着切换到thread1,接着执行完毕。此时,就会出现上述结果。

因此,讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。

二.同步方法

对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推...

在上例中,共享资源为account对象,当使用同步方法时,可以解决线程安全问题。只需在run()方法前加上synshronized关键字即可。

1 public synchronized void run() {
2
3 // ....
4
5 }

三.同步代码块

正如上面所分析的那样,解决线程安全问题其实只需限制对共享资源访问的不确定性即可。使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。

同步代码块的格式为:

1 synchronized (obj) {
2
3 //...
4
5 }

其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。

如上例中,最好选用account对象作为锁对象。(当然,选用this也是可以的,那是因为创建线程使用了runnable方式,如果是直接继承Thread方式创建的线程,使用this对象作为同步锁会其实没有起到任何作用,因为是不同的对象了。因此,选择同步锁时需要格外小心...)

四.Lock对象同步锁

上面我们可以看出,正因为对同步锁对象的选择需要如此小心,有没有什么简单点的解决方案呢?以方便同步锁对象与共享资源解耦,同时又能很好的解决线程安全问题。

使用Lock对象同步锁可以方便的解决此问题,唯一需要注意的一点是Lock对象需要与资源对象同样具有一对一的关系。Lock对象同步锁一般格式为:

Java—多线程编程
 1 class X {
2
3 // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系
4 private final Lock lock = new ReentrantLock();
5
6 public void m(){
7 // 加锁
8 lock.lock();
9
10 //... 需要进行线程安全同步的代码
11
12 // 释放Lock锁
13 lock.unlock();
14 }
15
16 }
Java—多线程编程

 五.wait()/notify()/notifyAll()线程通信

在博文《Java总结篇系列:java.lang.Object》中有提及到这三个方法,虽然这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。因此,理论上,任何Object对象都可以作为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通信。

wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

notify():唤醒在此同步锁对象上等待的单个线程,如果有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操作,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

notifyAll():唤醒在此同步锁对象上等待的所有线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

Java—多线程编程
  1 package com.qqyumidi;
2
3 public class ThreadTest {
4
5 public static void main(String[] args) {
6 Account account = new Account("123456", 0);
7
8 Thread drawMoneyThread = new DrawMoneyThread("取钱线程", account, 700);
9 Thread depositeMoneyThread = new DepositeMoneyThread("存钱线程", account, 700);
10
11 drawMoneyThread.start();
12 depositeMoneyThread.start();
13 }
14
15 }
16
17 class DrawMoneyThread extends Thread {
18
19 private Account account;
20 private double amount;
21
22 public DrawMoneyThread(String threadName, Account account, double amount) {
23 super(threadName);
24 this.account = account;
25 this.amount = amount;
26 }
27
28 public void run() {
29 for (int i = 0; i < 100; i++) {
30 account.draw(amount, i);
31 }
32 }
33 }
34
35 class DepositeMoneyThread extends Thread {
36
37 private Account account;
38 private double amount;
39
40 public DepositeMoneyThread(String threadName, Account account, double amount) {
41 super(threadName);
42 this.account = account;
43 this.amount = amount;
44 }
45
46 public void run() {
47 for (int i = 0; i < 100; i++) {
48 account.deposite(amount, i);
49 }
50 }
51 }
52
53 class Account {
54
55 private String accountNo;
56 private double balance;
57 // 标识账户中是否已有存款
58 private boolean flag = false;
59
60 public Account() {
61
62 }
63
64 public Account(String accountNo, double balance) {
65 this.accountNo = accountNo;
66 this.balance = balance;
67 }
68
69 public String getAccountNo() {
70 return accountNo;
71 }
72
73 public void setAccountNo(String accountNo) {
74 this.accountNo = accountNo;
75 }
76
77 public double getBalance() {
78 return balance;
79 }
80
81 public void setBalance(double balance) {
82 this.balance = balance;
83 }
84
85 /**
86 * 存钱
87 *
88 * @param depositeAmount
89 */
90 public synchronized void deposite(double depositeAmount, int i) {
91
92 if (flag) {
93 // 账户中已有人存钱进去,此时当前线程需要等待阻塞
94 try {
95 System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " -- i=" + i);
96 wait();
97 // 1
98 System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " -- i=" + i);
99 } catch (InterruptedException e) {
100 e.printStackTrace();
101 }
102 } else {
103 // 开始存钱
104 System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i);
105 setBalance(balance + depositeAmount);
106 flag = true;
107
108 // 唤醒其他线程
109 notifyAll();
110
111 // 2
112 try {
113 Thread.sleep(3000);
114 } catch (InterruptedException e) {
115 e.printStackTrace();
116 }
117 System.out.println(Thread.currentThread().getName() + "-- 存钱 -- 执行完毕" + " -- i=" + i);
118 }
119 }
120
121 /**
122 * 取钱
123 *
124 * @param drawAmount
125 */
126 public synchronized void draw(double drawAmount, int i) {
127 if (!flag) {
128 // 账户中还没人存钱进去,此时当前线程需要等待阻塞
129 try {
130 System.out.println(Thread.currentThread().getName() + " 开始要执行wait操作" + " 执行了wait操作" + " -- i=" + i);
131 wait();
132 System.out.println(Thread.currentThread().getName() + " 执行了wait操作" + " 执行了wait操作" + " -- i=" + i);
133 } catch (InterruptedException e) {
134 e.printStackTrace();
135 }
136 } else {
137 // 开始取钱
138 System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount + " -- i=" + i);
139 setBalance(getBalance() - drawAmount);
140
141 flag = false;
142
143 // 唤醒其他线程
144 notifyAll();
145
146 System.out.println(Thread.currentThread().getName() + "-- 取钱 -- 执行完毕" + " -- i=" + i); // 3
147 }
148 }
149
150 }
Java—多线程编程

上面的例子演示了wait()/notify()/notifyAll()的用法。部分输出结果为:

Java—多线程编程
 1 取钱线程 开始要执行wait操作 执行了wait操作 -- i=0
2 存钱线程 存款:700.0 -- i=0
3 存钱线程-- 存钱 -- 执行完毕 -- i=0
4 存钱线程 开始要执行wait操作 -- i=1
5 取钱线程 执行了wait操作 执行了wait操作 -- i=0
6 取钱线程 取钱:700.0 -- i=1
7 取钱线程-- 取钱 -- 执行完毕 -- i=1
8 取钱线程 开始要执行wait操作 执行了wait操作 -- i=2
9 存钱线程 执行了wait操作 -- i=1
10 存钱线程 存款:700.0 -- i=2
11 存钱线程-- 存钱 -- 执行完毕 -- i=2
12 取钱线程 执行了wait操作 执行了wait操作 -- i=2
13 取钱线程 取钱:700.0 -- i=3
14 取钱线程-- 取钱 -- 执行完毕 -- i=3
15 取钱线程 开始要执行wait操作 执行了wait操作 -- i=4
16 存钱线程 存款:700.0 -- i=3
17 存钱线程-- 存钱 -- 执行完毕 -- i=3
18 存钱线程 开始要执行wait操作 -- i=4
19 取钱线程 执行了wait操作 执行了wait操作 -- i=4
20 取钱线程 取钱:700.0 -- i=5
21 取钱线程-- 取钱 -- 执行完毕 -- i=5
22 取钱线程 开始要执行wait操作 执行了wait操作 -- i=6
23 存钱线程 执行了wait操作 -- i=4
24 存钱线程 存款:700.0 -- i=5
25 存钱线程-- 存钱 -- 执行完毕 -- i=5
26 存钱线程 开始要执行wait操作 -- i=6
27 取钱线程 执行了wait操作 执行了wait操作 -- i=6
28 取钱线程 取钱:700.0 -- i=7
29 取钱线程-- 取钱 -- 执行完毕 -- i=7
30 取钱线程 开始要执行wait操作 执行了wait操作 -- i=8
31 存钱线程 执行了wait操作 -- i=6
32 存钱线程 存款:700.0 -- i=7
Java—多线程编程

由此,我们需要注意如下几点:

1.wait()方法执行后,当前线程立即进入到等待阻塞状态,其后面的代码不会执行;

2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/所有-notifyAll())线程对象,但是,此时还并没有释放同步锁对象,也就是说,如果notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;

3.notify()/notifyAll()执行后,如果右面有sleep()方法,则会使当前线程进入到阻塞状态,但是同步对象锁没有释放,依然自己保留,那么一定时候后还是会继续执行此线程,接下来同2;

4.wait()/notify()/nitifyAll()完成线程间的通信或协作都是基于不同对象锁的,因此,如果是不同的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

当然,上面的例子相对来说比较简单,只是为了简单示例wait()/notify()/noitifyAll()方法的用法,但其本质上说,已经是一个简单的生产者-消费者模式了。

5.线程让步:yield()

上一篇博文中已经讲到了yield()的基本作用,同时,yield()方法还与线程优先级有关,当某个线程调用yiled()方法从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。

Java—多线程编程
 1 public class ThreadTest {
2
3 public static void main(String[] args) {
4 Thread myThread1 = new MyThread1();
5 Thread myThread2 = new MyThread2();
6 myThread1.setPriority(Thread.MAX_PRIORITY);
7 myThread2.setPriority(Thread.MIN_PRIORITY);
8 for (int i = 0; i < 100; i++) {
9 System.out.println("main thread i = " + i);
10 if (i == 20) {
11 myThread1.start();
12 myThread2.start();
13 Thread.yield();
14 }
15 }
16 }
17
18 }
19
20 class MyThread1 extends Thread {
21
22 public void run() {
23 for (int i = 0; i < 100; i++) {
24 System.out.println("myThread 1 -- i = " + i);
25 }
26 }
27 }
28
29 class MyThread2 extends Thread {
30
31 public void run() {
32 for (int i = 0; i < 100; i++) {
33 System.out.println("myThread 2 -- i = " + i);
34 }
35 }
36 }
Java—多线程编程