猿创征文 | Java多线程-初级学习笔记


在学习完java的基本语法、对象、继承、多态、反射、集合等基础知识后
假如我要实现如下功能:

程序不停地在屏幕上输出一句问候的语句(比如“你好”),同时,当我通过键盘输入固定输入的时候,程序停止向屏幕输出问候的语句(比如说输入gun)。

用我之前所学习的技术似乎无法做到,这时候就需要了解多线程相关知识。

如何理解线程?

从代码的执行路径的角度来理解线程。

首先,Java程序中所有的代码都运行在一条执行路径当中,而一条执行路径其实就是一个线程。换言之,我们所有的代码都是运行在某个线程当中的。

  • 同一条执行路径的代码,他们只能顺序执行(main方法所在的那条执行路径当中)。
  • 不同执行路径中的代码他们互不影响相互独立,可以”同时执行“。
    在这里插入图片描述
    了解多线程之前,首先了解多进程。

(一)多进程

那么什么是进程呢?一个进程就代表一个运行中的程序(这并非是进程的概念,只是形象化的理解)

前提:计算机中,cpu就是专门用来做运算,一切运算都是由cpu,而且cpu是计算机当中最宝贵的资源,事实上我们是把cpu的用来计算的时间当作是一种资源。

1.为什么会有多进程?

只有一个cpu,单核情况下。

  1. 单道批处理:

    • 单道:在整个操作系统当中,同一时间内内存中只有一个程序运行,程序的运行只能是上一个程序运行完才开始运行下一个程序。

    • 批处理:程序运行过程中,不会有任何响应,一次执行完毕。

    • 单道批处理操作系统,它并不能很好的利用cpu的计算时间。

      假设,在单道批处理系统中运行了一个程序,在他的程序中需要执行IO(例如和打印机传输数据),在IO的数据传输过程中,有很大一部分时间是不会用到cpu的计算功能的,此时cpu闲置。

  2. 多道批处理操作系统

    • 多道:在操作系统中,内存中同时可以有多个应用程序在运行,这样一来一旦某个程序不需要使用cpu的计算功能,操作系统就会把cpu的计算时间分配给内存中其他的应用程序在运算,这样一来大大提高了cpu的利用效率。

      应用程序的执行:在单核情况下,一个cpu在同一时间点只能为一个应用程序服务。当程序运行,占用cpu执行时间,这个应用才算真正的执行。在多道批处理系统中,多个应用程序交替执行,看起来好像在“同时”运行。

      核心原因:进程的交替执行,交替过程是需要付出额外代价的——进程的上下文切换。

      上下文切换

      程序的运行数据在运行时要放到cpu的寄存器里,供cpu的运算器来运算,对于单进程来说,多个程序交替运行的过程中,为了保证程序之间的运行数据不相互干扰。

      1. 在程序切走的时候,该进程失去了cpu的使用权,在被其他进程占用cpu之前,要利用cpu功能,将当前这一刻计算机等相关状态保存起来。
      2. 当该进程重新获取cpu执行权,开始执行之前利用cpu的功能以及刚刚保存起来的上下文,把计算机恢复到上次切走时的程序执行状态。

      上下文切换是一个比较耗时的工作,而且需要使用到cpu(cpu虽然不闲了,但是被加大了无关于程序本身运行的工作量,这就是额外的代价)。

  3. 现代操作系统

    引入了另外一个东西:线程。

    • 线程,又被称为轻量级进程,一个进程可以有多个线程。同一个进程的多个线程中,线程的上下文切换的时候付出的额外代价小得多——进一步提高cpu的利用率。

      以线程为单位占用cpu,同一个进程的多个线程,共享一个进程的数据。

  • 并发:并发执行就是交替执行,一段时间内多个程序“同时”运行。(其实是交替执行)
  • 并行:并行执行多cpu,同一个时间点,多个程序同时运行。(真正的同时运行,要求硬件需要多个cpu,多核操作系统)。

通常生活中所说的同时,指的是并行。

结论:java语言当中利用多线程,实现的“并发”concurrent的“同时”效果。

2.两个问题

1.Java命令运行一个Java程序的过程?

java 主类类名(命令行打入Java 类名,运行这个java程序)

  1. 其实java命令,它启动了一个jvm进程(运行了一个程序)。
  2. 该jvm进程,在执行的时候,首先会创建一个线程,main线程。
  3. 在main线程中,运行主类中的main方法代码。

2.JVM是单线程还是多线程?

jvm是多线程的,其实至少还应该有另外一个垃圾回收器线程在执行。

例如:当堆上的某个对象没有引用变量指向它的时候,该对象就变成垃圾,被同时运行的垃圾回收器线程回收,保障堆上的内存不会被用完。

(二)多线程01

1.Thread实现方式一
  1. 继承Thread,定义Thread的子类
  2. 重写子类的run方法
  3. 创建该子类的对象
  4. 启动线程(对象)start()

在这里插入图片描述

注意事项
  1. 一个Thread类(Thread子类)对象代表一个线程。

  2. 为什么我们重写Thread类中的run方法?

    只有Thread run()方法中的代码,才会执行在子线程中。为了保证子线程中运行的是我们想要在子线程中运行的代码,必须重写run()方法。

  3. 但是,如果想要让代码,在子线程中运行,并非一定,代码要写在run方法方法体中。

    对于定义在该Thread子类中,其他方法方法体中的代码,也可以运行在子线程中。换句话说,一个方法,被哪个线程中的代码调用,被调用的方法,就运行在调用它的线程中。

  4. 启动线程,必须使用start()方法来启动,这样才能使得Thread中的run方法运行在子线程中。
    如果通过调用run方法,来执行Thread的run方法代码,这仅仅只是普通的方法调用。

  5. 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次。如果,我们要启动多个线程,只能创建多个线程对象,并启动这些线程对象。

2.线程调度

线程调度是指系统为线程分配处理器使用权的过程。

调度方式

假设在单CPU的情况下线程的两种主要调度模型:

  1. 协同式线程调度(Cooperative Thread-Scheduling)

    如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制。当他把自己的工作完成之后,会主动通知系统切换到另外一个线程上去。

    好处:实现简单。

    缺点:线程执行时间不可控,不稳定

  2. 抢占式调度(Preemptive Thread-Scheduling)

    如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。

    好处:现成的执行时间是可控的,根据线程的优先级来控制线程的切换以及执行时间。线程的优先级是动态可变的(随等待时常增长而增加,随执行时长而降低)。

Java使用哪种调度模型

抢占式调度。

Java中的优先级之所以会不太可靠:

  1. 不同操作系统的优先级机制不同,比如Solaris中有2147483648种优先级,但windows就只有7种优先级
  2. 可能会被系统改变,比如windows中存在一个称为“优先级推进器(Priority Boosting,当然他可以被关闭掉)的功能,他的大致作用,就是当他发现一个线程执行的特别勤奋的时候,可能会越过线程的优先级,去为他分配执行时间”。
3.Thread类API
Thread类成员方法
  1. 获取和设置线程名称
    • public final String getName()

    • public final void setName(String name)

    • public static Thread currentThread()//返回当前正在执行的线程对象的引用。该方法在哪个线程中被调用,那个线程就是该方法的当前线程。

      思考,如何获取main线程的名称呢?

      只要能拿到main方法所在线程的线程对象即可。在主方法中调用用currentThread()方法。

  2. 多线程的优先级
    • public final int getPriority()//获取线程优先级(默认值为5)
    • public final void setPriority(int priority)//修改线程优先级
      在这里插入图片描述

    注意事项:

    • 多线程的优先级的取值范围1 <= priority <=10

      线程的默认优先级为5

    • 然而给线程对象设置优先级并没有什么用。

      事实上我们在Java语言中设置的线程优先级,他仅仅只能被看成是一种“建议”(给操作系统的建议)。实际上操作系统本身有它自己的一套优先级(静态优先级+动态优先级)。

      java官方:线程优先级并非完全没有用,我们Thread的优先级它具有统计意义,总的来说高优先级的线程占用cpu的执行时间多一点,低优先级的线程占用cpu的时间少一点。

    • 结论:千万不要试图通过设置线程的优先级控制线程的执行的先后顺序

    多线程带来了并发,但是同时多线程也给我们的程序带来了不确定性。

  3. 线程控制API
    • public static native void sleep (long millis)//在指定的毫秒数内让正在执行的线程休眠(暂时暂停)。指定的休眠时间以毫秒为单位。

      被native修饰的方法称之为本地方法,本地方法都不是由java语言实现的。

    • public final void join()//等待该线程终止。

      线程对象.join(),”该线程“:在哪个线程对象上调用join方法,该线程指的就是哪个线程对象所表示的线程。

      谁等待?当前线程等待,join线程在哪个线程中被调用,哪个线程就等待。

      等待谁?该线程,在哪个线程对象上调用join方法,等待的就是哪个对象的线程。
      主线程等joinThread线程执行完毕再继续执行。

在这里插入图片描述

  • public static void yield()//(礼让方法)

    1. 暂停当前正在执行的线程对象(自己放弃cpu的使用权,yield方法可以实现)

    2. 执行其他线程。(yield方法不一定实现这个功能)

      线程放弃cpu的使用权之后就会进入等待状态,此时cpu会在众多等待线程中再选择一个,不排除选到刚才自己放弃使用权的线程。

      不确定性导致实际开发中也不会使用yield方法来进行线程调度。(Demo中可能会看到。)

  • public final void setDaemon(boolean on)//将该线程标记为守护线程(参数为true时)或用户线程(参数为false时)(我们创建出来的普通线程)。

    运行特征:

    1. 当正在运行的线程都是守护线程,jvm直接退出(结束执行)。

      使用场景:垃圾回收器实际上就运行在守护线程中。

    2. 该方法必须在线程启动前调用。

  • public void interrupt()//中断线程(打断线程的阻塞状态)

    如果该线程在调用wait方法或join方法或sleep方法过程中受阻,线程将收到一个InterruptedException。
    在这里插入图片描述

    interrupt方法可以打断线程的休眠或者等待,跳转到catch分支,这意味着这个子线程中try块中的功能被异常终止了。

    实际在应用层很少去使用,但是不是不用(比较底层的代码例如线程池会用到interrupt方法)。

4.线程的生命周期
  1. 新建:线程处于刚刚创建的状态。start方法。

  2. 就绪:有执行资格,等待cpu调度获得执行权。万事俱备只欠cpu,除了cpu资源,之外的程序执行条件都已经满足了。

  3. 运行:取得执行权,正在cpu上执行。优先级降低到某种程度后,操作系统会剥夺线程的cpu资源,调度其他优先级较高的就绪态线程,此时若上一个线程还没运行完,他又会回到就绪态。

  4. 阻塞:无执行资格,无执行权。处于执行态的线程如果调用了sleep(),join(),或者执行IO,就会处于等在状态,此时不该让他继续占用cpu,处于阻塞状态。

    除了缺少cpu资源之外,还需要满足其他未满足的线程执行条件。当执行条件重新满足之后,就会再次转回就绪态,等待下一次被调度。

  5. 死亡:线程正常终止(run()方法执行完毕)或异常终止,线程对象成为垃圾,等待垃圾回收器回收。
    在这里插入图片描述

5.线程的实现方式二
  1. 定义实现Runnable接口的子类。

  2. 实现Runnable接口的run方法。
    在这里插入图片描述

  3. 创建该子类对象

  4. 在创建Thread对象的时候,将创建好的Runnable子类对象作为初始化参数,传递给Thread对象。
    在这里插入图片描述

    Thread并不是一个抽象类,我们是可以直接new Thread类的对象的。

  5. 启动Thread对象(启动线程),Thread.start();

注意事项:

  1. 我们的Runnable接口子类的run方法中的代码,会运行在子线程当中。
  2. 所以在线程的第二种实现方式当中,我们自己来定义子类,实现Runnable接口的run方法,将要在子线程中执行的代码在run方法中。
  3. 但是runnable子类对象并不代表一个线程,他只代表线程中执行的任务。

逻辑上说,第二种实现方法逻辑更加清晰。

  1. 线程就是一条执行路径,至于在线程这条执行路径上,究竟执行什么样的具体代码应该是和线程本身没有关系的。
  2. 也就是说线程和在线程(执行路径)上执行的任务应该是没有直接关系的。
  3. 线程的第二种实现方式,把线程(Thread对象代表线程)和在线程上执行的任务(Runnable接口子类的run方法)
    在这里插入图片描述

Runnable接口子类对象的run方法运行在Thread接口中的原因图解。

实现方式一VS实现方式二
  • 方式一比方式二少了一步。

  • 方式一的实现方式存在一种单重继承的局限性。
    在这里插入图片描述

    复用已有代码并且让其运行在子线程中,做不到。
    在这里插入图片描述

  • 方式二将线程和任务解耦。

  • 方式二便于多线程的数据的共享。

练习

多线程仿真如下场景:

假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。请设计程序模拟窗口售票的场景。

分析:

  • 3个窗口售票,互不影响,同时进行。
  • 3个窗口共同出售这100张电影票
//Ver1.0,实现方式一
class SalesWindow extends Thread {
   int tickets = 100;
    @Override
    //售票
    public void run() {
        //当票没卖完,就一直卖票
        while (tickets > 0) {
            System.out.println(getName()+"售出了第"+(this.tickets--)+"张票");
        }
    }
}

三个窗口各买各的,每个窗口卖一百张,总共卖了三百张,功能实现失败。若要实现数据共享可以在tickets前加static修饰,使所有对象共享它。

//Ver2.0,实现方式二
class SalesTask implements Runnable {
    int tickets = 100;
    @Override
    public void run() {
        //当票没卖完,就一直卖票
        while (tickets > 0) {          
            System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
        }
    }
}

三个线程都使用一个功能,实现了数据共享。

在不考虑其他因素的前提下,通过多线程程序模拟上述的售场景,基本没啥问题。

但我们的程序其实还可以做的更真一些,因为实际情况下,售票数据通过网络传输,总是存在一些延迟的情况。
所以在真正售出一张票后,需要一段时间,才可以真正去修改剩余票数。
因此,我们可以在程序中,增加对于售票延迟的模拟,即每次卖票延迟100ms,后再去修改剩余票数

class SalesTaskVer2 implements Runnable {
    private int tickets = 100;
    @Override
    public void run() {
        //当票没卖完,就一直卖票
        while (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }            System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
        }
    }
}

当增加售票延迟之久,出现了一些明显的错误。

  1. 相同的票,被卖出了多次。(多卖问题)
    在这里插入图片描述
  2. 卖出了不存在的票。(超卖问题)
    在这里插入图片描述

其实不管是超卖还是多卖问题,都属于典型的多线程的数据安全问题。

6.数据安全

在多线程运行环境下,在多个线程访问线程之间共享的数据时,访问到了错误的共享数据的值。不管是超卖还是多卖问题,都属于典型的多线程的数据安全问题。

产生原因
  1. 多线程运行环境。

    需求决定,无法打破

  2. 数据共享。

    需求决定,无法打破

  3. 共享数据的非原子操作。

    原子操作:就是一组不可分割的操作。这组操作要么一次全部完成,要么一步都不做。当某个线程在进行原子操作的过程中产生了线程的切换,其他线程访问了这个时刻的中间状态,这可能会导致多线程共享数据错乱。

    可以打破

    解决多线程数据安全问题一一>如何将多线程中对共享变量一组的操作变成原子操作。

解决方案

打破以上三个产生原因之一即可。

两种思路构造原子操作(一个线程对共享数据的访问一次完成)

  1. 在一次原子操作过程当中阻止线程切换。思路没问题,但我们做不到,这是涉及到线程调度,在抢占式线程调度当中,线程调度是系统决定的。
  2. 线程切换无法阻止,给共享变量加锁,保证:
    • 只有加锁的线程,能访问到共享变量。
    • 在加锁线程没有完成对共享变量的一组操作之前,不会释放锁。
    • 只要不释放锁,其他线程即使被调度执行也无法访问共享变量。
7.同步代码块synchronized
synchronized(锁对象){
//一组要作为原子操作的代码(需要访问共享数据)
}
  • synchronized代码块中的锁对象可以是java语言中的任意一个对象(仅限于synchronized代码块语句中)。

    1. 因为java中所有的对象内部都有一个标志位,表示加锁和解锁的状态。
    2. 所以其实锁对象就充当着锁的角色,所谓的加锁和解锁其实就是设置锁对象的标志位,来表示加锁解锁的状态。
  • 我们的代码都是在某一条执行路径(某一个线程)中运行,当某个线程执行到synchronized代码块时,会尝试在当前线程中对锁对象加锁。

    1. 此时如果锁对象处于未加锁状态,jvm就会设置锁对象标志为(加锁),并且在锁对象中记录是哪个线程加的锁,然后让枷锁成功的当前线程执行同步代码块中的代码。
    2. 此时如果锁对象已经被枷锁而且加锁线程不是当前线程,系统会让当前线程处于阻塞状态(等待),直到加锁线程执行完了对共享变量的一组操作并释放锁。
  • 加锁线程何时释放锁?

    当枷锁线程执行完了同步代码块中的代码(对共享变量的一组操作),在推出同步代码块之前jvm会自动清理锁对象的标志位,将锁对象变成未上锁状态(释放锁)。

class SalesTaskVer3 implements Runnable {
    private int tickets = 100;
    Object lockObj = new Object();//锁对象
    @Override
    public void run() {
        //当票没卖完,就一直卖票
        while (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockObj) {
                //double check
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
                }
            }
        }
    }
}

千万要注意:

  • 虽然synchronized代码块中的锁对象可以是java语言中的任意对象。
  • 但是在多线程运行环境下,想要让访问同一个共享变量的多个synchronized代码块中的代码是原子操作,那么对同一个共享变量的访问必须使用同一个锁对象!
同步异步

解决多线程数据安全问题是通过加锁,构造一个线程对共享变量的原子操作。但其实加锁其实就是在完成线程同步

  • 同步:我走你不走,你走我不走。所有的加锁失败的线程步调变得一致了,都需要等待锁对象被释放。
  • 异步:你走你的我走我的,多线程天生异步,不同的线程相互独立不受影响,各自按照各自的执行步调,执行各自的代码。

所以,最终,其实我们是用线程同步来解决线程数据安全问题的。

线程同步优缺点:

  • 优点:解决了多线程数据安全问题
  • 缺点:相比于异步,因为等待锁资源而引发的阻塞,降低了程序运行的效率。
同步方法

整个方法就是一个同步代码块,其效果等价于同步代码块。同步方法的锁对象是this.当前对象(锁对象是隐式给出的)

class SalesTasknew implements Runnable {
    private int tickets = 100;
    Object lockObj = new Object();

    @Override
    public void run() {
        //当票没卖完,就一直卖票
        while (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.salesingLock();
        }
    }
    private synchronized void salesingLock() {
        if (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
        }
    }
}

静态方法也可以是同步方法。静态方法通过类名.静态方法()调用。静态方法依赖于类而存在,而jvm中一个类对应一个class对象,所以一个静态方法锁对象就是表示静态方法所属类的Class对象(隐式给出的)。

(三)多线程02

1.Lock锁(线程锁)

synchronized关键字虽然可以理解加锁,解锁的原理,但是却看不到具体的过程。实现同步代码块,除了使用synchronized之外,其实JDK1.5之后,提供了另外的方式Lock锁机制。

利用线程同步,解决多线程的数据安全问题,有两种方式:

  1. synchronized+java中任意对象。
  2. Lock锁对象。
Lock锁对象VSsynchronized锁对象
  1. synchronized锁对象只提供了用来模拟锁状态的标志位(加锁和释放锁),但是加锁和释放锁都是由jvm隐式完成的,和锁对象本身无关,所以synchronized锁对象不是一把完整的锁。

  2. 一个Lock对象,就代表一把锁,而且还是一把完整的锁,Lock对象,它如果要实现加锁和释放锁,不需要synchronized关键字配合,它自己就可以完成。

    Lock:loch()加锁;unlock释放锁。

  3. 两种锁对象,实现方式完全不同

    class SalesTasknew implements Runnable {
        private int tickets = 100;
        //常用子类:ReentrantLock
        Lock lock = new ReentrantLock();
        @Override
        public void run() {
            //当票没卖完,就一直卖票
            while (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //this.salesingLock();
                lock.lock();
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + "售出了第" + (tickets--) + "张票");
                }
                lock.unlock();
            }
        }
    }
    

    联系:都可以实现线程同步

    1. synchronized(锁对象){需要同步的代码}

    2. lock.lock()

      需要同步的代码

      lock.unlock()

它们分别什么情况下使用

推荐使用synchronized+java对象完成线程同步。

  1. 两种方式实现的效果是相同的,而synchronized+java对象更简单易用。
  2. jdk1.5/1.6等早期版本中,Lock锁效率高一些,而现在从效率角度来讲,使用synchronized和使用Lock对象,效率几乎完全相同。
Lock锁比较标准的用法
Lock l=......;
l.lock();
try{
    //
}finally{
    //不管正常还是异常都可以释放这把锁
    l.unlock();
}

在这里插入图片描述

死锁问题

同步另一个弊端:如果出现了嵌套锁,可能产生死锁。

某个线程要同时处有两把锁lockA和lockB,换个说法,这个线程在成功持有lockA锁的情况下,再持有lockB锁。

synchronized(lockA){
//当某线程的代码执行到这里
	synchronized(lockA){
//执行到这里,在成功持有lockA锁的情况下,再持有lockB锁,此时当前线程就同时持有两把锁
	}
}

案例:
在这里插入图片描述

public class Exnew02 {
    public final static Object lockA = new Object();
    public final static Object lockB = new Object();
}  
class ABThread extends Thread {//线程一
    @Override
    public void run() {
        synchronized (Exnew02.lockA) {
            //访问共享变量,计算,得到一些中间结果
            System.out.println("ABThread,A锁");//输出了
            synchronized (Exnew02.lockB) {
                //访问打印机,打印中间结果
                System.out.println("ABThread,B锁");
            }
        }
    }
}
class BAThread extends Thread {//线程二
    @Override
    public void run() {
        //尝试访问打印机
        synchronized (Exnew02.lockB) {
            //获得打印机访问权
            System.out.println("BAThread,B锁");//输出了
            synchronized (Exnew02.lockA) {
                //再获取共享变量访问权,进行计算
                System.out.println("BAThread,A锁");
            }
        }
    }
}
死锁问题

死锁是指两个以上的线程在执行过程中,因为争夺资源而产生的一种相互等待的现象。

解决方式:

  1. 调整线程获取多把锁的顺序,将多个线程中获取锁的顺序换成一样的。

  2. 要么一个线程同时持有所需要的多把锁,要么一把锁都不加。实质就是把加多把锁作为一个原子操作。

    再定义一把新锁,利用这把锁(synchronized+该对象),实现将加多把锁的操作变成一个原子操作。

生产者消费者模型

在这里插入图片描述

多个生产者和多个消费者各自都是以异步的方式运行
但是在某些情况下,生产者和消费者之间必须保持协作:

  • 当缓冲区空的时候,不允许消费者到缓冲区中取数据。
  • 当缓冲区满的时候,不允许生产者向缓冲区中放入数据。
  • 同时缓冲区中的一个单元,只能放入一个产品。
  • 同时还要注意,因为生产者和消费者都是异步的,但是它们都共享缓冲区!!
线程间通信

Java中主要通过Object中的方法来实现:

  • public final void wait()//阻止自己,在其他线程调用此对象的notify方法或notifyAll方法前导致当前线程等待。

    • wait方法的阻塞条件:

      在当前线程中,如果在对象上调用wait的方法,就会导致当前线程阻塞。

    • wait方法的的唤醒条件:

      首先有一种说法,当某线程因为在某对象上调用了wait方法,处于阻塞状态,我们就说该线程在该对象上阻塞。

      如果要唤醒在某对象上阻塞的线程,就必须在其他线程中,在同一(线程阻塞的)对象上调用notify或notifyAll方法。

    • wait方法的使用条件:

      当前线程必须拥有此对象监视器。当前线程持有该锁对象(把该对象当成锁对象,并让当前线程加锁)。

      当某线程成功对某把锁加锁,我们就说某线程持有这把锁。
      在这里插入图片描述

    • wait方法执行特征(锁对象的角度):

      该线程释放(release)对此监视器的所有权并等待。释放锁对象的持有。
      在这里插入图片描述
      理解:wait方法只能被一个线程的锁对象调用。当调用wait方法后,当前线程先释放锁,进入阻塞状态等待下次被调用。当有其他线程中的notify或者notifyAll被调用,通知这个被阻塞的线程,这个线程就会进入就绪态等待cpu的调度,被调度后先再次加锁,然后接着上次执行被切走的地方继续执行。

  • notify()//通知别人

    唤醒在此对象监视器(锁)上等待的线程;如果有多个线程都在此对象上等待,则选择一个线程,选择是随机的并在对实现做出决定时发生。

  • notifyAll()//通知别人

    唤醒在此对象监视器上阻塞的所有线程。

/第二版蒸笼,智能蒸笼,完成功能
//线程同步:利用同步方法,都在同一个container上调用同步方法
//线程通信(协作)wait,notifyAll
class Container {
    private Food food;
    //消费者用
    public synchronized void eatFood() {
        if (food == null) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + "吃了" + food);
            food = null;
            this.notifyAll();
        }
    }
    //生产者用
    public synchronized void setFood(Food newfood) {
        if (food == null) {
            //newfood是PDTask类里的新做的食物,把它放到蒸笼里
            food = newfood;
            System.out.println(Thread.currentThread().getName() + "做了" + food);
            this.notifyAll();
        } else {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Food {
    private double price;
    private String name;

    public Food(double price, String name) {
        this.price = price;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Food{" +
                "price=" + price +
                ", name='" + name + '\'' +
                '}';
    }
}
class ConsumerTask implements Runnable {
    private Container container;

    public ConsumerTask(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        while (true) {
            container.eatFood();
        }
    }
}
class PDTask implements Runnable {
    private Container container;
    public PDTask(Container container) {
        this.container = container;
    }
    private Food[] foodMenue = {new Food(30, "蟹黄包"), new Food(3, "豆沙包"),
            new Food(8, "小笼包"), new Food(6, "牛肉包")};
    @Override
    public void run() {
        while (true) {
            container.setFood(foodMenue[new Random().nextInt(foodMenue.length)]);
        }
    }
}

当有多个生产者消费者的时候,一定要用notifyAll通知别人。
在这里插入图片描述

面试题

Thread.sleep() VS Object.wait()

Thread.sleep()Object.wait()
所属不同sleep是定义在Thread类中的静态方法wait方法是定义在Object类中的非静态方法
唤醒条件不同休眠时间到在其他线程中的同一个所对象上调用了notify或notifyAll方法
使用条件不同没有任何前提条件必须在当前线程的锁对象上调用wait()
休眠时对锁的持有状态不同(核心区别)线程因为sleep而处于阻塞状态时,阻塞的时候不会放弃对锁的持有wait方法在阻塞的时候放弃对锁的持有
在这里插入图片描述

(四)线程池

概念

线程池就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了侵犯创建线程对象的操作,无需反复创建线程而消耗过多的资源。

线程池容器——集合:ArraryList,HashSet,LinkedList(推荐),HashMap

底层原理

  1. 当程序第一次启动的时候,创建多个线程到集合中。
  2. 当我们需要使用线程的时候,就可以从集合中拿线程出来使用。
  3. 当我们使用完毕线程,需要把线程归还给线程池。
    在这里插入图片描述

JDK1.5之后,JDK中内置了线程池可以直接使用。

线程池优点:

  1. 降低资源消耗,减少了创建和销毁线程的次数,每个线程都可以被重复利用,可执行多个任务。
  2. 提高了响应速度,当任务到达时,任务可以不需要等到线程创建就能立刻执行。
  3. 提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作现成的数目,防止因为消耗过多的内存而把服务器累趴下。(每个线程大约需要1MB内存,线程开得越多,消耗的内存就越大,最后死机)

线程池:JDK1.5之后提供的。

java.util.concurrent.Executors:线程池的工厂类,用来生成线程池。

Executors类中的静态方法

static ExercutorService newFixedThreadPool(int nThreads)//创建一个可以复用固定数量线程的线程池。

  • 参数:Int nThreads:创建线程池中包含的线程数量。
  • 返回值:ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)

ExecutorService:线程池接口

用来从线程池中获取线程,调用start方法,执行线程任务。

submit(Runnable task)提交一个Runnable任务用于执行。

关闭和销毁线程池的方法:void shutdown()

线程池的使用步骤:

  1. 使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池。
  2. 创建一个类,实现Runnable接口,设置线程任务。
  3. 调用ExecutorService中的submit方法,传递线程任务(实现类),开启线程,执行run方法。
  4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LxyrichBos

老板~坐下喝杯茶啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值