讲述线程间通信机制(等待唤醒机制),完成生产者消费者模型的案例代码

第一章   线程

什么是线程?

  • 线程(Thread)是一个程序内部的一条执行路径。
  • 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
  • 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
public static void main(String[] args){
        //代码.....
        for(int i = 0; i < 10; i++){
            System.out.println(i);
        }
        //代码.....
    }

1.1多线程

 什么是多线程?

 多线程是指从软硬件上实现多条执行流程的技术。

一般来说,消息通信,淘宝,京东系统都离不开多线程

1.2多线程的创建

翻阅 API 后得知创建线程的方式总共有两种,一种是继承Thread 类方式,一种是实现 Runnable 接口方式。

1.2.1  Thread类 

Java是通过java.lang.Thread类来代表线程。

按照面向对象的思想,Thread类应该提供了实现多线程的方式。

步骤如下:

  • 继承Thread类
  • 重写run方法
  • 创建线程对象
  • 调用start()方法启动

构造方法:

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

常用方法: 

  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

 代码如下:

自定义线程类:

/**
 * 类继承Thread 表示该类是一个线程类
 *  需要重写run方法,该方法就是多线程要执行的功能
 */
public class MyThread01 extends Thread{
    public void run(){
        for (int i = 0;i < 10; i++){
            System.out.println("打游戏...."+i);
        }
    }
}
public class MyThread02 extends Thread{
    public void run(){
        for (int i = 0;i<=10;i++){
            System.out.println("听歌...."+i);
        }
    }
}

测试类:

/**
 * 多线程可以让多个任务同时(并发 和 并行 )执行,实现步骤
 * 1.自定义类,继承Thread 类;
 * 2.在类中重写重号run方法;
 * 3.在调用的位置,实例化线程类的对象,然层调用start方法,启动线程自动执行run方法
 */
public class ThreadTest01 {
    public static void main(String[] args) {
        //实例化线程类的对象
        MyThread01 t1 = new MyThread01();
        MyThread02 t2 = new MyThread02();
        //启动线程,实现了多线程任务,多个任务交替执行
        t1.start();//自动调用run方法
        t2.start();
    }
}

运行结果如下:

使用Thread类创建多线程的优点以及缺点:

优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

1.2.2   Runnable接口 

采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。

设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread 类实现了 Runnable。激活的意思是说某个线程已启动并且尚未停止。

步骤如下: 

  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 创建MyRunnable任务对象
  • 把MyRunnable任务对象交给Thread处理。
  • 调用线程对象的start()方法启动线程

 常用方法:

  •  run()使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。 

代码如下:

public class MyRunnable01 implements Runnable{
    public void run(){
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"Runnable方式执行听歌..."+i);
        }
    }
}
/**
 * Runnable接口实现多线程的步骤:
 *          1.自定义类实现 Runnable 接口;
 *          2.重写run方法,里面放入多线程执行的功能:
 *          3.创建Runnable接口实现类的对象:
 *          4.把Runnable接口实现类的对象作为参数,创建线程对象(把接口对象加入到线程执行中去);
 *          5.线程类对象启动线程---- 自动调用run 方法
 */
public class ThreadTest02 {
    public static void main(String[] args) {
        MyRunnable01 r1 = new MyRunnable01();
        //把上面的接口实现类对象,加入到线程中
        Thread t1=new Thread(r1,"库洛米");
        t1.start();
    }
}

 运行结果如下:

使用Runnable()接口创建多线程优点以及缺点:

优点:线程任务类只是实现接口,可以继承类和实现接口,扩展性强。

缺点:编程多一层对象包装,如果线程有执行结果不可以直接返回的。 

1.2.3   实现Runnable接口(匿名内部类形式)

 使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

步骤如下:

  • 创建Runnable的匿名内部类对象。
  • 交给Thread处理
  • 调用线程对象的start()启动线程。

举例如下:

package com.hp.test16;

public class UnNameTest {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++){
                    System.out.println("库洛米:"+i);
                }
            }
        };
        new Thread(r).start();
        for (int i = 0; i < 10; i++){
            System.out.println("玉桂狗:"+i);
        }
    }
}

 运行结果如下:

 1.3Thread和Runnable的区别

如果一个类继承 Thread ,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。
总结:
实现 Runnable 接口比继承 Thread 类所具有的优势:
  • 适合多个相同的程序代码的线程去共享同一个资源。
  • 可以避免java中的单继承的局限性。
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  • 线程池只能放入实现RunableCallable类线程,不能直接放入继承Thread的类。
扩充:在 java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个 JVM ,每一个 JVM 其实在就是在操作系统中启动了一个进程

第二章   线程安全

2.1   线程安全 

线程安全问题:

  • 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。 

 线程安全问题出现的原因:

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源

举个例子:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “芭比公主豪宅日记 ,本次电影的座位共 50
( 本场电影只能卖 50 张票 )
我们来模拟电影院的售票窗口,实现多个窗口同时卖 “芭比公主豪宅日记”这场电影票 ( 多个窗口一起卖这5 0 张票 )
需要窗口,采用线程对象来模拟;需要票, Runnable 接口子类来模拟

我们来分析一下:

  • 需要提供一个卖票类,创建一个类对象代表卖票的数量。
  • 需要定义一个线程类,线程类可以处理电影票。
  • 创建2个线程对象,传入一个卖票类。
  • 启动2个线程,去卖同种电影票。

卖票类:

public class Ticket implements Runnable{
    /**
     * 执行卖票操作
     */
    private int ticket = 50;
    @Override
    public void run(){
        //获取当前程序对应执行线程的 名称
        String tn = Thread.currentThread().getName();
        while (true){
            //1.获得票的变量
            if (ticket>0){
                //2.模拟卖票aqq
                System.out.println(tn+"正在卖第"+ticket+"张票");
                //模拟出票,需要一段时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //把票的总数-1
                ticket--;
            }else {
                //票卖完了,结束循环
                break;
            }
            }
        }
    }

测试类:

public class TreadTest01 {
    public static void main(String[] args) {
        //实例化卖票对象
        Ticket ticket = new Ticket();
        //定义线程,指定线程执行卖票程序,启动线程
        Thread t1 = new Thread(ticket,"窗口1");
        Thread t2 = new Thread(ticket,"窗口2");
        t1.start();
        t2.start();
    }
}

 运行结果如下:

 2.2   线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题 : 也就是解决重复取钱与不存在多余10w问题, Java 中提供了同步机制 (synchronized ) 来解决。
为了保证每个线程都能正常执行原子操作 ,Java 引入了线程同步机制。
那么怎么去使用呢?有三种方式完成同步操作:
  • 同步代码块。
  • 同步方法。
  • 锁机制。

2.3   同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
  • 作用:把出现线程安全问题的核心代码给上锁。
  • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

格式如下:

synchronized(同步锁){

操作共享资源的代码(核心代码)

}
同步锁 :
对象的同步锁只是一个概念 , 可以想象为在对象上标记了一个锁 .
1. 锁对象 可以是任意类型。
2. 多个线程对象 要使用同一把锁

锁对象的规范要求规范上:

  • 建议使用共享资源作为锁对象。
  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

使用同步代码块解决代码: 

public class Ticket implements Runnable {
    /**
     * 执行卖票操作
     */
    private int ticket = 50;
    Object lock = new Object();

    @Override
    public void run() {
        //获取当前程序对应执行线程的 名称
        String tn = Thread.currentThread().getName();
        while (true) {
            synchronized (lock) {
                //1.获得票的变量
                if (ticket > 0) {
                    //2.模拟卖票aqq
                    System.out.println(tn + "正在卖第" + ticket + "张票");
                    //模拟出票,需要一段时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //把票的总数-1
                    ticket--;
                } else {
                    //票卖完了,结束循环
                    break;
                }
            }
        }
    }
}

2.4   同步方法

使用 synchronized 修饰的方法 , 就叫做同步方法 , 保证 A 线程执行该方法的时候 , 其他线程只能在方法外等着。
格式:
public synchronized void method (){
可能会产生线程安全问题的代码
}
同步方法底层原理
  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

同步代码块与同步方法的区别:

  • 同步代码块的范围更小
  • 同步方法的范围更大 

2.5   Lock锁

  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
  • Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
Lock 锁也称同步锁,加锁与释放锁方法化了,如下:
  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。
public class Ticket03 implements Runnable{
    /**
     * 执行卖票操作
     */
    private int ticket = 50;
    //定义lock锁对象
    Lock lock = new ReentrantLock();

    @Override
    public void run(){
        //获取当前程序对应执行线程的 名称
        String tn = Thread.currentThread().getName();
        while (true){
            //锁定
        lock.lock();
        try {


            //1.获得票的变量
            if (ticket>0){
                //2.模拟卖票aqq
                System.out.println(tn+"正在卖第"+ticket+"张票");
                //模拟出票,需要一段时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //把票的总数-1
                ticket--;
            }else {
                //票卖完了,结束循环
                break;
            }
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            //解除锁定
            lock.unlock();
        }
        }
    }
}

第三章   线程状态

3.1   线程状态概述

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在APIjava.lang.Thread.State 这个枚举中给出了六种线程状态:

1.新建状态(New)

新创建了一个线程对象,但还没有调用start()方法。实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了新建状态。

2.就绪状态(Runnable)

新建状态的线程,调用线程的start()方法,此线程进入就绪状态。

3.运行状态(Running)

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。

4.阻塞状态(Blocked)

线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

5.等待状态/计时等待(Waiting/Timed_Waiting)   

线程进入等待状态有三种方式:

 cpu调度给优先级更高的线程

线程要等待获得资源或者信号

时间片的轮转,时间片到了,进入等待状态

在可执行状态下,如果调用 sleep()、  wait()等方法,线程都将进入等待状态。

sleep() 和 wait() 的区别:
       sleep方法是Thread类里面的,主要的意义就是让当前线程停止执行,让出CPU给其他的线程,但是不会释放对象锁资源以及监控的状态,当指定的时间到了之后又会自动恢复运行状态。
       wait方法是Object类里面的,主要的意义就是让线程放弃当前的对象的锁,进入等待此对象的等待锁定池,只有针对此对象调动notify方法后本线程才能够进入对象锁定池准备获取对象锁进入运行状态。

6.终止状态(Terminated)

表示该线程已经执行完毕。当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。

3.2 Timed_Waiting(计时等待)

Timed Waiting API 中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?
在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在 run 方法中添加了 sleep 语句,这样就强制当前正在执行的线程休眠(暂停执行 ),以 减慢线程 。其实当我们调用了sleep 方法之后,当前执行的线程就进入到 休眠状态 ,其实就是所谓的 Timed Waiting( 计时等
)

3.3 BLOCKED(锁阻塞)

Blocked 状态在 API 中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获 取到锁,线程A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。
这是由 Runnable 状态进入 Blocked 状态。除此 Waiting 以及 Time Waiting 状态也会在某种情况下进入阻塞状态。

3.4 Waiting(无限等待)

Wating 状态在 API 中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

第四章  等待唤醒机制

4.1  线程通信

所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

线程通信常见形式

  • 通过共享一个数据的方式实现。
  • 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

线程通信实际应用场景

  • 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
  • 要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

线程通信的前提:

线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。 

 4.2  等待唤醒机制

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

就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其他线程执行完他们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有等待的线程。

wait、notify就是线程间的一种协作机制。

等待唤醒中的方法:

  • wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费cpu资源,也不会竞争锁,这时的线程状态即是waiting。它还要等着别的线程执行一个特别的动作,也是“通知notify”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
  • notify:选取所通知对象的wait set中一个线程释放;例如,餐厅有位置后,等候就餐最久的顾客最先入座。
  • notifyAll:释放所通知对象的wait set上的全部线程。

 注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以需要再次常识去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。      

总结:如果能获取锁,线程就从waitiing状态变成runnable状态;

否则,从wait set出来,又进入entry set,线程就从waiting状态又变成blocked状态。

调用wait和notify方法需要注意的细节:

        1.wait方法与notify方法必须要由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。

        2.wait方法与notify方法是属于Object类的方法的。因为锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。

        3.wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为必要通过锁对象调用这两个方法。

案例生产者消费者模型

 我们就拿买牛奶和喝牛奶来举例子:

奶箱类 ( Box) : 定义一个成员变量, 表示第 x 瓶奶, 提供存储牛奶和获取牛奶的操作

生产者类 (Producer) : 实现 Runnable 接口, 重写 run() 方法, 调用存储牛奶的操作

消费者类 (Customer) : 实现 Runnable 接口, 重写 run() 方法, 调用获取牛奶的操作

测试类 ( BoxTest) : 里面有 main 方法。

main 方法中的代码步骤如下:

  • ①创建奶箱对象, 这是共享数据区域
  • ②创建生产者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用存储牛奶的操作
  • ③创建消费者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用获取牛奶的操作
  • ④创建 2 个线程对象, 分别把生产者对象和消费者对象作为构造方法参数传递
  • ⑤启动线程

代码实现:

奶箱类 ( Box) :

public class Box {
    //定义一个成员变量,表示第几瓶奶
    private int milk;
    //定义一个成员变量,表示奶箱的状态
    private boolean state = false;
    //提供储存牛奶和获取牛奶的操作
    public synchronized void put(int milk){
        //如果有牛奶,等待消费
        if (state){
            try {
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        //如果没有牛奶 就生产牛奶
        this.milk = milk;
        System.out.println("奶箱里放了第"+this.milk+"瓶奶了哦");
        //生产完毕之后,修改奶箱的状态
        state = true;
        //唤醒其他等待的线程
        notify();
    }
    public synchronized void get(){
        //如果没有牛奶,就等待生产
        if (!state){
            try {
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        //如果有牛奶,就消费牛奶
        System.out.println("我要喝第"+this.milk+"瓶奶了哦");
        //消费完毕后,修改奶箱状态
        state = false;
        //唤醒其他等待的线程
        notify();
    }

}

生产者类 (Producer) :

public class Producer implements Runnable {
    private Box b;

    public Producer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            b.put(i);
        }
    }
}

消费者类 (Customer) :

public class Customer implements Runnable{
    private Box b;
    public Customer(Box b){
        this.b = b;
    }
    @Override
    public void run() {
        while (true){
            b.get();
        }
    }
}

测试类 ( BoxTest) :

public class BoxTest {
    public static void main(String[] args) {
        //创建奶箱对象, 这是共享数据区域
        Box b = new Box();
        //创建生产者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用存储牛奶的操作
        Producer p = new Producer(b);
        //创建消费者对象, 把奶箱对象作为构造方法参数传递, 因为在这个类中要调用获取牛奶的操作
        Customer c = new Customer(b);
        //创建 2 个线程对象, 分别把生产者对象和消费者对象作为构造方法参数传递
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);
        //启动线程
        t1.start();
        t2.start();
    }
}

运行结果如下:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值