Skip to content

Java 的多线程

线程的英语是 thread,

进程的英语是 process。

多线程的英语是 multithreading,单线程的英语是 single thread。

查看进程与线程

每个应用程序的运行都是一个进程。运行多个应用程序就有多个进程。

每个进程可以包含多个线程。

线程调度

  • 分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间
  • 抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个线程

Java 使用的为抢占式调度。

多线程程序的优点

如果是一个单核 CPU,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,那为何仍需多线程呢?

考虑到充分利用多核 CPU,多线程程序的优点有:

  • 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
  • 提高计算机系统 CPU 的利用率
  • 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

单核 CPU 和多核 CPU

单核 CPU,在一个时间单元内,只能执行一个线程的任务。例如,可以把 CPU 看成是医院的医生诊室,在一定时间内只能给一个病人诊断治疗。所以单核 CPU 就是,代码经过前面一系列的前导操作(类似于医院挂号,比如有 10 个窗口挂号),然后到 cpu 处执行时发现,就只有一个 CPU(对应一个医生),大家排队执行。

这时候想要提升系统性能,只有两个办法,要么提升 CPU 性能(让医生看病快点),要么多加几个 CPU(多整几个医生),即为多核的 CPU。

多核的效率是单核的倍数吗?

譬如 4 核 A53 的 cpu,性能是单核 A53 的 4 倍吗?理论上是,但是实际不可能,至少有两方面的损耗。

  • 一个是多个核心的其他共用资源限制。譬如,4 核 CPU 对应的内存、cache、寄存器并没有同步扩充 4 倍。这就好像医院一样,1 个医生换 4 个医生,但是做 B 超检查的还是一台机器,性能瓶颈就从医生转到 B 超检查了
  • 另一个是多核 CPU 之间的协调管理损耗。譬如多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以工作效率绝对不可能达到 2 倍

并行与并发

  • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。或者说在同一时刻,有多条指令在多个 CPU 上同时执行。
  • 并发(concurrency):指两个或多个事件在同一个时间段内发生。或者说在一段时间内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。

在操作系统中,启动了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单核 CPU 系统中,每一时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多核 CPU 系统中,则这些可以并发执行的程序便可以分配到多个 CPU 上,实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

创建和启动线程

Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。

Thread 类的特性:

  • 每个线程执行的内容由某个特定 Thread 对象的 run() 方法体来决定,因此把 run() 方法体称为线程执行体
  • 通过该 Thread 对象的 start() 方法来启动这个线程,而非直接调用 run() 方法
  • 要想实现多线程,必须在主线程中创建新的线程对象

方式 1,继承 Thread 类

Java 通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 定义 Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程需要完成的任务
  2. 创建 Thread 子类的实例,即创建了线程对象
  3. 调用线程对象的 start() 方法来启动该线程

注意

  1. 如果自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式
  2. run() 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU 调度决定
  3. 想要启动多线程,必须调用 start 方法
  4. 一个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”

举个例子:

java
package javacode.multithread;

public class ThreadTest1 {
    public static void main(String[] args) {
        // 创建自定义线程对象 1
        MyThread mt1 = new MyThread("子线程 1");
        // 开启子线程 1
        mt1.start();

        // 创建自定义线程对象 2
        MyThread mt2 = new MyThread("子线程 2");
        //开启子线程 2
        mt2.start();

        // 在主方法中执行 for 循环
        for (int i = 0; i < 10000; i++) {
            System.out.println("main 线程!"+i);
        }
    }
}

// 自定义线程类
class MyThread extends Thread {
    // 定义指定线程名称的构造方法
    public MyThread(String name) {
        // 调用父类的 String 参数的构造方法,指定线程的名称
        super(name);
    }

    /**
     * 重写 run 方法,完成该线程执行的逻辑
     */
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(getName() + ":正在执行!" + i);
        }
    }
}

方式 2,实现 Runnable 接口

Java 有单继承的限制,当我们的类已经继承了一个类导致无法继承 Thread 类时,那么该如何做呢?

在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run() 方法, 然后再通过 Thread 类的对象代理启动和执行我们的线程体 run() 方法。

步骤如下:

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正 的线程对象
  3. 调用线程对象的 start() 方法,启动线程。最终 JVM 会在合适的时机自动调用 Runnable 接口实现类的 run 方法

举例如下:

java
package javacode.multithread;

public class RunableThreadTest {
    public static void main(String[] args) {
        // 创建自定义类对象,或者说线程任务对象
        MyRunnable mr = new MyRunnable();
        // 创建线程对象
        Thread t = new Thread(mr, "长江");
        // 启动线程
        t.start();

        // 主线程执行代码
        for (int i = 0; i < 10000; i++) {
            System.out.println("黄河 " + i);
        }
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            // Thread.currentThread().getName() 用于获取当前线程的名字
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

通过实现 Runnable 接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在 run 方法里面。

在启动多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象, 然后调用 Thread 对象的 start() 方法来运行多线程代码。

实际上,所有的多线程代码都是通过运行 Thread 的 start() 方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。

TIP

Runnable 对象仅仅作为 Thread 对象的target,Runnable 实现类里包含的 run() 方法仅作为线程执行体。 而实际的线程对象依然是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run() 方法。

变形写法

使用匿名内部类对象来实现线程的创建和启动。

java
package javacode.multithread;

public class AnnymousThread {
    public static void main(String[] args) {

        new Thread("新的线程!"){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println(getName()+":正在执行!"+i);
                }
            }
        }.start();

        for (int i = 0; i < 1000; i ++) {
            System.out.println("主线程++++++++++++++++++");
        }
    }
}
java
package javacode.multithread;

public class AnnymousThread2 {
    public static void main(String[] args) {

        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println(Thread.currentThread().getName()+":" + i);
                }
            }
        }).start();

        for (int i = 0; i < 1000; i ++) {
            System.out.println("主线程++++++++++++++++++");
        }
    }
}

对比继承 Thread 类和实现 Runnable 接口两种方式

Thread 类实际上也实现了 Runnable 接口。

区别在于:

  • 继承 Thread,线程代码存放在 Thread 类的子类的 run 方法中
  • 实现 Runnable,线程代码存在接口的实现类的 run 方法中

实现 Runnable 接口比继承 Thread 类所具有的优势:

  • 避免了单继承的局限性
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立

Thread 类的常用结构

构造器

  • public Thread(): 分配一个新的线程对象
  • public Thread(String name): 分配一个指定名字的新的线程对象
  • public Thread(Runnable target): 指定创建线程的目标对象,它实现了 Runnable 接口中的 run 方法
  • public Thread(Runnable target, String name): 分配一个带有指定目标新的线程对象并指定名字

常用方法系列 1

  • public void run(): 此线程要执行的任务代码放在这个方法里面
  • public void start(): 让此线程开始执行,让 Java 虚拟机在某个时刻调用此线程的 run 方法
  • public String getName(): 获取当前线程名称
  • public void setName(String name): 设置该线程名称
  • public static Thread currentThread(): 返回当前正在执行的线程对象的引用。 在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
  • public static void sleep(long millis): 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
  • public static void yield(): yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次, 希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是, 当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行

常用方法系列 2

  • public final boolean isAlive(): 测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态
  • void join(): 等待该线程终止
  • void join(long millis): 等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间到,将不再等待
  • void join(long millis, int nanos): 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
  • public final void stop(): 已过时,不建议使用。 强行结束一个线程的执行,让其直接进入死亡状态。run() 方法体即刻停止,可能会导致一些清理性的工作得不到完成, 如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题
  • void suspend() / void resume(): 已过时,不建议使用。这两个操作就好比播放器的暂停和恢复。 二者必须成对出现,否则非常容易发生死锁。suspend() 调用会导致线程暂停,但不会释放任何锁资源, 导致其它线程都无法访问被它占用的锁,直到调用 resume()

常用方法系列 3

每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。 优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。

Thread 类的三个优先级常量:

  • MAX_PRIORITY(10): 最高优先级

  • MIN _PRIORITY(1): 最低优先级

  • NORM_PRIORITY(5): 普通优先级,默认情况下 main 线程具有普通优先级

  • public final int getPriority(): 返回线程优先级

  • public final void setPriority(int newPriority): 改变线程的优先级,范围在 [1,10] 之间

练习 1

获取 main 线程对象的名称和优先级。

声明一个匿名内部类继承 Thread 类,重写 run 方法,在 run 方法中获取线程名称和优先级。 设置该线程优先级为最高优先级并启动该线程。

java
package javacode.multithread;

public class Examp1 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                System.out.println(getName() + "的优先级:" + getPriority());
            }
        };
        t.setPriority(Thread.MAX_PRIORITY);
        t.start();
        System.out.println(Thread.currentThread().getName() + "的优先级:" + Thread.currentThread().getPriority());
    }
}

练习 2

声明一个匿名内部类继承 Thread 类,重写 run 方法,实现打印 [1,100] 之间的偶数,要求每隔 1 秒打印 1 个偶数。

声明一个匿名内部类继承 Thread 类,重写 run 方法,实现打印 [1,100] 之间的奇数,

  • 当打印到 5 时,让奇数线程暂停一下,再继续。
  • 当打印到 5 时,让奇数线程停下来,让偶数线程执行完再打印。

了解守护线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线程被称为“守护线程”。 JVM 的垃圾回收线程就是典型的守护线程。

守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。正所谓兔死狗烹,鸟尽弓藏。

调用 setDaemon(true) 方法可将指定线程设置为守护线程。 必须在线程启动之前设置,否则会报 IllegalThreadStateException 异常。

调用 isDaemon() 可以判断线程是否是守护线程。

java
package javacode.multithread;

public class DaemonT {
    public static void main(String[] args) {
        MyDaemon m = new MyDaemon();
        m.setDaemon(true);
        m.start();
        for (int i = 1; i <= 1000; i++) {
            System.out.println("main:" + i);
        }

    }
}

class MyDaemon extends Thread {
    public void run() {
        while (true) {
            System.out.println("我一直守护者你...");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程的生命周期

Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中会经历很多状态。

JDK1.5 之前有 5 种状态

线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。 CPU 需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。

1 新建

当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。 此时它和其他 Java 对象一样,仅仅由 JVM 为其分配了内存,并初始化了实例变量的值。 此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体 run()。

2 就绪

但是当线程对象调用了 start() 方法之后,就不一样了,线程就从新建状态转为就绪状态。 JVM 会为其创建方法调用栈和程序计数器,当然,处于这个状态中的线程并没有开始运行, 只是表示已具备了运行的条件,随时可以被调度。至于什么时候被调度,取决于 JVM 里线程调度器的调度。

注意

程序只能对新建状态的线程调用 start(),并且只能调用一次, 如果对非新建状态的线程,如已启动的线程或已死亡的线程调用 start() 都会报错 IllegalThreadStateException 异常。

3 运行

如果处于就绪状态的线程获得了 CPU 资源时,开始执行 run() 方法的线程体代码,则该线程处于运行状态。

如果计算机只有一个 CPU 核心,在任何时刻只有一个线程处于运行状态,如果计算机有多个核心,将会有多个线程并行(Parallel)执行。

当然,美好的时光总是短暂的,而且 CPU 讲究雨露均沾。对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务, 当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。 此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。

4 阻塞

当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执行,进入阻塞状态:

  • 线程调用了 sleep() 方法,主动放弃所占用的 CPU 资源
  • 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有
  • 线程执行过程中,同步监视器调用了 wait(),让它等待某个通知(notify)
  • 线程执行过程中,同步监视器调用了 wait(time)
  • 线程执行过程中,遇到了其他线程对象的加塞(join)
  • 线程被调用 suspend 方法被挂起(已过时,因为容易发生死锁)

当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况, 当发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度它:

  • 线程的 sleep() 时间到
  • 线程成功获得了同步监视器
  • 线程等到了通知 (notify)
  • 线程 wait 的时间到了
  • 加塞的线程结束了
  • 被挂起的线程又被调用了 resume 恢复方法(已过时,因为容易发生死锁)

5 死亡

线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:

  • run() 方法执行完成,线程正常结束
  • 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
  • 直接调用该线程的 stop() 来结束该线程(已过时)

JDK1.5 及之后有 6 种状态

在 java.lang.Thread.State 的枚举类中这样定义:

java
public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}
  • NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法
  • RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只能标记为可运行,至于什么时候运行,不是 JVM 来控制, 而是 OS 来进行调度的,而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分
  • Teminated(被终止):表明此线程已经结束生命周期,终止运行
  • 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:BLOCKED、WAITING、TIMED_WAITING
    • BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。 比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态
    • TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。 当前线程执行过程中遇到 Thread 类的 sleep 或 join,Object 类的 wait,LockSupport 类的 park 方法, 并且在调用这些方法时,设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间到,或被中断
    • WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。 当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的 join,LockSupport 类的 park 方法, 并且在调用这些方法时,没有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒
      • 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的 notify/notifyAll 唤醒
      • 通过 Condition 的 await 进入 WAITING 状态的要有 Condition 的 signal 方法唤醒
      • 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有 LockSupport 类的 unpark 方法唤醒
      • 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join 方法的线程对象结束才能让当前线程恢复

TIP

当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现当前线程没有得到监视器锁, 那么会立刻转入 BLOCKED 状态。

举例:

java
package javacode.multithread;

public class ThreadStateTest {
    public static void main(String[] args) throws InterruptedException {
        SubThread t = new SubThread();
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start();

        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 状态 " + t.getState());
            Thread.sleep(500);
        }
        System.out.println(t.getName() + " 状态 " + t.getState());
    }
}

class SubThread extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("打印:" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

线程安全问题及解决

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候, 若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

同一个资源问题和线程安全问题

假设火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位共 100 个(即只能出售 100 张火车票)。 我们来模拟车站的售票窗口,实现多个窗口同时售票的过程。注意不能出现错票、重票。

以下的 5 个方法都不能解决线程安全问题。

局部变量不能共享

因为局部变量不能共享,每个线程的 run() 的 ticket 是独立的,最终会发现卖出 300 张票,所以这种方式不可行。

java
package javacode.multithread;

public class SaleTicketDemo1 {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("窗口 1");
        w2.setName("窗口 2");
        w3.setName("窗口 3");

        w1.start();
        w2.start();
        w3.start();
    }
}

class Window extends Thread {
    public void run() {
        int ticket = 100;
        while (ticket > 0) {
            System.out.println(getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

不同对象的实例变量不共享

不同的实例对象的实例变量是独立的,所以这种方法不可行。

java
package javacode.multithread;

class TicketWindow extends Thread {
    private int ticket = 100;

    public void run() {
        while (ticket > 0) {
            System.out.println(getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class SaleTicketDemo2 {
    public static void main(String[] args) {
        TicketWindow w1 = new TicketWindow();
        TicketWindow w2 = new TicketWindow();
        TicketWindow w3 = new TicketWindow();

        w1.setName("窗口 1");
        w2.setName("窗口 2");
        w3.setName("窗口 3");

        w1.start();
        w2.start();
        w3.start();
    }
}

静态变量是共享的

TicketThread 类的静态变量,被所有 TicketThread 类的实例对象共享。

运行代码的结果发现卖出近 100 张票。

但是发现有重复票或负数票问题。模拟不了两场电影各卖100张票的情况。

java
package javacode.multithread;

class TicketSaleThread extends Thread {
    private static int ticket = 100;

    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10); // 加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class SaleTicketDemo3 {
    public static void main(String[] args) {
        TicketSaleThread t1 = new TicketSaleThread();
        TicketSaleThread t2 = new TicketSaleThread();
        TicketSaleThread t3 = new TicketSaleThread();

        t1.setName("窗口 1");
        t2.setName("窗口 2");
        t3.setName("窗口 3");

        t1.start();
        t2.start();
        t3.start();
    }
}

同一个对象的实例变量共享

多个 Thread 线程使用同一个 Runnable 对象。

发现卖出近 100 张票,有重复票或负数票问题。

java
package javacode.multithread;

class TicketSaleRunnable implements Runnable {
    private int ticket = 100;

    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class SaleTicketDemo4 {
    public static void main(String[] args) {
        TicketSaleRunnable tr = new TicketSaleRunnable();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}

抽取资源类,共享同一个资源对象

发现卖出近 100 张票,但是有重复票或负数票问题。

java
package javacode.multithread;

//1、编写资源类
class Ticket {
    private int ticket = 100;

    public void sale() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        } else {
            throw new RuntimeException("没有票了");
        }
    }

    public int getTicket() {
        return ticket;
    }
}

public class SaleTicketDemo5 {
    public static void main(String[] args) {
//2、创建资源对象
        Ticket ticket = new Ticket();

        //3、启动多个线程操作资源类的对象
        Thread t1 = new Thread("窗口一") {
            public void run() {
                while (true) {
                    ticket.sale();
                }
            }
        };
        Thread t2 = new Thread("窗口二") {
            public void run() {
                while (true) {
                    ticket.sale();
                }
            }
        };
        Thread t3 = new Thread(new Runnable() {
            public void run() {
                ticket.sale();
            }
        }, "窗口三");


        t1.start();
        t2.start();
        t3.start();
    }
}

同步机制解决线程安全问题

要解决上述多线程并发访问一个资源的安全性问题也就是解决重复票与不存在票问题,Java 中提供了同步机制 synchronized 来解决。

窗口 1 线程进入操作的时候,窗口 2 和窗口 3 线程只能在外等着,窗口 1 操作结束,窗口 1 和窗口 2 和窗口 3 才有机会进入代码去执行。 也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺 CPU 资源,完成对应的操作, 保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java 引入了线程同步机制。 注意在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)。

同步机制解决线程安全问题的原理

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码, 都要先获得“锁”,我们称它为同步锁。因为 Java 对象在堆中的数据分为对象头、实例变量、空白的填充。

而对象头中包含:

  • Mark Word:记录了和当前对象有关的 GC、锁标记等信息
  • 指向类的指针,每一个对象需要记录它是由哪个类创建出来的
  • 数组长度(只有数组对象才有)

哪个线程获得了“同步锁”对象之后,“同步锁”对象就会记录这个线程的 ID, 这样其他线程就只能等待了,除非这个线程“释放”了锁对象,其他线程才能重新获得/占用“同步锁”对象。

同步代码块和同步方法

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
     需要同步操作的代码
}

同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

public synchronized void method(){
    可能会产生线程安全问题的代码
}

同步锁机制

在《Thinking in Java》中是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。 第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

synchronized 的锁是什么

同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程使用同一个“同步锁对象”。

对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或类名 .class),但是对于同步方法来说,同步锁对象只能是默认的:

静态方法:当前类的 Class 对象(类名 .class)

非静态方法:this

同步操作的思考顺序

1、如何找问题,即代码是否存在线程安全?

  1. 明确哪些代码是多线程运行的代码
  2. 明确多个线程是否有共享数据
  3. 明确多线程运行代码中是否有多条语句操作共享数据

2、如何解决?

对多条操作共享数据的语句,在一个线程执行完之前,其他线程不可以参与执行。

3、切记

  • 范围太小,不能解决安全问题
  • 范围太大,一旦某个线程抢到锁,其他线程就只能等待,效率会降低,不能合理利用 CPU 资源

代码演示

1 静态方法加锁
java
package javacode.multithread;

class TicketSale1 extends Thread{
    private static int ticket = 100;
    public void run(){ // 直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (ticket > 0) {
            saleOneTicket();
        }
    }

    // 锁对象是 TicketSale1 类的 Class 对象,而一个类的 Class 对象在内存中肯定只有一个
    public synchronized static void saleOneTicket(){
        if(ticket > 0) { // 不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class AsyncTicketSale1 {
    public static void main(String[] args) {
        TicketSale1 t1 = new TicketSale1();
        TicketSale1 t2 = new TicketSale1();
        TicketSale1 t3 = new TicketSale1();

        t1.setName("窗口 1");
        t2.setName("窗口 2");
        t3.setName("窗口 3");

        t1.start();
        t2.start();
        t3.start();
    }
}
2 非静态方法加锁
java
package javacode.multithread;

class TicketSale2 implements Runnable {
    private int ticket = 100;

    public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (ticket > 0) {
            saleOneTicket();
        }
    }

    public synchronized void saleOneTicket() {//锁对象是 this,这里就是 TicketSaleRunnable 对象,因为上面 3 个线程使用同一个 TicketSaleRunnable 对象,所以可以
        if (ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}

public class AsyncTicketSale2 {
    public static void main(String[] args) {
        TicketSale2 tr = new TicketSale2();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}
3 同步代码块
java
package javacode.multithread;

//1、编写资源类
class Ticket3 {
    private int ticket = 1000;

    public void sale() { // 也可以直接给这个方法加锁,锁对象是 this,这里就是 Ticket3 对象
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        } else {
            throw new RuntimeException("没有票了");
        }
    }

    public int getTicket() {
        return ticket;
    }
}

public class AsyncTicketSale3 {
    public static void main(String[] args) {
        //2、创建资源对象
        Ticket3 ticket = new Ticket3();

        //3、启动多个线程操作资源类的对象
        Thread t1 = new Thread("窗口一") {
            public void run() {//不能给 run() 直接加锁,因为 t1,t2,t3 的三个 run 方法分别属于三个 Thread 类对象,
                // run 方法是非静态方法,那么锁对象默认选 this,那么锁对象根本不是同一个
                while (true) {
                    synchronized (ticket) {
                        ticket.sale();
                    }
                }
            }
        };
        Thread t2 = new Thread("窗口二") {
            public void run() {
                while (true) {
                    synchronized (ticket) {
                        ticket.sale();
                    }
                }
            }
        };
        Thread t3 = new Thread(new Runnable() {
            public void run() {
                while (true) {
                    synchronized (ticket) {
                        ticket.sale();
                    }
                }
            }
        }, "窗口三");


        t1.start();
        t2.start();
        t3.start();
    }
}

同步

单例设计模式的线程安全问题

饿汉式没有线程安全问题

饿汉式在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的。

形式一:

java
package javacode.multithread;

public class HungrySingle {
    private static HungrySingle INSTANCE = new HungrySingle(); //对象是否声明为 final 都可以

    private HungrySingle(){}

    public static HungrySingle getInstance(){
        return INSTANCE;
    }
}

形式二:

java
package javacode.multithread;

/*
public class HungryOne2{
    public static final HungryOne2 INSTANCE = new HungryOne2();
    private HungryOne2(){}
}*/

public enum HungryOne2 {
    INSTANCE
}

测试类:

java
package javacode.multithread;

public class HungrySingleTest {
    static HungrySingle hs1 = null;
    static HungrySingle hs2 = null;

    // 演示存在的线程安全问题
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                hs1 = HungrySingle.getInstance();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                hs2 = HungrySingle.getInstance();
            }
        };
        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(hs1);
        System.out.println(hs2);
        System.out.println(hs1 == hs2);//true
    }
}

懒汉式线程安全问题

懒汉式延迟创建对象,第一次调用 getInstance 方法再创建对象。

形式一:

java
package javacode.multithread;

public class LazyOne {
    private static LazyOne instance;

    private LazyOne(){}

    // 方式 1
    public static synchronized LazyOne getInstance1(){
        if(instance == null){
            instance = new LazyOne();
        }
        return instance;
    }
    // 方式 2
    public static LazyOne getInstance2(){
        synchronized(LazyOne.class) {
            if (instance == null) {
                instance = new LazyOne();
            }
            return instance;
        }
    }
    // 方式 3
    public static LazyOne getInstance3(){
        if(instance == null){
            synchronized (LazyOne.class) {
                try {
                    Thread.sleep(10); // 加这个代码,暴露问题
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(instance == null){
                    instance = new LazyOne();
                }
            }
        }

        return instance;
    }
    /*
    注意:上述方式 3 中,有指令重排问题
    mem = allocate(); 为单例对象分配内存空间
    instance = mem;   instance 引用现在非空,但还未初始化
    ctorSingleton(instance); 为单例对象通过 instance 调用构造器
    从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要
    volatile 关键字,避免指令重排。
    */
}

形式二,使用内部类:

java
package javacode.multithread;

public class LazySingle {
    private LazySingle(){}

    public static LazySingle getInstance(){
        return Inner.INSTANCE;
    }

    private static class Inner{
        static final LazySingle INSTANCE = new LazySingle();
    }
}

TIP

内部类只有在外部类被调用才加载,产生 INSTANCE 实例;不用加锁。

此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。

此时的内部类,使用 enum 进行定义,也是可以的。

测试类:

java
package javacode.multithread;

public class TestLazy {
    static LazyOne s3;
    static LazyOne s4;
    public static void main(String[] args) {
        LazyOne s1 = LazyOne.getInstance1();
        LazyOne s2 = LazyOne.getInstance1();

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);

//        ===========================



        Thread t1 = new Thread(){
            public void run(){
                s3 = LazyOne.getInstance2();
            }
        };
        Thread t2 = new Thread(){
            public void run(){
                s4 = LazyOne.getInstance2();
            }
        };

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(s3);
        System.out.println(s4);
        System.out.println(s3 == s4);

    }
}

死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

举例 1:

java
package javacode.multithread;

public class DeadLockTest {
    public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();

        new Thread() {
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);

                    }
                }
            }
        }.start();

        new Thread() {
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);

                    }

                }
            }
        }.start();
    }
}

举例 2:

java
package javacode.multithread;

class A {
    public synchronized void foo(B b) {
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + " 进入了 A 实例的 foo 方法"); // ①
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + " 企图调用 B 实例的 last 方法"); // ③
        b.last();
    }

    public synchronized void last() {
        System.out.println("进入了 A 类的 last 方法内部");
    }
}

class B {
    public synchronized void bar(A a) {
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + " 进入了 B 实例的 bar 方法"); // ②
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + " 企图调用 A 实例的 last 方法"); // ④
        a.last();
    }

    public synchronized void last() {
        System.out.println("进入了 B 类的 last 方法内部");
    }
}

public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主线程");
        // 调用 a 对象的 foo 方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    public void run() {
        Thread.currentThread().setName("副线程");
        // 调用 b 对象的 bar 方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }
}

举例 3:

java
package javacode.multithread;

public class TestDeadLock {
    public static void main(String[] args) {
        Object g = new Object();
        Object m = new Object();
        Owner s = new Owner(g,m);
        Customer c = new Customer(g,m);
        new Thread(s).start();
        new Thread(c).start();
    }
}
class Owner implements Runnable{
    private Object goods;
    private Object money;

    public Owner(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }

    @Override
    public void run() {
        synchronized (goods) {
            System.out.println("先给钱");
            synchronized (money) {
                System.out.println("发货");
            }
        }
    }
}
class Customer implements Runnable{
    private Object goods;
    private Object money;

    public Customer(Object goods, Object money) {
        super();
        this.goods = goods;
        this.money = money;
    }

    @Override
    public void run() {
        synchronized (money) {
            System.out.println("先发货");
            synchronized (goods) {
                System.out.println("再给钱");
            }
        }
    }
}

诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

解决死锁:

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。

针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。

针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。

针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

JDK5.0 新特性 Lock 锁

  • JDK5.0 的新增功能,保证线程的安全。与采用 synchronized 相比, Lock 可提供多种锁方案,更灵活、更强大。Lock 通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当
  • java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
  • 在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。 ReentrantLock 类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。 此外,它还提供了在激烈争用情况下更佳的性能
  • Lock 锁也称同步锁,加锁与释放锁方法,如下:
    • public void lock(): 加同步锁
    • public void unlock(): 释放同步锁
  • 代码结构:
class A{
    // 1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
	private final ReentrantLock lock = new ReenTrantLock();
	public void m(){
        // 2. 调动 lock(),实现需共享的代码的锁定
		lock.lock();
		try{
			// 保证线程安全的代码;
		}
		// 如果同步代码有异常,要将 unlock() 写入 finally 语句块
		finally{
            // 3. 调用 unlock(),释放共享代码的锁定
			lock.unlock();  
		}
	}
}

举例:

java
package javacode.lock;

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{
    int ticket = 100;
    //1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
    private final ReentrantLock lock = new ReentrantLock();
    public void run(){

        while(true){
            try{
                //2. 调动 lock(),实现需共享的代码的锁定
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticket--);
                }else{
                    break;
                }
            }finally{
                //3. 调用 unlock(),释放共享代码的锁定
                lock.unlock();
            }
        }
    }
}

public class ThreadLock {
    public static void main(String[] args) {
        Window t = new Window();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        t2.start();
    }
}

synchronized 与 Lock 的对比

  • Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用域、遇到异常等自动解锁
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象
  • Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
  • Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以

开发建议中处理线程安全问题优先使用顺序为:Lock --> 同步代码块 --> 同步方法

线程的通信

线程间通信

为什么要处理线程间通信?

当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。

比如线程 A 用来生产包子的,线程 B 用来吃包子的,包子可以理解为同一资源,线程 A 与线程 B 处理的动作,一个是生产,一个是消费, 此时 B 线程必须等到 A 线程完成后才能执行,那么线程 A 与线程 B 之间就需要线程通信,即等待唤醒机制。

等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。

在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定 wait 的时间,等时间到了自动唤醒; 在有多个线程进行等待时,如果需要,可以使用 notifyAll() 来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了, 这时的线程状态是 WAITING 或 TIMED_WAITING。它还要等着别的线程执行一个特别的动作,也即“通知(notify)”或者等待时间到, 在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程

TIP

被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁, 所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行)状态
  • 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁)状态

举例

使用两个线程打印 1-100。线程 1, 线程 2 交替打印。

java
class Communication implements Runnable {
    int i = 1;
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                } else
                    break;
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

调用 wait 和 notify 需注意的细节

  • wait 方法与 notify 方法必须要由同一个锁对象调用。因为对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程
  • wait 方法与 notify 方法是属于 Object 类的方法的。因为锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的
  • wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用。 因为必须要通过锁对象调用这 2 个方法。否则会报 java.lang.IllegalMonitorStateException 异常

生产者与消费者问题

等待唤醒机制可以解决经典的“生产者与消费者”的问题。 生产者与消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem), 是一个多线程同步问题的经典案例。 该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。 该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

举例:

生产者 (Productor) 将产品交给店员 (Clerk),而消费者 (Customer) 从店员处取走产品, 店员一次只能持有固定数量的产品 (比如:20),如果生产者试图生产更多的产品, 店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产; 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

类似的场景,比如厨师和服务员等。

生产者与消费者问题中其实隐含了两个问题:

  • 线程安全问题:因为生产者与消费者共享数据缓冲区,产生安全问题。不过这个问题可以使用同步解决
  • 线程的协调工作问题:要解决该问题,就必须让生产者线程在缓冲区满时等待 (wait), 暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知 (notify) 正在等待的线程恢复到就绪状态, 重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待 (wait),暂停进入阻塞状态, 等到生产者往缓冲区添加数据之后,再通知 (notify) 正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题

代码实现:

java
package javacode.lock;

public class ConsumerProducerTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1 = new Producer(clerk);

        Consumer c1 = new Consumer(clerk);
        Consumer c2 = new Consumer(clerk);

        p1.setName("生产者 1");
        c1.setName("消费者 1");
        c2.setName("消费者 2");

        p1.start();
        c1.start();
        c2.start();
    }
}

//生产者
class Producer extends Thread{
    private Clerk clerk;

    public Producer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {

        System.out.println("=========生产者开始生产产品========");
        while(true){

            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //要求 clerk 去增加产品
            clerk.addProduct();
        }
    }
}

//消费者
class Consumer extends Thread{
    private Clerk clerk;

    public Consumer(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println("=========消费者开始消费产品========");
        while(true){

            try {
                Thread.sleep(90);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //要求 clerk 去减少产品
            clerk.minusProduct();
        }
    }
}

//资源类
class Clerk {
    private int productNum = 0;//产品数量
    private static final int MAX_PRODUCT = 20;
    private static final int MIN_PRODUCT = 1;

    //增加产品
    public synchronized void addProduct() {
        if(productNum < MAX_PRODUCT){
            productNum++;
            System.out.println(Thread.currentThread().getName() +
                    "生产了第" + productNum + "个产品");
            //唤醒消费者
            this.notifyAll();
        }else{

            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //减少产品
    public synchronized void minusProduct() {
        if(productNum >= MIN_PRODUCT){
            System.out.println(Thread.currentThread().getName() +
                    "消费了第" + productNum + "个产品");
            productNum--;

            //唤醒生产者
            this.notifyAll();
        }else{

            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

区分 sleep() 和 wait()

相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。

不同点:

  1. 定义方法所属的类:sleep(): Thread 中定义。wait(): Object 中定义
  2. 使用范围的不同:sleep() 可以在任何需要使用的位置被调用。wait() 必须使用在同步代码块或同步方法中
  3. 都在同步结构中使用的时候,是否释放同步监视器的操作不同,sleep() 不会释放同步监视器,wait() 会释放同步监视器
  4. 结束等待的方式不同:sleep() 指定时间一到就结束阻塞。wait() 可以指定时间也可以无限等待直到 notify 或 notifyAll

是否释放锁的操作

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?

释放锁的操作

当前线程的同步方法、同步代码块执行结束。

当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行。

当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致当前线程异常结束。

当前线程在同步代码块、同步方法中执行了锁对象的 wait() 方法,当前线程被挂起,并释放锁。

不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行。

线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该该线程挂起,该线程不会释放锁(同步监视器)。

应尽量避免使用 suspend() 和 resume() 这样的过时来控制线程。

JDK5.0 新增线程创建方式

新增方式一,实现 Callable 接口

  • 与使用 Runnable 相比,Callable 功能更强大些
    • 相比 run() 方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)
  • Future 接口
    • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
    • FutureTask 是 Futrue 接口的唯一的实现类
    • FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
  • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低

代码举例:

java
package javacode.lock;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/*
 * 创建多线程的方式三:实现 Callable(jdk5.0 新增的)
 */
//1.创建一个实现 Callable 的实现类
class NumThread implements Callable {
    //2.实现 call 方法,将此线程需要执行的操作声明在 call() 中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class CallableTest {
    public static void main(String[] args) {
        //3.创建 Callable 接口实现类的对象
        NumThread numThread = new NumThread();

        //4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
        new Thread(futureTask).start();


//      接收返回值
        try {
            //6.获取 Callable 中 call 方法的返回值
            //get() 返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

新增方式二,使用线程池

现有问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, 这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关 API:

  • JDK5.0 之前,我们必须手动自定义线程池。从 JDK5.0 开始,Java 内置线程池相关的 API。 在 java.util.concurrent 包下提供了线程池相关 API:ExecutorService 和 Executors
  • ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行 Callable
    • void shutdown():关闭连接池
  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

代码举例:

java
package javacode.lock;

import java.util.concurrent.*;

class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }

}

public class ThreadPoolTest {
    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限

        //2.执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象
        service.execute(new NumberThread());//适合适用于 Runnable
        service.execute(new NumberThread1());//适合适用于 Runnable

        try {
            Future future = service.submit(new NumberThread2());//适合使用于 Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }
}

Released under the MIT License.