在理解Java中多线程的相关内容前,我们需要对宏观层面上操作系统中和进程相关的内容做一些了解,在知道了操作系统是如何管理进程和线程后,才能更好的理解Java中有关多线程的创建、调度和同步控制等内容。

1. 操作系统四大特性

  • 并发(concurrence)
    并行性与并发性这两个概念是既相似又区别的两个概念。并行性是指两个或者多个事件在同一时刻发生,这是一个具有微观意义的概念,即在物理上这些事件是同时发生的;而并发性是指两个或者多个事件在同一时间的间隔内发生,它是一个较为宏观的概念。在多道程序环境下,并发性是指在一段时间内有多道程序在同时运行,但在单处理机的系统中,每一时刻仅能执行一道程序,故微观上这些程序是在交替执行的。

  • 共享 (sharing)
    所谓共享是指系统中的资源可供内存中多个并发执行的进程共同使用。由于资源的属性不同,故多个进程对资源的共享方式也不同,可以分为:互斥共享方式同时访问方式。

  • 虚拟 (virtual)
    指通过技术把一个物理实体变成若干个逻辑上的对应物。在操作系统中虚拟的实现主要是通过分时的使用方法。显然,如果浅析Java中的多线程 - 图1是某一个物理设备所对应的虚拟逻辑设备数,则虚拟设备的速度必然是物理设备速度的浅析Java中的多线程 - 图2

  • 异步 (asynchronism)
    在多道程序设计环境下,允许多个进程并发执行,由于资源等因素的限制,通常,进程的执行并非“一气呵成”,而是以“走走停停”的方式运行。内存中每个进程在何时执行,何时暂停,以怎样的方式向前推进,每道程序总共需要多少时间才能完成,都是不可预知的。

2. 进程和线程

2.1 进程:

指一个在内存中运行的程序,每个进程都有自己独立的内存空间,它表示程序的一次运行过程,系统运行程序的基本单位,系统运行程序既是一个程序从创建、运行到消亡的过程

2.2 线程:

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中可以包含多个线程,但至少有一个线程。

2.3 进程和线程的区别和联系

区别

  • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。进程所维护的是程序所包含的资源(静态资源),如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等;线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等;
  • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

联系

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
  • 处理机分给线程,即真正在处理机上运行的是线程;
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

2.4 进程的状态转换

进程的五状态模型:

  • 运行态:该进程正在执行
  • 就绪态:进程已经做好了准备,只要有机会就开始执行
  • 阻塞态(等待态):进程在某些事情发生前不能执行,等待阻塞进程的事件完成
  • 新建态:刚刚创建的进程,操作系统还没有把它加入到可执行进程组中,通常是进程控制块已经创建但是还没有加载到内存中的进程
  • 退出态:操作系统从可执行进程组中释放出的进程,或由于自身或某种原因停止运行

可能的转换如下:
浅析Java中的多线程 - 图3

3. 进程调度算法

往往计算机中并行运行多个线程或进程,那么选择哪些运行就涉及到了调度算法,操作系统中常用的调度算法有:

周转时间=程序结束时间—开始服务时间,带权周转时间=周转时间/ 要求服务时间

  • 先来先服务调度算法:一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。

  • 短作业(进程)优先调度算法:指对短作业或短进程优先调度的算法,该算法既可用于作业调度, 也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。

  • 高优先权优先调度算法:为了照顾紧迫性作业,使之进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用在批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度,还可以用于实时系统中。当其用于作业调度,将后备队列中若干个优先权最高的作业装入内存。当其用于进程调度时,把处理机分配给就绪队列中优先权最高的进程,此时,又可以进一步把该算法分成以下两种:

    • 高响应比优先调度算法:为了弥补短作业优先算法的不足,我们引入动态优先权,使作业的优先等级随着等待时间的增加而以速率a提高。 该优先权变化规律可描述为:优先权=(等待时间+要求服务时间)/要求服务时间;即 =(响应时间)/要求服务时间
    • 基于时间片的轮转调度算法:时间片轮转法一般用于进程调度,每次调度,把CPU分配队首进程,并令其执行一个时间片。 当执行的时间片用完时,由一个记时器发出一个时钟中断请求,该进程被停止,并被送往就绪队列末尾;依次循环。
  • 多级反馈队列调度算法:不必事先知道各种进程所需要执行的时间,它是目前被公认的一种较好的进程调度算法。 过程如下:
    a. 设置多个就绪队列,并为各个队列赋予不同的优先级。在优先权越高的队列中,为每个进程所规定的执行时间片就越小。
    b. 当一个新进程进入内存后,首先放入第一队列的末尾,按FCFS原则排队等候调度。 如果他能在一个时间片中完成,便可撤离;如果未完成,就转入第二队列的末尾,在同样等待调度…… 如此下去,当一个长作业(进程)从第一队列依次将到第n队列(最后队列)后,便按第n队列时间片轮转运行。
    c. 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1到第(i-1)队列空时, 才会调度第i队列中的进程运行,并执行相应的时间片轮转。
    d. 如果处理机正在处理第i队列中某进程,又有新进程进入优先权较高的队列, 则此新队列抢占正在运行的处理机,并把正在运行的进程放在第i队列的队尾。

4. Java中创建线程

4.1 创建Thread类的子类

java.lang.Thread类是描述线程的类,想要实现多线程就必须继承Thread类。使用Thread创建多线程的步骤:

  • 创建Thread的子类
  • 在Thread类的子类中重写thread类中run(),设置线程任务
  • 创建Thread类的子类对象
  • 创建Thread类中的start(),开启新的线程,执行run()

NOTE:

  • 要启动创建的新线程,必须在main()中调用类对象的start()而不是重写的run()
  • 当调用start()时,Java虚拟机会自动的调用run()来创建一个新的线程,此时系统中就有两个线程并发的执行,分别是main线程和创建的新线程
  • 不能多次启动同一个线程,特别是当线程已经结束执行后不能再重新启动
  • Java采用抢占式调度算法,哪个线程的优先级高就优先执行哪一个;如果是同样的优先级,那么随机选择一个执行

下面通过例子看一下如何通过实现Thread的子类来创建多线程程序,创建Thread的子类并重写run()

  1. public class MyThread extends Thread{
  2. public MyThread() {
  3. }
  4. public MyThread(String name) {
  5. super(name);
  6. }
  7. @Override
  8. public void run() {
  9. for (int i = 0; i < 10; i++) {
  10. System.out.println("MyThread" + i);
  11. }
  12. }
  13. }

然后在调用方法中创建MyThread类的子类对象,调用start()开启线程。为了方便理解,我们分别在main()和重写的run()中都进行循环打印。main()是主线程,创建的为新的线程,通过输出结果可以看到系统如何执行这两个线程:

  1. public class ThreadMain {
  2. public static void main(String[] args) {
  3. MyThread my = new MyThread();
  4. my.start();
  5. for (int i = 0; i < 10; i++) {
  6. System.out.println("main: " + i);
  7. }
  8. }
  9. }

控制台打印输出:

  1. MyThread: 0
  2. main: 0
  3. MyThread: 1
  4. main: 1
  5. MyThread: 2
  6. main: 2
  7. MyThread: 3
  8. MyThread: 4
  9. MyThread: 5
  10. main: 3
  11. main: 4
  12. main: 5
  13. main: 6
  14. main: 7
  15. main: 8
  16. main: 9
  17. MyThread: 6
  18. MyThread: 7
  19. MyThread: 8
  20. MyThread: 9

那么操作系统是如何选择线程进行调度的呢?下面通过图分析一下上面代码所表述的过程:
浅析Java中的多线程 - 图4

  • 执行main()new MyThread()会创建两个不同的线程,CPU都拥有通向它们的路径
  • 此时系统中拥有两个线程,根据对CPU的抢占分别进行执行

对于不同的线程来说,系统会为它们分配不同的栈空间,然后选择执行。
浅析Java中的多线程 - 图5

在知道了使用创建Thread的子类的方式创建新线程,以及在线程调度过程中内存空间的变化后,我们具体来看一下Thread类中的一些常用方法:

  • public Thread():分配一个新的线程对象

  • public Thread(String name):分配一个指定名字的新的线程对象

  • public Thread(Runnable target):分配一个带有指定目标的新的线程对象

  • public Thread(Runnable target, String name):分配一个带有指定目标新的线程对象并指定名字

  • public String getName():获取当前线程名称

  • public void setName(String name):设置当前线程的名字

  • public void start():执行线程,JVM调用此线程的run()

  • public void run():线程想要执行的逻辑

  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停

  • public static Thread currentThread():返回对当前正在执行的线程对象的引用

  1. public class MyThread extends Thread{
  2. public MyThread() {
  3. }
  4. public MyThread(String name) {
  5. super(name);
  6. }
  7. @Override
  8. public void run() {
  9. // 法1:通过Thread类的getName()
  10. System.out.println(this.getName());
  11. // 法2:首先通过currentThread()获取当前的线程,然后使用getName()
  12. System.out.println(Thread.currentThread());
  13. System.out.println(Thread.currentThread().getName());
  14. }
  15. }
  16. public class ThreadMain {
  17. public static void main(String[] args) {
  18. System.out.println(Thread.currentThread().getName());
  19. MyThread my = new MyThread();
  20. my.setName("MyThread-1");
  21. my.start();
  22. for (int i = 0; i < 10; i++) {
  23. System.out.println(i);
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. }
  31. }

4.2 实现Runnable接口

创建线程的另一种方式是创建java.lang.Runnable接口的实现类,并重写接口中无参的run(),最后通过Thread类public Thread(Runnable target)创建新线程。通过实现Runnable接口创建新线程的步骤为:

  • 创建一个Runnable接口的实现类
  • 在实现类中重写Runnable接口中的run(),设置线程任务
  • 创建一个Runnable接口实现类的对象
  • 创建Thread类对象,构造方法中传入Runnable接口的实现类对象
  • 调用Thread类中的start(),开启新线程
  1. public class MyRunnable implements Runnable{
  2. @Override
  3. public void run() {
  4. for (int i = 0; i < 10; i++) {
  5. System.out.println("MyRunnable: " + i);
  6. }
  7. }
  8. }
  9. public class RunnableMain {
  10. public static void main(String[] args) {
  11. MyRunnable mr = new MyRunnable();
  12. Thread t = new Thread(mr);
  13. t.start();
  14. for (int i = 0; i < 10; i++) {
  15. System.out.println(Thread.currentThread().getName() + i);
  16. }
  17. }
  18. }
  1. main0
  2. main1
  3. main2
  4. MyRunnable: 0
  5. main3
  6. main4
  7. main5
  8. main6
  9. main7
  10. MyRunnable: 1
  11. main8
  12. main9
  13. MyRunnable: 2
  14. MyRunnable: 3
  15. MyRunnable: 4
  16. MyRunnable: 5
  17. MyRunnable: 6
  18. MyRunnable: 7
  19. MyRunnable: 8
  20. MyRunnable: 9

从输出结果中可以看出,通过实现Runnable接口同样可以创建新线程进行调度。相比于通过创建Thread类的的子类来创建新线程,通过实现Runnable接口创建新线程有如下的优势:

  • 避免了单继承的局限:类是单继承的,类继承了Thread类就无法继续继承其他类,实现了Runnable接口还可以继承其他类
  • 增强了程序的扩展性,降低了程度的耦合性解耦):实现Runnable接口的方式,把设置线程任务和开始新线程进行了解耦。实现类中重写run(),用来设置线程任务,创建Thread类对象,调用start(),用来开启新线程

不管是通过创建Thread类的子类对象还是通过实现Runnable接口来创建新线程,它们都需要定义一个类。因此,有时为了使用的方便可直接使用匿名内部类实现。

  1. public class InnerClassThread {
  2. public static void main(String[] args) {
  3. new Thread(){
  4. @Override
  5. public void run() {
  6. for (int i = 0; i < 3; i++) {
  7. System.out.println("MyThread " + i);
  8. }
  9. }
  10. }.start();
  11. new Thread(new Runnable(){
  12. @Override
  13. public void run() {
  14. for (int i = 0; i < 3; i++) {
  15. System.out.println("MyRunnable " + i);
  16. }
  17. }
  18. }).start();
  19. for (int i = 0; i < 3; i++) {
  20. System.out.println(Thread.currentThread().getName() + " " + i);
  21. }
  22. }
  23. }

5. 线程安全

5.1 引入

操作系统的第二大特点是共享,前面所述侧重于进程之间对于系统资源的竞争。由于进程是系统资源分配的基本单位,因此属于同一个进程的多个线程之间同样存在资源共享问题,那么有共享便会有竞争。如果对于共享资源的竞争处理不当,那么系统可能就会陷入死锁,此时也可以说发生了线程不安全问题。

因此,线程安全问题是多线程编程中一个很重要的方面。那么如何理解线程安全问题呢?例如,如果我们想去坐高铁就需要买票,由于买票的方式各种各样,而且售票的渠道也很多,如果对于售票不加以安全性控制,那么有很大可能就会售出相同的票,这显然是不合理的。再比如我们到一家电影院不同的售票窗口去购买同一场次的电影票,如果每个窗口售卖的票之间不存在重叠问题,那么显然它们处理的是不共享的资源,自然不会发生线程安全问题。如果它们售卖是该场次全部的票,如果不加以控制,那么就会出现前面卖出同票的问题。

下面我们通过代码直观感受一下线程安全问题是如何发生的,以及该用什么样的方法来解决它。首先创建Runnable接口的实现类:

  1. public class Ticket implements Runnable{
  2. // 假设该场次只有10张票
  3. private int ticket = 10;
  4. @Override
  5. public void run() {
  6. while (true){
  7. if (this.ticket > 0){
  8. System.out.println(Thread.currentThread().getName() + " -- " + ticket + "'th ticket");
  9. this.ticket--;
  10. }
  11. }
  12. }
  13. }

然后在main()中创建三个不同的线程来表示不同的售票窗口,通过分别调用start()开启线程:

  1. package Thread.Ticket;
  2. public class TicketMain {
  3. public static void main(String[] args) {
  4. Ticket ticket = new Ticket();
  5. Thread t1 = new Thread(ticket);
  6. Thread t2 = new Thread(ticket);
  7. Thread t3 = new Thread(ticket);
  8. t1.start();
  9. t2.start();
  10. t3.start();
  11. }
  12. }

控制台的输出如下所示,从输出中可以看出,不同的窗口竟然售出了同一张票!

  1. Thread-0 -- 10'th ticket
  2. Thread-2 -- 10'th ticket
  3. Thread-1 -- 10'th ticket
  4. Thread-2 -- 8'th ticket
  5. Thread-0 -- 9'th ticket
  6. Thread-2 -- 6'th ticket
  7. Thread-1 -- 7'th ticket
  8. Thread-2 -- 4'th ticket
  9. Thread-0 -- 5'th ticket
  10. Thread-2 -- 2'th ticket
  11. Thread-1 -- 3'th ticket
  12. Thread-0 -- 1'th ticket

5.2 解决方案

Java中提供了同步机制(synchronized)来通过控制对共享资源的访问解决线程安全问题,即保证每时刻只能有一个线程访问共享资源。同步机制的实现有三种方法:

5.2.1 同步代码块

同步代码块通过将synchronized关键字用于执行共享资源访问的某个代码块中,来实现资源的互斥访问。使用格式为:

  1. ...
  2. synchronized(同步锁){
  3. 需要同步操作的代码
  4. }
  5. ...

其中括号中的同步锁提供了互斥访问的机制,它可以是任何的对象,但必须保证多个线程使用的是同一个锁对象。如果每个线程保持自己独有的一个是锁对象,那么互斥访问控制就无从谈起了。每个时刻只能有一个线程拥有锁,其他的线程就只能等待锁的释放,从而实现了互斥访问。

下面我们将同步代码块的方法用于上面的例子中:

  1. public class Ticket implements Runnable{
  2. private int ticket = 10;
  3. Object obj = new Object();
  4. @Override
  5. public void run() {
  6. while (true){
  7. synchronized (this.obj){
  8. if (this.ticket > 0){
  9. System.out.println(Thread.currentThread().getName() + " -- " + ticket + "'th ticket");
  10. this.ticket--;
  11. }
  12. }
  13. }
  14. }
  15. }

然后创建三个线程来启动执行,输出结果如下,从结果中可以看出,不同的窗口就不可能卖出相同的票了。

  1. Thread-0 -- 10'th ticket
  2. Thread-2 -- 9'th ticket
  3. Thread-2 -- 8'th ticket
  4. Thread-2 -- 7'th ticket
  5. Thread-2 -- 6'th ticket
  6. Thread-2 -- 5'th ticket
  7. Thread-2 -- 4'th ticket
  8. Thread-1 -- 3'th ticket
  9. Thread-1 -- 2'th ticket
  10. Thread-1 -- 1'th ticket

不同的线程在抢占到CPU后就会执行run(),在遇到同步代码块后会检查synchronized代码块中是否有同步锁对象:

  • 如果有就获取锁对象,然后开始执行
  • 如果发现没有锁对象,说明共享资源正在被其他线程访问,它就会进入阻塞态等待其他线程对锁对象的释放。当阻塞态的线程获取到了锁对象,才能继续运行线程

通过这样强制互斥访问的方式保证了安全问题,但是频繁的获取和释放锁对象的方式效率较低

5.2.2.同步方法

原理类似于同步代码块,使用synchronized关键字修饰的方法就称为同步方法,它同样可以保证对共享资源的互斥访问。使用格式为:

  1. 修饰符 synchronized 返回值类型 方法名(参数列表){
  2. 可能会产生线程安全问题的代码
  3. }

将其应用到相同的例子中:

  1. public class Ticket implements Runnable{
  2. private int ticket = 10;
  3. @Override
  4. public synchronized void run() {
  5. while (true){
  6. if (this.ticket > 0) {
  7. System.out.println(Thread.currentThread().getName() + " -- " + ticket + "'th ticket");
  8. this.ticket--;
  9. }
  10. }
  11. }
  12. }

同步方法实际上也是使用了锁对象来进行互斥访问控制,它的锁对象就是线程的实现类对象,也就是说上面的代码等价于:

  1. public class Ticket implements Runnable{
  2. private int ticket = 10;
  3. @Override
  4. public void run() {
  5. while (true){
  6. synchronized (this){
  7. if (this.ticket > 0){
  8. System.out.println(Thread.currentThread().getName() + " -- " + ticket + "'th ticket");
  9. this.ticket--;
  10. }
  11. }
  12. }
  13. }
  14. }

5.2.3 Lock锁

java.util.concurrent.locks.Lock接口提供了相比于前两种方法更广义的锁操作机制,更能体现面向对象的思想。Lock锁主要用到如下的两个方法:

  • public void lock():加同步锁
  • public void unlock():释放同步锁

使用Lock锁机制要使用接口的实现类java.util.concurrent.locks.ReentrantLock,使用步骤如下:

  • 创建ReentrantLock对象
  • 在可能出现线程安全的代码前调用lock()获取同步锁
  • 在可能出现线程安全的代码后调用unlock()释放同步锁

将其应用到例子中:

  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. public class Ticket implements Runnable{
  4. private int ticket = 10;
  5. // 创建锁对象
  6. Lock lock = new ReentrantLock();
  7. @Override
  8. public void run() {
  9. while (true){
  10. if (this.ticket > 0) {
  11. try{
  12. // 加锁
  13. lock.lock();
  14. Thread.sleep(100);
  15. System.out.println(Thread.currentThread().getName() + " -- " + ticket + "'th ticket");
  16. this.ticket--;
  17. } catch (InterruptedException e){
  18. e.printStackTrace();
  19. } finally {
  20. // 释放锁
  21. lock.unlock();
  22. }
  23. }
  24. }
  25. }
  26. }

6. 等待与唤醒机制

6.1 Java线程中的状态转换

在理解等待与唤醒机制前,我们首先将前面讲的操作系统中的状态转换具体到Java程序中,看一下Java中有哪些状态,以及不同的状态之间是通过什么方式相互转换的。Java多线程中主要有以下六种状态:

  • NEW :至今尚未启动的线程处于这种状态
  • RUNNABLE :正在Java虚拟机中执行的线程处于这种状态
  • BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态
  • WAITING:无限期的等待另一个线程来执行某一待定操作的线程处于这种状态
  • TIMED_WAITNG:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态
  • TERMINATED:已退出的线程处于这种状态

六种状态之间的转换为:
浅析Java中的多线程 - 图6

6.2 等待与唤醒机制

线程间通信:如果多个线程之间处理同一个资源,而且它们只能互斥访问,那么它们之间就存在线程间通信问题。通过线程间通信,使得不同的线程可以正确的互斥访问所共享的资源。

那么如何保证线程间的通信呢?等待与唤醒机制就为我们提供了一种方法,不同的线程之间对于同一资源存在着某种协作关系,只有当某个线程结束后,资源发生了改变,另外的某些线程才能进行执行。具体来说,如果某个线程进行了某些操作后没有得到想要的资源,那么它就处于等待(wait())状态;等到其他线程执行完毕,再将可能的线程唤醒(notify())执行;如果需要可以唤醒所有(notifyAll())等待线程。

其中涉及到三个方法:

  • wait():此时线程不再活动,不能参与CPU的调度,对应于状态集中的WAITING状态,它们同一保存在wait set中
  • notify():从wati set中随机选择一个线程将其重新加入到调度队列,如果该线程能获取到同步锁,那么进入RUNNABLE状态,否则进入BLOCKED状态
  • notifyAll():释放wait set中所有等待的线程

NOTE:

  • wait()notify()必须有同一个锁对象使用
  • wait()notify()属于Object类的方法
  • wait()notify()必须要在同步代码块或是同步方法中使用

下面我们就通过一个例子来看一下等待与唤醒机制是如何工作的。此时有一个产品类Product,它包含三个属性变量:

  1. public class Product {
  2. String name;
  3. Integer price;
  4. boolean flag = false; // 表示产品是否可用
  5. }

然后我们需要定义生产者和消费者用于生产和消费产品,而且只有产品的flag == true,消费者才能使用产品,只有flag == flase,生产者才能生产新的产品。它们都通过同步代码块进行线程安全控制。

生产者:

  1. public class producer extends Thread{
  2. Product product;
  3. public Producter(Product product) {
  4. this.product = product;
  5. }
  6. @Override
  7. public void run() {
  8. while (true){
  9. synchronized (product){
  10. // 如果当前产品未被消费,则进行等待
  11. if (product.flag == true){
  12. try {
  13. product.wait();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. // 如果没有产品则生产一个新产品
  19. product.name = "car";
  20. product.price = 2000;
  21. try {
  22. Thread.sleep(2000);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. System.out.println("The product has been produced...");
  27. // 重置标识符
  28. product.flag = true;
  29. // 唤醒消费者
  30. product.notify();
  31. }
  32. }
  33. }
  34. }

消费者:

  1. public class Consumer extends Thread{
  2. Product product;
  3. public Consumer(Product product) {
  4. this.product = product;
  5. }
  6. @Override
  7. public void run() {
  8. while (true){
  9. synchronized (product){
  10. // 如果当前没有产品可供使用,进行等待
  11. if (product.flag == false){
  12. try {
  13. product.wait();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. // 如果有产品可以使用,进行消费
  19. System.out.println("Consuming product...");
  20. // 重置标识符
  21. product.flag = false;
  22. // 消费完产品唤醒生产者
  23. product.notify();
  24. System.out.println("Please produce a new product...");
  25. }
  26. }
  27. }
  28. }

然后我们在main()中分别常见生产者和消费者线程,并使用start()启动两个线程:

  1. public class producerAndConsumer {
  2. public static void main(String[] args) {
  3. Product p = new Product();
  4. new producer(p).start();
  5. new Consumer(p).start();
  6. }
  7. }

输出如下:

  1. The product has been produced...
  2. Consuming product...
  3. Please produce a new product...
  4. The product has been produced...
  5. Consuming product...
  6. Please produce a new product...
  7. The product has been produced...
  8. Consuming product...
  9. Please produce a new product...
  10. The product has been produced...
  11. Consuming product...
  12. Please produce a new product...
  13. The product has been produced...
  14. Consuming product...
  15. Please produce a new product...
  16. ...

7. 线程池

线程池指一个可容纳多个线程的容器,其中的线程可反复使用,节省了创建和销毁线程的开销。使用线程池的好处有:

  • 降低系统资源消耗:减少了创建和销毁线程的次数,池中每个线程可被重复利用,可执行多个任务
  • 提高响应速度:当新任务到达时,任务可以不等待线程创建就立即执行
  • 提高线程的可管理性:可以根据系统的承受能力,调整线程池中工作线程的数目,防止消耗过多的资源

创建线程池需使用java.util.concurrent.Excutors这个线程池的工厂类,它用于生产线程池。其中Excutors中的静态方法有:

  • static ExecutorService newFixedThreadPool(int nThreads):它用于创建一个可重用的指定线程数的线程池,返回值是ExecutorService 接口的实现类对象,后续可以使用ExecutorService 接口接收

java.util.concurrent.ExecutorService 是线程池接口,其中submit(Runnable task)用于提交一个Runnable任务用于执行,shutdown()用于销毁线程池。

线程池的使用步骤为:

  • 使用线程池的工厂类Excutors的FixedThreadPool()生产一个指定数目的线程池
  • 创建Runnable接口的实现类,并重写run(),设置线程任务
  • 调用ExecutorService 中的submit()传递线程任务,开启线程,执行run()
  • 调用ExecutorService 中的shutdown()销毁线程池(尽量不用)
  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class ThreadPool {
  4. public static void main(String[] args) {
  5. ExecutorService es = Executors.newFixedThreadPool(3);
  6. Runnable r = new Runnable() {
  7. @Override
  8. public void run() {
  9. System.out.println(Thread.currentThread().getName());
  10. }
  11. };
  12. es.submit(r);
  13. es.submit(r);
  14. es.submit(r);
  15. //es.shutdown()
  16. }
  17. }
  18. /*
  19. pool-1-thread-1
  20. pool-1-thread-3
  21. pool-1-thread-2
  22. */