/Java-Thread

Java多线程实现消费者生产者模型

Primary LanguageJava

Java 多线程


代码问题背景

  • 模拟病人去医生看病的过程,n个病人去看病,有m个医生。医院有k个座位,病人在座位上排队,如果座位满了则这个病人出去逛一下过一段时间再回来。
  • 将这个问题抽象成生产者-消费者模型。医生是消费者,病人是生产者。座位是仓库。

编译器

作者本机编译器:JDK13

不过应该JDK1.5之后的都可以用,看用到的核心代码的源码的部分全部都是“since 1.5”


输入参数

在\out\production\Thread目录下有编译好的.class文件

输入以下指令:

java Main [doctorNum] [patientNum] [seatNum]

例如:

java Main 2 10 3

表示有2个医生,10个病人,3个座位


用到核心技术

  • atomic类用作计数器
  • BlockingQueue用来保证仓库的存和取线程安全
  • ReentrantLock用来保证存储打印顺序的正确性

学习过程

首先感谢廖雪峰Java教程, 这个网站可以用于简单的将JavaSE过一遍,基本的功能都有介绍。但要真正做到精通还是需要阅读经典书籍。 等过一阵子基本开发必备知识都学完后再读《Java核心技术》

下面部分作为学习笔记记录这次学习收获:

  1. 明白一些Java多线程的基本概念,比如线程之间的状态和转换,线程的启动方式有两种继承父类Thread或者重写Runnable, 守护进程的定义,进程的优先级等等。
  2. synchronized的同步方法,以及和它互相协作的函数wait,notify。
  3. 从JDK1.5开始引进了ReentrantLock和Condition。Condition的await、signal函数和ReentrantLock配合使用 可以替代synchronized、wait,notify。ReentrantLock是个更加轻量级的锁,同时可以通过tryLock这种机制避免死锁,更加灵活。
  4. 明白理解和乐观锁和悲观锁。乐观锁是假设读取过程内容没有被改变,只在结束时进行校验,如果改变了就要上锁重新读取一遍。 而悲观锁则是在读取刚开始就上锁防止别的线程不管是读还是写都禁止访问。显然乐观锁适合用于读多写少的情况, 悲观锁适合用于读少写多的情况。ReentrantLock是悲观锁,StampedLock则可以实现乐观锁。同时CAS机制也可以算作一种乐观锁。
  5. 学会并使用了BlockingQueue。通过阅读它的源码发现它是用ReentrantLock和Condition实现的。Java还有实现了其他的线程安全集合, 比如ConcurrentHashMap等等。
  6. 了解使用Atomic类,通过CAS的方法实现线程安全,适合用作累加器、计数器
  7. 初步了解了用Executors创建线程池的方法,在本次代码初步阶段也是使用线程池来实现医生线程。不过后面由于线程编号的问题无法实现, 所以放弃使用线程池。不过在阅读网上资料过程中发现用Executors初始化线程存在很大弊端,工业上常用ThreadPoolExecutor创建线程池。

遇到问题即解决

  • ClassCastExcception:

这个问题主要是在初期BlockingQueue从主线程传参给医生和病人线程的时候出现问题。原来想用接口编程的,像List和ArrayList一样。 但是chuangdi传递给其他类的时候报错了。所以最后就统一用ArrayBlockingQueue这个类来实现阻塞队列了。

  • 打印顺序无序

每个线程操作阻塞队列的时候都保证了有序性。但打印信息的时候却是无序的。所以我用ReentrantLock将操作阻塞队列外面又加了一层锁。 医生和病人线程同时只有一个可以操作阻塞队列(忽然发现那这样用阻塞队列的意义是什么。。用普通不就行了吗?)。同时要防止两种死锁出现: 在某个医生线程获得锁后调用quue.take()引起线程挂起但没有释放外层的锁,以及某个病人获得外层锁后queue.put()引起线程挂起的死锁情况。 这两种情况要先做条件判断后再执行后面语句。

  • 主要以上两个问题(其实主要就是问题2,以及更改过程中延伸出的其他小问题)

总结

算是一次最初级的Java多线程练手。主要是改写上学期的用C语言实现的操作系统实验,总的感觉和C相比,Java的很多多线程操作都封装好了,不用自己mutex,pthread_create 自己实现阻塞队列之类的。但是因为封装好了所以加锁的地方就可能不是个人想要的地方,就会出现实际操作正确但打印无序的新问题。

同时这次编码还有一些东西没有用到,比如线程池的使用,ThreadLocal的使用。离熟悉Java多线程还有一段路要走。

一个晚上学习,一个晚上编码,一个下午写README.md。感觉效率还行,多线程先告一段落,接下来学习shell脚本。