多 线 程

多线程

1.并发与并行

  • 并行:指两个或多个事件在同一时刻发生(同时执行)。
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。

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

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

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

2.线程与进程

  • 进程:进程是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;
    • 进程:其实就是应用程序的可执行单元(.exe文件)
    • 每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;
  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
    • 线程:其实就是进程的可执行单元
    • 每条线程都有独立的内存空间,一个进程可以同时运行多个线程;
  • 多线程并行: 多条线程在同一时刻同时执行
  • 多线程并发:多条线程在同一时间段交替执行
  • 在java中线程的调度是:抢占式调度
  • 在java中只有多线程并发,没有多线程并行(高并发)

进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

**注意:**下面内容为了解知识点

1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程调度:

  • 分时调度

    ​ 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    ​ 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

3.Thread类

  • 表示线程,也叫做线程类,创建该类的对象,就是创建线程对象(或者说创建线程)
  • 线程的任务: 执行一段代码
  • Runnable : 接口,线程任务接口
构造方法
方法含义
public Thread()分配一个新的线程对象,线程名称是默认生成的。
public Thread(String name)分配一个指定名字的新的线程对象。
public Thread(Runnable target)`分配一个带有指定目标新的线程对象,线程名称是默认生成的。
public Thread(Runnable target,String name)`分配一个带有指定目标新的线程对象并指定名字。

创建线程的方式有2种:

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

4.创建线程的方式

  1. 继承方式

    Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
    2. 创建Thread子类的实例,即创建了线程对象
    3. 调用线程对象的start()方法来启动该线程
    public class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("子线程 第"+(i+1)+"次循环");
            }
        }
    }
    
  2. 实现方式

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

    步骤如下:

    1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
    3. 调用线程对象的start()方法来启动线程。
    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 线程需要执行的任务代码
            for (int i = 0; i < 100; i++) {
                System.out.println("子线程 第"+(i+1)+"次循环");
            }
        }
    }
    
    public class Test {
        pub
            MyRunnable mr = new MyRunnable();
            //创建Thread线程对象,并传入Runnable接口的实现类对象
            Thread t1 = new Thread(mr);
            //调用start()方法启动线程,执行任务
            t1.start();
        }
    }
    

    通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

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

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

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

  3. 匿名内部类方式

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

    使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

    Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 线程需要执行的任务代码
                    for (int i = 0; i < 100; i++) {
                        System.out.println("子线程 第"+(i+1)+"次循环");
                    }
                }
            });
    
            // 调用start()方法启动线程,执行任务
            t.start();
    

5.Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

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

  1. 可以避免java中的单继承的局限性。
  2. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  3. 适合多个相同的程序代码的线程去共享同一个资源。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

6. 线程安全问题

通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。

售票窗口: 使用线程来模拟

共同卖100张票

窗口卖票的任务是一样的(线程的任务代码是一样的)

模拟票:

package com.itheima.demo1_线程安全问题;

/**
 * @Author:pengzhilin
 * @Date: 2020/9/17 8:47
 */
public class MyRunnable implements Runnable {
    // 共享变量
    int tickets = 100;

    @Override
    public void run() {
        // 线程的任务代码---卖票
        while (true) {
            if (tickets < 1) {
                break;
            }
            // 暂停100ms模拟收钱的操作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":正在出售第" + tickets + "张票");
            tickets--;
        }
    }
}

测试类:

package com.itheima.demo1_线程安全问题;

/**
 * @Author:pengzhilin
 * @Date: 2020/9/17 8:46
 */
public class Test {
    public static void main(String[] args) {
        /*
            需求: 模拟电影院4个窗口卖100张电影票
            分析:
                售票窗口: 使用线程来模拟
                4个窗口共同卖100张票
                4个窗口卖票的任务是一样的(线程的任务代码是一样的)
            问题:
                1.重复票
                2.遗漏票
                3.负数票(最多到-2)
         */
        // 创建任务对象
        MyRunnable mr = new MyRunnable();

        // 创建4个窗口---创建4条线程
        Thread t1 = new Thread(mr, "窗口1");
        Thread t2 = new Thread(mr, "窗口2");
        Thread t3 = new Thread(mr, "窗口3");
        Thread t4 = new Thread(mr, "窗口4");

        // 启动线程,执行任务
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

程序执行后,结果会出现的问题
在这里插入图片描述

7.synchronized

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式:
    a).同步代码块【常用】

    b).同步方法【常用】

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

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

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
7.1同步代码块
  • synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
     需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

7.2同步方法
  • 使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
修饰符 synchronized 返回值类型 方法名(形参列表){
   	可能会产生线程安全问题的代码
}

同步锁是谁?

对于非static方法,同步锁就是this。

对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

8.Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更加面向对象

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。

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

	Lock lock = new ReentrantLock();

	@Override
    public void run() {
        // 加锁
        lock.lock();
        
   		可能会产生线程安全问题的代码
        // 释放锁
        lock.unlock();
	}

9.高并发及线程安全

  • 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
  • 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。

10.多线程的运行机制

当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。
在这里插入图片描述
多个线程在各自栈区中独立、无序的运行,当访问一些代码,或者同一个变量时,就可能会产生一些问题

11.多线程的安全性问题

  1. 可见性

    一个线程没有看见另一个线程对共享变量的修改

    原因:

    • Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

    • 简而言之: 就是所有共享变量都是存在主内存中的,线程在执行的时候,有单独的工作栈内存,会把共享变量拷贝一份到线程的单独工作内存中,并且对变量所有的操作,都是在单独的工作内存中完成的,不会直接读写主内存中的变量值

  2. 有序性
    • 有些时候“编译器”在编译代码时,会对代码进行“重排”

    • 但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响

  3. 原子性

    概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。

    原因:两个线程对共享变量的操作产生覆盖的效果

12.volatile关键字

  • volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
  • volatile能解决变量的可见性、有序性;
  • volatile不能解决变量的原子性
  1. volatile解决可见性

    当变量被修饰为volatile时,会迫使线程每次使用此变量,都会去主内存获取,保证其可见性

  2. volatile解决有序性

    当变量被修饰为volatile时,会禁止代码重排。

  3. volatile不能解决原子性

    volatile关键字只能解决"变量"的可见性、有序性问题,并不能解决原子性问题**

13.原子类

在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:

​ 1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;

​ 2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;

​ 3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;

它们可以保证对“变量”操作的:原子性、有序性、可见性。

AtomicInteger、AtomicIntegerArray等

工作原理: CAS机制
在这里插入图片描述
在这里插入图片描述

14.并发包

CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、Hashtable、是线程安全的,但效率低
  • ConcurrentHashMap高效的原因:CAS + 局部(synchronized)锁定

  • HashTable效率低下原因:

public synchronized V put(K key, V value) 
public synchronized V get(Object key)

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

14.1CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。

CountDownLatch构造方法:

public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象

CountDownLatch重要方法:

public void await() throws InterruptedException// 让当前线程等待
public void countDown()	// 计数器进行减1
    
例如:线程1要执行打印:A和C,线程2要执行打印:B,但要求线程1打印C之前,一定要打印B
            分析:
                线程1:
                    任务:
                        打印A
                        调用await()方法进入等待(线程2打印B)
                        打印C
                线程2:
                    任务:
                        打印B
                        调用countDown()方法让计数器-1

                注意:
                    1.创建的CountDownLatch对象的计数器初始值为1
                    2.线程1和线程2使用的CountDownLatch对象要一致

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

14.2CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier构造方法:

public CyclicBarrier(int parties, Runnable barrierAction
    //parties: 代表要达到屏障的线程数量
    //barrierAction:表示达到屏障后要执行的线程

CyclicBarrier重要方法:

public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
  • 使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
  • 需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。
14.3 Semaphore

emaphore的主要作用是控制线程的并发数量。

synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

Semaphore构造方法:

public Semaphore(int permits)						permits 表示许可线程的数量

Semaphore重要方法:

public void acquire() throws InterruptedException	表示获取许可
public void release()								release() 表示释放许可
14.4 Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

A线程 exchange方法 把数据传递B线程

B线程 exchange方法 把数据传递A线程

Exchanger构造方法:

public Exchanger()

Exchanger重要方法:

public V exchange(V x)
  • 使用场景:可以做数据校对工作
  • 需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,并对两个文件数据进行校对,看看是否录入一致

15.线程池

  • 线程池: 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
  • 线程池的工作原理:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WUDwmxJf-1624449870506)(F:/Java/02阶段_java语言进阶级/02阶段_java语言进价课件/day10-线程安全、volatile关键字、原子性、并发包、死锁、线程池/01_笔记/img/线程池原理.bmp)]
线程池的好处
  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
小结
  • 线程池的原理: 创建线程池的时候初始化指定数量的线程,当有任务需要线程执行的时候,就在线程池中随机分配空闲线程来执行当前的任务;如果线程池中没有空闲的线程,那么该任务就进入任务队列中进行等待,等待其他线程空闲下来,再执行任务.(线程重复利用)
线程池的使用:

ava里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行任务

  • public <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行任务

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

16.死锁

**死锁:**在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。

产生死锁的条件:

  1. 有多把锁
  2. 有多个线程
  3. 有同步代码块嵌套

死锁代码:

 // 线程1: 锁A,锁B,执行
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("锁A"){
                    System.out.println("线程1:拿到锁A,准备拿锁B...");
                    synchronized ("锁B"){
                        System.out.println("线程1:拿到了锁A和锁B,开始执行");
                    }
                }
            }
        }, "线程1").start();


        // 线程2:锁B,锁A,执行
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("锁B"){
                    System.out.println("线程2:拿到锁B,准备拿锁A...");
                    synchronized ("锁A"){
                        System.out.println("线程2:拿到了锁B和锁A,开始执行");
                    }
                }
            }
        }, "线程2").start();

17.线程状态

线程由生到死的完整过程:技术素养和面试的要求。

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

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。创建线程对象时
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法)。调用start方法时
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。等待锁对象时
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。调用wait()方法时
Timed Waiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。调用sleep()方法时
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。run方法执行结束时
  • 无限等待:
    • 进入无限等待: 使用锁对象调用wait()方法
    • 唤醒无限等待线程: 其他线程使用锁对象调用notify()或者notifyAll()方法
    • 特点: 不会霸占cpu,也不会霸占锁对象(释放)
线程状态的切换

在这里插入图片描述

18.等待唤醒机制

等待唤醒机制是多个线程间的一种协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

就是在一个线程进行了规定操作后,就进入无限等待状态(wait()),调用notfiy()方法唤醒其他线程来执行,其他线程执行完后,进入无限等待,唤醒等待线程执行,依次类推… 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

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

  • 实现等待唤醒机制程序:
    • 必须使用锁对象调用wait方法,让当前线程进入无限等待状态
    • 必须使用锁对象调用notify\notifyAll方法唤醒等待线程
    • 调用wait\notfiy\notfiyAll方法的锁对象必须一致
  • 分析的等待唤醒机制程序:
    • 线程的调度依然是抢占式调度
    • 线程进入无限等待状态,就不会霸占cpu和锁对象(释放),也不会抢占cpu和锁对象
    • 如果是在同步锁中\Lock锁中,调用**sleep()**方法进入计时等待,不会释放cpu和锁对象(依然占用)
等待唤醒机制相关方法介绍
  • public void wait() : 让当前线程进入到无限等待状态 此方法必须锁对象调用.
  • public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值