java多线程编程的概述以及案例详解

引子:  java编程中有时候会要求线程安全(注:多个线程同时访问同一代码的时候,不会产生不同的结果。编写线程安全的代码需要线程同步),这时候就需要进行多线程编程。从而用到线程间通信的技术。那么在java里面,线程间通信是怎么实现的?这篇文章将通过一个案例详细分析。

文章关键词: Object,wait,notify,notifyAll,锁,同步(synchronized).

 

详解一个经典的生产者消费者模型,其中用到了 wait和notifyAll方法。

源码如下:

  1   2 
  3 import java.util.LinkedList;
  4 import java.util.Queue;
  5 
  6 public class MainTest {
  7     public static void main(String[] args) {
  8         test();
  9     }
 10 
 11     private static final long waitTime = 3000;
 12 
 13     private static void test() {
 14         Queue<Integer> queue = new LinkedList<>();// 队列对象,它就是所谓的“锁”
 15         int maxsize = 2;// 队列中的最大元素个数限制
 16 
 17         // 下面4个线程,一瞬间只能有一个线程获得该对象的锁,而进入同步代码块
 18         Producer producer = new Producer(queue, maxsize, "Producer");
 19         Consumer consumer1 = new Consumer(queue, maxsize, "Consumer1");
 20         Consumer consumer2 = new Consumer(queue, maxsize, "Consumer2");
 21         Consumer consumer3 = new Consumer(queue, maxsize, "Consumer3");
 22 
 23         // 其实随便先启动哪个都无所谓,因为只有一个锁,每一次只会有一个线程能持有这个锁,来操作queue
 24         producer.start();
 25         consumer2.start();
 26         consumer1.start();
 27         consumer3.start();
 28     }
 29 
 30     /**
 31      * 生产者线程
 32      */
 33     public static class Producer extends Thread {
 34         Queue<Integer> queue;// queue,对象锁
 35         int maxsize;// 貌似是队列的最大产量
 36 
 37         Producer(Queue<Integer> queue, int maxsize, String name) {
 38             this.queue = queue;
 39             this.maxsize = maxsize;
 40             this.setName(name);
 41         }
 42 
 43         @Override
 44         public void run() {
 45             while (true) {// 无限循环,不停生产元素,直到达到上限,只要达到上限,那就wait等待。
 46                 synchronized (queue) {// 同步代码块,只有持有queue这个锁的对象才能访问这个代码块
 47                     try {
 48                         Thread.sleep(waitTime);
 49                         // sleep和wait的区别,sleep会让当前执行的线程阻塞一段时间,但是不会释放锁,
 50                         // 但是wait,会阻塞,并且会释放锁
 51                     } catch (Exception e) {
 52                     }
 53 
 54                     System.out.println(this.getName() + "获得队列的锁");// 只有你获得了queue对象的锁,你才能执行到这里
 55                     // 条件的判断一定要使用while而不是if
 56                     while (queue.size() == maxsize) {// 判断生产有没有达到上限,如果达到了上限,就让当前线程等待
 57                         System.out.println("队列已满,生产者" + this.getName() + "等待");
 58                         try {
 59                             queue.wait();// 让当前线程等待,直到其他线程调用notifyAll
 60                         } catch (Exception e) {
 61                         }
 62                     }
 63 
 64                     // 下面写的就是生产过程
 65                     int num = (int) (Math.random() * 100);
 66                     queue.offer(num);// 将一个int数字插入到队列中
 67 
 68                     System.out.println(this.getName() + "生产一个元素:" + num);
 69                     // 唤醒其他线程,在这里案例中是 "等待中"的消费者线程
 70                     queue.notifyAll();// (注:notifyAll的作用是
 71                                         // 唤醒所有持有queue对象锁的正在等待的线程)
 72 
 73                     System.out.println(this.getName() + "退出一次生产过程!");
 74                 }
 75             }
 76         }
 77     }
 78 
 79     public static class Consumer extends Thread {
 80         Queue<Integer> queue;
 81         int maxsize;
 82 
 83         Consumer(Queue<Integer> queue, int maxsize, String name) {
 84             this.queue = queue;
 85             this.maxsize = maxsize;
 86             this.setName(name);
 87         }
 88 
 89         @Override
 90         public void run() {
 91             while (true) {
 92                 synchronized (queue) {// 要想进入下面的代码,就必须先获得锁。
 93                     try {
 94                         Thread.sleep(waitTime);// sleep,让当前线程阻塞指定时长,但是并不会释放queue锁
 95                     } catch (Exception e) {
 96                     }
 97 
 98                     System.out.println(this.getName() + "获得队列的锁");// 拿到了锁,才能执行到这里
 99                     // 条件的判断一定要使用while而不是if,
100                     while (queue.isEmpty()) {// while判断队列是否为空,如果为空,当前消费者线程就必须wait,等生产者先生产元素
101                         // 这里,消费者有多个(因为有多个consumer线程),每一个消费者如果发现了队列空了,就会wait。
102                         System.out.println("队列为空,消费者" + this.getName() + "等待");
103                         try {
104                             queue.wait();
105                         } catch (Exception e) {
106                         }
107                     }
108 
109                     // 如果队列不是空,那么就弹出一个元素
110                     int num = queue.poll();
111                     System.out.println(this.getName() + "消费一个元素:" + num);
112                     queue.notifyAll();// 然后再唤醒所有线程,唤醒不会释放自己的锁
113 
114                     System.out.println(this.getName() + "退出一次消费过程!");
115                 }
116             }
117         }
118     }
119 }

案例解析:

1)此案例模拟的是,生产者线程 生产元素并且插入到Queue中,Queue有一个存储个数的限制。消费者线程,从Queue中拿出元素。两个线程都是无限循环执行的。

2)在生产者线程的生产过程(随机产生一个int然后插入到queue中)执行之前,首先检查Queue的存储个数有没有到达上限,如果到达了,那就不能生产,代码中调用了queue.wait();来使生产者线程进入等待状态并且释放锁。如果没超过,那就反复执行,直到到达上限。

3)消费者线程在执行消费过程(从queue中弹出一个元素)执行之前,首先要检查queue是不是空,如果是空,那就不能消费,调用queue.wait()让消费线程进入等待状态并且释放锁。

4)在生产过程 或 消费过程执行完毕之后,都会有queue.notifyAll();来唤醒等待锁的所有线程。

5)生产者中,判定queue的元素个数是不是到达上限。以及 消费者中,判定queue是不是空,这种判定queue.wait()的条件 所使用的关键字,并不是if,而是while.

因为在执行了wait之后,该线程的执行,会暂时停留在这个while循环中,等待被唤醒,一旦被唤醒,while循环会继续执行,从而会再次判断条件是否满足。

6)代码中能找到Thread.sleep(long);方法,它的作用,是当当前线程阻塞指定时间,但是它并不会释放锁。而wait除了阻塞之外,还会释放锁。

 

 案例执行的结果打印:

Producer获得队列的锁
Producer生产一个元素:86
Producer退出一次生产过程!
Producer获得队列的锁
Producer生产一个元素:31
Producer退出一次生产过程!
Producer获得队列的锁
队列已满,生产者Producer等待
Consumer2获得队列的锁
Consumer2消费一个元素:86
Consumer2退出一次消费过程!
Consumer3获得队列的锁
Consumer3消费一个元素:31
Consumer3退出一次消费过程!
Consumer1获得队列的锁
队列为空,消费者Consumer1等待
Consumer3获得队列的锁
队列为空,消费者Consumer3等待
Consumer2获得队列的锁
队列为空,消费者Consumer2等待
Producer生产一个元素:29
Producer退出一次生产过程!
Producer获得队列的锁
Producer生产一个元素:82
Producer退出一次生产过程!
Producer获得队列的锁
队列已满,生产者Producer等待
Consumer2消费一个元素:29
Consumer2退出一次消费过程!

结果分析(请对照日志来看,大神请绕道,下面的描述比较啰嗦):

由于首先启动的是生产者线程(Producer),所以producer先获得了锁,进行了两次生产。再次尝试生产的时候发现queue满了,于是,生产者进入等待。

之后,consumer2的得到了锁,于是进行消费,消费执行了一次,锁被consumer3夺走,consumer3执行了一次消费。

之后,consumer1得到了锁,就当它准备开始消费的时候,发现queue空了,不能消费了,于是代码调用queue.wait().来让consumer1进入等待。

之后,consumer3和consumer2相继得到锁,但是他们都发现,queue空了,也不能消费,于是同样调用queue.wait()来让consumer3和consumer1进入等待。

再然后,生产者得到了锁(这里可能很奇怪,生产者不是在等待么?它什么时候被唤醒的,查看Consumer的代码,能发现,在每一次成功消费之后,都会有queue.notifyAll(),也就是说,在之前cunsumer2消费之后,生产者就已经被唤醒了,只是他没有得到锁,所以就没有执行生产过程)。

生产者得到锁之后,继续while循环,发现queue并没有填满,于是进入生产过程。之后···就是无限循环了。

 

这种模型在线程安全比较高的场景中,会被经常用到,比如买票系统,同一张票不能被卖两次。所以,这张票,在同一时间只能被一个线程访问。

-------------------

 

 

案例解析完毕,但是针对java多线程,也许有人会有其他疑问,下面列举几个比较重要的问题加以说明:

问:在java中,wait,notify以及notifyAll是用来做线程之间的通信的,但是为什么这3个方法不是在Thread类里面,而是在Object类里面?
答:  

这3个方法虽然是用于线程间的通信,但是他们并不是直接就在Thread类里面,而是在Object类。

这是 因为 调用一个Object的wait,notify,notifyAll 必须保证该段代码对于该Object是同步的, 否则就可能会报异常IllegalMonitorStateException(具体可以进入Object类的源码搜索此异常,注释中有详细说明),通常的写法如下,

synchronized(obj){//在执行wait,notify,notifyAll时,必须保证这段代码持有obj对象的锁。
  obj.wait();
  ...
  obj.notify();
  ...
  obj.notifyAll();
}

如果多个线程都写了上面的代码,那么同一时间,只会有一个线程能获取obj对象锁。

所以说,这3个方法在Object类里,而不是在Thread类里,其实是java框架的设定,通过Object锁来完成线程间的通信。

 

问:wait,notify,notifyAll的作用分别是什么?

答:

wait-让当前线程进入等待状态,并且释放锁;

notify -唤醒任意一个正在等待锁的线程,并且让它得到锁。

notifyAll,唤醒所有等待对象锁的线程,如果有多个线程都被唤醒,那么锁将会被他们争夺,同一时间只会有一个线程得到锁。

问:notify,notifyAll有啥区别?

答:

notify,让任意一个等待对象锁的线程得到锁,并且唤醒他。

notifyAll,唤醒所有等到对象锁的线程,如果有多个被唤醒的线程,锁将会被争夺,争夺到锁的线程就可以执行.

 

===================就写到这里了。上面的是基础知识,在复杂场景中可能会被复杂化千万倍,但是万变不离其宗,了解了原理,就能应对大部分场景了。

 

原文地址:https://www.cnblogs.com/hankzhouAndroid/p/8693278.html