引言:
操作系统课程上学习的生产者消费者模型可以说是学习并发的最好例子。这里需要注意Java不支持进程,只支持多线程。本篇文章将以一个最简单的生产者消费者模型进行Java并发的讲解。学习了本篇博文你应该学会了一下几个内容
1. 多个线程如何正确并发对一个变量进行读和写
2. 生产者消费者模型的实现
Java并发:
上文说了Java中没有进程只有线程,所以Java的并发只涉及到线程。在Java里可以通过两种方法创建一个线程,第一种为继承Thread类,第二种为实现Runnable接口。两种方法个人更偏向于第二种,因为在Java中没有多继承,所以如果采用第一种继承Thread类,那么就不能继承其他的类了。当然如果一个类确实不需要继承其他的类只是一个线程任务类那么继承Thread也没关系。总体而言二者没什么区别。
生产者消费者模型
生产者消费者模型在工程中到处被应用到。这是一个应用广泛而且很简单的模型。通常会有一个“生产者”进行数据的生产或者添加由一个或者多个消费者进行数据的消费。比如我们想做一个装修订单的推送需求。这个场景的生产者就是订单的生产系统,每生产一个订单我们就将一个订单扔到一个集合中,同时创建若干个给商家推送的线程任务,这些任务就监听这订单集合,发现有订单就取出来(这就是消费行为)然后匹配商家进行推送。这么做的好处
1. 当订单产生的速度明显快过一个订单被推送给商家的速度时(当数据增多这个现象是必然要发生的),使用多个订单推送任务进行推送了的好处就不言而喻了。如果只是来一个订单就进行推送显然已经跟不上订单的产生速度了。
2. 松耦合。松耦合几乎是所有方法,模型,框架的最终目标。采用生产者消费者可以让修改订单系统和推荐系统互不干扰。不用牵一发而动全身
看下面的一个小例子。
public class ThreadTest { //可是理解为订单数量 public static int count = 0; public static void main(String[] argvs) { Producer producer = new Producer(); Consumer consumer1 = new Consumer(); // 继承Thread创建线程通过start方法来启动线程 producer.start(); // 实现Runnable接口创建线程通过run方法启动线程 consumer1.run(); System.out.println(count); } //生产者生产一个订单 public static void countInc() { count++; } // 消费者处理一个订单 public static void countDec() { count--; } } class Producer extends Thread { // 这个线程采用继承Thread来实现需要重写run方法。 这个生产者没20MS就生产一个订单 @Override public void run() { for (int i = 0; i < 10; i++) { // TODO Auto-generated method stub ThreadTest.countInc(); try { sleep(20); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } class Consumer implements Runnable { @Override // 这个线程采用实现Runnable接口来实现,需要重写run方法,这个消费者没20MS就消费一个订单 public void run() { // TODO Auto-generated method stub for(int i = 0; i < 10;i++) { ThreadTest.countDec(); try { Thread.sleep(20); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
synchronized实现Java并发保护
这个实现特别简单。只要在函数countInc以及countDec都加上一个synchronized关键字就可以了。修改如下
public class ThreadTest { //可是理解为订单数量 public static int count = 0; public static void main(String[] argvs) { Producer producer = new Producer(); Consumer consumer1 = new Consumer(); // 继承Thread创建线程通过start方法来启动线程 producer.start(); // 实现Runnable接口创建线程通过run方法启动线程 consumer1.run(); System.out.println(count); } //生产者生产一个订单 public static synchronized void countInc() { count++; // save some thing to mysql } // 消费者处理一个订单 public static synchronized void countDec() { count--; // save some thing to mysql } }
其余代码不变即可。这次再运行则每次都是0,实现了多线程对count访问的线程安全目的。关键字synchronized虽然好用方便但是这个锁是对一个对象上锁的,锁的粒度比较大,因此效率很低。比如函数countInc如果不仅仅是对count+1这么简单呢,比如做了一些数据库操作,那么关键字synchronized会导致调用countInc的时候整个ThreadTest的其他所有的带有关键字synchronized的方法都不可以调用了,因为被锁住了。这样效率很低的,因为我们的目的仅仅是对count++这一行代码加锁就可以了。因此Java里提供了各种各样的锁。
Lock实现Java并发
使用Lock后修改的代码如下
public class ThreadTest { //可是理解为订单数量 public static int count = 0; public static Lock lock = new ReentrantLock(); public static void main(String[] argvs) { Producer producer = new Producer(); Consumer consumer1 = new Consumer(); // 继承Thread创建线程通过start方法来启动线程 producer.start(); // 实现Runnable接口创建线程通过run方法启动线程 consumer1.run(); System.out.println(count); } //生产者生产一个订单 public static void countInc() { lock.lock(); count++; lock.unlock(); // 一些其他不需要锁的代码 } // 消费者处理一个订单 public static void countDec() { lock.lock(); count--; lock.unlock(); // 一些其他不需要锁的代码 } }
可以看到这里new了一个ReentrantLock,Java还有很多其他类型的锁,以后有机会再详细说明这里暂且不提。可以看到对于count++以及count--这两行代码运行之前都想要进行加锁,这样这两行代码的执行就互质了,谁得到lock谁才能执行。这里一定要记住,调用了lock()函数一定要在对应的地方调用unlock()函数,不然就废了,整个系统因为这一个小小的锁就卡死了。当然了你可能还会觉得这么做比较麻烦,有没有原子的整形的,原子的整形就是线程安全的不需要进行保护。答案是肯定有的。
AtomicInteger
代码修改如下
public class ThreadTest { //可是理解为订单数量 public static AtomicInteger count = new AtomicInteger(0);public static void main(String[] argvs) { Producer producer = new Producer(); Consumer consumer1 = new Consumer(); // 继承Thread创建线程通过start方法来启动线程 producer.start(); // 实现Runnable接口创建线程通过run方法启动线程 consumer1.run(); System.out.println(count); } //生产者生产一个订单 public static void countInc() { // count加1 count.incrementAndGet(); // 一些其他不需要锁的代码 } // 消费者处理一个订单 public static void countDec() { //count减1 count.decrementAndGet(); // 一些其他不需要锁的代码 } }
AtomicInteger与int的区别就像hashtable与hashmap的区别一样,一个是线程安全的,一个是线程不安全的。AtomicInteger是Java实现的支持原子操作的int类型,所谓原子操作就是在操作期间一定不会发生线程切换一定是线程安全的。除了AtomicInteger之外在java.util.concurrent.atomic包下有各种数据类型的原子实现。
OK,这篇的内容就这么多,这篇多而杂,设计的话题比较高大上,需要好好吸收消化。其实这只是Java并发编程的冰山一角,但是学会了这个小小的生产者消费者模型却一定会让你受益匪浅的。加油吧,少年。