线程间的通信和线程生命周期

一、概念

        当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

二、线程通信模型

        常见的线程常见模型:生产者与消费者模型。

        1、生产者负责生产数据

        2、消费者线程负责消费生产者生产的数据

        3、生产者生产完数据应该进行等待,并通知消费者进行消费;消费者消费完数据后也应该进行等待,并通知生产者再生产。

三、常用方法

        Object 类提供了等待和唤醒线程的方法,如下,需要注意的是,下面的 3 个方法应该使用当前同步锁对象进行调用。

3.1 wait 方法

3.1.1 作用

        wait() Object 类的方法,使当前执行代码的线程进行等待,该方法用来将当前线程置入“预执行队列”中,并且在 wait() 所在的代码行处进行停止执行,直到接到通知或被中断为止。在调用 wait() 之前,线程必须获得该对象的对象级别锁,即只有在同步方法或同步代码块中调用 wait() 方法。在执行 wait() 方法后,当前线程释放锁。在从 wait() 返回前,线程与其他线程竞争重新获得锁。

3.2 notify 方法

3.2.1 作用

         notify() Object 类的方法,notify() 也要在同步方法或同步块中调用,即在调用前,线程也必须获取该对象的对象级别锁。该方法用来通知那么可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈 wait 状态的线程,对其发出通知 notify,并使它等待获取该对象的对象锁。

        需要说明的是,在执行完 notify() 方法后,当前线程并不会马上释放该对象锁,呈 wait 状态的线程也并不能马上获取该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,而呈 wait 状态所在的线程才可以获取该对象锁。

3.3 notifyAll 方法

3.3.1 作用

        notifyAll() 方法和 notify() 方法用法基本一致,只不过 notify() 唤醒唤醒正在等待的单个线程,而 notifyAll() 方法则是唤醒正在等待的所有线程。

3.1 join 方法

3.1.1 作用

        在很多的情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程之前结束。这时如果主线程想等待子线程执行完之后再结束,比如子线程处理一个数据,主线程想取得这个数据中的值,就要用到 join() 这个方法了。方法 join() 的作用是等待线程对象销毁。

       方法 join() 的作用是使所属的线程对象 正常执行 run() 方法中的任务,而使当前线程 进行无限期的阻塞,等待线程 销毁后再继续执行线程 后面的代码。

       方法 join() 具有使线程排队运行的作用,有些类似于同步的运行效果。

        方法 join() 与 synchronized 的区别是:join 在内部使用 wait() 方法进行等待,而 synchronized 关键字使用的是 “对象监视器” 原理作为同步。

3.1.2 用法

        接下来模拟一个主线程结束,子线程还没结束的场景,如下所示:

public class TestThread extends Thread{
    public TestThread(String name){
        super(name);
    }
    public void run(){
        try {
            int second = (int) (Math.random() * 10000);
            String name = Thread.currentThread().getName();
            Thread.sleep(second);
            System.out.println(name+"我睡眠了" + second + "秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        TestThread thread = new TestThread("thread1");
        thread.start();

        System.out.println("我想让子线程结束后我再执行");
        System.out.println("但是我不知道我要等多少秒");
    }
}

        使用 join() 方法可以使主线程等待子线程结束后再执行,如下所示:

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        TestThread thread = new TestThread();
        thread.start();
         thread.join();

        System.out.println("我想让子线程结束后我再执行");
        System.out.println("但是我不知道我要等多少秒");
    }
}

        还可以使用 join() 方法达到多个子线程顺序执行的效果,代码如下所示:

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        TestThread thread = new TestThread("thread1");
        thread.start();
        thread.join();

        TestThread thread2 = new TestThread("thread2");
        thread2.start();
        thread2.join();

        TestThread thread3 = new TestThread("thread3");
        thread3.start();
        thread3.join();

        System.out.println("我想让子线程结束后我再执行");
        System.out.println("但是我不知道我要等多少秒");
    }
}

3.1.3 join(long) 用法

        方法 join(long) 中的参数是设定等待的时间。

        演示代码如下所示:

public class MyThread extends Thread{

	public void run() {
		try {
			System.out.println("begin Timer = "+System.currentTimeMillis());
			Thread.sleep(5000);
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class Test {

	public static void main(String[] args) throws InterruptedException {
		MyThread myThread = new MyThread();
		myThread.start();
                //Thread.sleep(2000);
		myThread.join(2000);
		System.out.println("  end timer = " +System.currentTimeMillis());
	}
}

       输出结果如下,main线程只等待2秒,2秒执行不完就不再等待了。

       如果将 main 方法中的代码改成使用 Thread.sleep(2000) 方法时,运行的效果还是等待了 2 秒,那是用 join(2000) 和 sleep(2000) 有什么区别呢?在上面的示例中在运行效果上并没有什么区别,其实区别主要还是来自这两个方法对同步的处理上。 

       方法 join(long) 的内部使用 wait(long) 方法来实现的,所以 join(long) 具有释放锁的特点。 Thread.sleep(long) 方法却不释放锁。 

四、实现生产者和消费者

        假设有 3 个生产者线程,负责生产包子,每个线程每次只能生产 1 个包子放在桌子上。2 个消费者线程负责吃包子,每人每次只能从桌子上拿 1 个包子吃。

        核心代码的桌子类,代码如下:

// 线程通信的前提一定是保证线程安全,否则线程通信没有任何意义
// 锁是可以夸方法的,底下的 put 和 get 方法用的就是同一把锁(桌子对象),这个锁可以锁住底下的 5 个线程
// 每次只有一个线程可以抢到桌子对象,让一个线程访问 put 或者 get 方法,这样就控制了 5 个线程访问桌子对象是线程安全的
public class Desk {
    // 创建的五个线程都会抢同一个桌子对象
    private List<String> list = new ArrayList<>();

    // 放一个包子的方法
    // 厨师1、厨师、厨师3都会来竞争的做包子
    public synchronized void put() {
        try {
            String name = Thread.currentThread().getName();
            if(list.size() == 0){
                list.add(name+"做的肉包子");
                System.out.println(name+"做了一个肉包子");
                Thread.sleep(2000);

                // 唤醒别人,自己等待
                this.notifyAll();
                this.wait();
            }else{
                // 有包子,不做了
                this.notifyAll();
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    // 取一个包子的方法
    // 吃货1、吃货2都会来竞争的吃包子
    public synchronized  void get(){
        try {
            String name = Thread.currentThread().getName();
            if(list.size() ==1){
                // 有包子,吃了
                System.out.println(name+"吃了:"+list.get(0));
                list.clear();
                Thread.sleep(1000);
                this.notifyAll();
                this.wait();
            }else{
                // 没有包子
                this.notifyAll();
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

        测试代码如下:

public class ThreadTest {
    public static void main(String[] args) {
        Desk desk = new Desk();
        // 创建三个一直做包子的线程
        new Thread(() ->{
            while (true){
                desk.put();
            }
        },"厨师1").start();
        new Thread(() ->{
            while (true){
                desk.put();
            }
        },"厨师2").start();
        new Thread(() ->{
            while (true){
                desk.put();
            }
        },"厨师3").start();
        // 创建两个一直吃包子的线程
        new Thread(() ->{
            while (true){
                desk.get();
            }
        },"吃货1").start();
        new Thread(() ->{
            while (true){
                desk.get();
            }
        },"吃货2").start();
    }
}

五、进程

5.1 进程概念

        进程是正在运行的程序(软件)就是一个独立的进程。如下图所示:

        线程是属于进程的,一个进程中可以同时运行很多个线程。

        进程中的多个线程其实是并发和并行执行的。

5.2 并发的概念

        进程中的线程是由 CPU 负责调度执行的,但 CPU 能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU 会轮询为系统的每个线程服务,由于 CPU 切换的速度很快,给我们的感觉就是这些线程在同时执行,这就是并发。

        以单核的 CPU 为例,给班里的小朋友发糖,每人发 5 颗,如何保证每一个小朋友好像都在收糖呢?我可以并发的做,我可以先给第一个小朋友发一颗糖,再给第二个小朋友发一颗糖,由于我跑的速度非常快。就好像一个班的学生都在被我发糖,好像整个班的学生都在收糖一样,这就是并发。

        如果在发糖的时候暂停,那么此时就只有一个小朋友在收糖。

5.3 并行的概念         

         在同一时刻,同时有多个线程在被 CPU 调度执行。

        以我的电脑为例,当暂停的时刻,真的有 16 个线程在执行。

5.4 多线程到底是怎么在执行

        多线程是并发和并行同时进行的

        以我的电脑为例,如下图,逻辑处理器为 8 ,代表电脑可以同时处理 8 条线程,而我的电脑里面现在有 3158 个线程在运行,而我的电脑每次只能处理 8 条线程。那么我的电脑就会 8 个线程的切换,这一刻处理这 8 个线程,下一刻处理另外 8 个线程。

六、线程的生命周期

6.1 概念

        线程的声明周期也就是线程从生到死的过程,经历的各种状态以及状态转换。

6.2 java 的线程状态

        java 总共定义了 6 种状态,6 种状态都定义在 Thread 类的内部枚举类中,如下所示:

public class Thread{

	// ......
	public enum State {
	   
		NEW,
	  
		RUNNABLE,

		BLOCKED,

		WAITING,
	  
		TIMED_WAITING,

		TERMINATED;
	}
	// ......
}

线程状态说明
NEW(新建)线程刚被创建,但是并未启动
Runnable(可运行)线程已经调用了 start() 方法,等待 CPU 调度
Blocked(锁阻塞)线程在执行的时候未竞争到锁对象,则该线程进入 Blocked 状态
Waiting(无限等待)一个线程进入 Waiting 状态,另一个线程调用 notify 或者 notifyAll 方法才能够唤醒
Time Waiting(计时等待)同 Waiting 状态,有几个方法(sleepwait)有超时参数,调用他们将进入 Timed Waiting 状态。
Teminated(被终止)因为 run() 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡

6.3 转换关系

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值