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

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

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

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

源码如下:

1 2

3 importjava.util.LinkedList;4 importjava.util.Queue;5

6 public classMainTest {7 public static voidmain(String[] args) {8 test();9 }10

11 private static final long waitTime = 3000;12

13 private static voidtest() {14 Queue 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 extendsThread {34 Queue queue;//queue,对象锁

35 int maxsize;//貌似是队列的最大产量

36

37 Producer(Queue queue, intmaxsize, String name) {38 this.queue =queue;39 this.maxsize =maxsize;40 this.setName(name);41 }42

43 @Override44 public voidrun() {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 extendsThread {80 Queuequeue;81 intmaxsize;82

83 Consumer(Queue queue, intmaxsize, String name) {84 this.queue =queue;85 this.maxsize =maxsize;86 this.setName(name);87 }88

89 @Override90 public voidrun() {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,唤醒所有等到对象锁的线程,如果有多个被唤醒的线程,锁将会被争夺,争夺到锁的线程就可以执行.

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值