线程同步、线程状态、线程通信、线程池

  • 线程同步
  • 线程状态
  • 线程通信
  • 线程池
学习目标
  • 能够描述Java中多线程运行原理
  • 能够使用继承类的方式创建多线程
  • 能够使用实现接口的方式创建多线程
  • 能够说出实现接口方式的好处
  • 能够解释线程安全问题出现的原因
  • 能够使用同步代码块解决线程安全问题
  • 能够使用同步方法的方式解决线程安全问题
  • 能够说出线程6个状态并进行描述
  • 能够理解线程通信概念
  • 能够理解等待唤醒机制
  • 能够描述java中线程池的运行原理
同步方法

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面等待着,排队。

格式:

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

备注:同步锁是谁?

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

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

Lock锁

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

Lock锁也称为同步锁,定义了加锁和解锁的动作,方法如下:

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

备注:锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。

线程状态

线程状态概述

当线程被创建并启动之后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态

在JavaAPI帮助文档java.lang.Thread.State这个枚举给出了线程的6种状态

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是还没有启动,还没有调用start方法
RUNNABLE(可运行)线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能 没有,这取决于操作系统处理器
BLOCKED(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程就进入到Runnable状态
WAITING(无限等待)一个线程在等待另一个线程执行一个动作(新建)时,该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
TIMED_WAITING(计时等待)同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知。带有超时参数的常用方法Thread.sleep(),Object.wait().
TERMINATED(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

六种状态切换描述
在这里插入图片描述

Timed Waiting(计时等待)

Timed Waiting在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态

其实当我们调用了sleep方法之后,当前正在执行的线程就进入到了计时等待状态

练习:实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1;i <= 100 ; i ++) {
            if (i % 10 == 0) {
                System.out.println("------------------>" + i);
            }
            System.out.println(i);
            // 在每个数字之间暂停1秒
            try{
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }       
    } 
    // 准备一个main函数
    public static void main(String[] args) {
        new MyThread().start();
    }
}

备注:

1.进入到Timed Waiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系

2.为了让其他线程有机会执行到,一般建议Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠

3.sleep与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会允许的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行。

在这里插入图片描述

Blocked锁阻塞状态

Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态,反之线程B就进入到Blocked锁阻塞状态。

在这里插入图片描述

Waiting无限等待状态

Waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态

一个调用了某个对象的Object.wait()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法

其实waiting状态它并不是一个线程的操作,它体现的是多个线程中间的*** 通信 ***,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。

等待唤醒机制
线程间通信
概念 :

​ 多个线程在处理同一个资源,但是处理的动作(线程的任务)却又不相同

比如说,线程A用来生产哇哈哈饮料,线程B用来消费娃哈哈饮料,哇哈哈饮料可以理解为统一资源,线程A和线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题

在这里插入图片描述

为什么要处理线程之间的通信

多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协议通信,以此来帮助我们达到多线程共同操作一份数据。

为什么要处理线程之间的通信:

多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协议通信,以次来帮助我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源

多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。

而这种手段就是---->等待唤醒机制

等待唤醒机制

这是多个线程间的一种 协作 机制。

就是一个线程进行了规定操作后,就进入到了等待状态(wait()),等待其他线程执行完他们的指定代码后,再将其唤醒(notify());

还有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。

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

等待唤醒的方法:

等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法有三个如下:

wait():线程不再活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不再去竞争锁,这时的线程状态就是WAITING。他还要等着别的线程执行一个特别的动作,就是唤醒通知(notify)在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中

notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等候就餐最久的顾客最先入座

notifyAll():释放所通知对象的wait set中的全部线程

备注:

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

总结后:

如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态

否则,从wait set中出来,又进入到set中,线程九乘WAITING状态转变成BLOCKED状态

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

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

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

3.wait方法与notify方法必须要在同步代码块或者同步方法中使用。因为,必须通过锁对象调用这两个方法来实现等待与唤醒。

生产者与消费者问题

等待唤醒机制经典的案例就是 生产者与消费者 的问题

举一个例子:生产包子与消费包子来描述等待唤醒机制如何有效的利用资源:

在这里插入图片描述在这里插入图片描述

/*
 * 资源类:包子类
 *    设置包子的属性
 *      皮
 *      馅
 *      包子的状态  有 true 没有 false
 */
public class Baozi {
	// 皮
	String pi;
	// 馅
	String xian;
	// 包子的状态  有 true 没有 false,设置初始值为false,没有包子
	boolean flag = false;
	
}
// 包子铺
/*
 * 生产者(包子铺):是一个线程类,继承Thread类
 * 	设置线程的任务:生产包子
 *   true:有包子
 *    包子铺调用wait方法进入等待状态
 *   false:没有包子
 *     增加一些难度:交替生产两种包子
 *     有两种状态:(i % 2 == 0)
 *    包子铺生产包子
 *    修改包子的状态为true
 *    唤醒吃货线程,让吃货去吃包子
 *    
 *  注意:
 *  	包子铺线程和吃货线程关系---->通信(互斥)
 *      必须使用同步技术保证两个线程只能有一个线程在执行
 *      锁对象必须保证唯一,可以使用包子对象作为锁对象
 *      包子铺线程和吃货线程的类需要把包子对象作为参数传递进来
 *         1.需要在成员的位置上创建一个包子变量
 *         2.使用带参构造,为这个包子变量赋值 	     
 */
public class Costs extends Thread{
	//1.需要在成员的位置上创建一个包子变量
	private Baozi baozi;
	
	//2.使用带参构造,为这个包子变量赋值 	
	public Costs(Baozi baozi) {
		this.baozi = baozi;
	}
	
	// 重写run方法
	@Override
	public void run() {
		// 设置线程任务:生产包子
		// 定义一个变量
		int count = 0;
		// 让包子铺一直生产包子
		while(true) {
			// 必须保证两个线程只能有一个线程在执行
			synchronized (baozi) {
				// 进行包子状态的判断
				if (baozi.flag) {
					//包子铺有包子,包子铺需要调用wait方法进入等待状态
					try {
						baozi.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				// 包子铺没有包子,被唤醒之后,包子铺生产包子
				// 增加一些难度:交替生产两种类型的包子
				if (count % 2 == 0) {
					//生产 三鲜馅的包子,皮是薄皮
					baozi.pi = "薄皮";
					baozi.xian= "三鲜馅";
				} else {
					// 生产  猪肉大葱馅 冰皮
					baozi.pi = "冰皮";
					baozi.xian = "猪肉大葱馅";
				}
				count++;
				System.out.println("包子铺正在生产:" + baozi.pi + baozi.xian + "包子");
				// 生产包子需要有一个过程:等待3秒钟
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 包子铺生产好了包子
				// 修改包子的状态为true 有
				baozi.flag = true;
				// 唤醒吃货线程,让吃货线程去吃包子
				baozi.notify();
				System.out.println("包子铺已经生产好了:" + baozi.pi + baozi.xian + "包子,吃货可以开始吃了。。");
			}
		}
	}
}
/*
 * 消费者(吃货)类:是一个线程类  extends Thread
 *   设置线程的任务:吃包子
 *   对包子的状态进行判断
 *   true:有包子
 *      吃货吃包子
 *      吃货吃完包子
 *      修改包子的状态味false:没有包子
 *      吃货唤醒包子铺线程,生产包子
 *   false:没有包子
 *      吃货调用wait方法,进入到等待状态
 */
public class Foodie extends Thread{
	// 1. 需要在成员的位置上定义一个包子变量
	private Baozi baozi;
	
	//2.使用带参构造,为这个包子变量赋值
	public Foodie(Baozi baozi) {
		this.baozi = baozi;
	}
	
	//3. 重写run方法
	@Override
	public void run() {
		// 设置线程任务:吃包子
		// 使用死循环,让吃货一直吃包子
		while(true) {
			// 使用同步技术保证两个线程只有一个线程在执行
			synchronized (baozi) {
				// 对包子的状态进行判断
				if (baozi.flag == false) {
					// 让吃货线程进入到等待状态
					try {
						baozi.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				// 被唤醒后执行吃包子
				System.out.println("吃货正在吃:" + baozi.pi + baozi.xian + "包子");
				// 吃货吃完包子
				// 修改包子的状态为false 没有
				baozi.flag = false;
				// 吃货线程唤醒包子铺线程--->生产包子
				baozi.notify();
				System.out.println("吃货已经把" + baozi.pi + baozi.xian + "的包子");
				System.out.println("------------------------------------------");
			}
		}
	}
}
public class TestChihuoAndBaoziPuDemo {

	public static void main(String[] args) {
		// 创建包子对象
		Baozi baozi = new Baozi();
		// 创建包子铺线程对象
		new Costs(baozi).start();
		// 创建吃货线程对象
		new Foodie(baozi).start();
	}
	
}

在这里插入图片描述

包子铺线程生产包子,吃货线程消费包子。当没有包子的时候(包子的状态为false),吃货线程需要等待,包子铺线程生产包子(包子的状态为true),并通知吃货线程(解除吃货等待的状态),因为已经有了包子,所以包子铺线程就需要进入到等待状态。
    接下来,吃货线程能否进一步执行则取决于锁的获取情况,如果吃货线程获取到锁,那么就执行吃包子的动作,包子吃完了(包子的状态为false),需要通知包子铺线程(解除包子铺线程等待状态),此时吃货线程就进入到等待状态。包子铺线程能否进一步执行则取决于锁的获取情况。
线程池
线程池的概念

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

由于线程池中有很多操作都是优化系统资源有关的,先来介绍下线程池的工作原理

在这里插入图片描述在这里插入图片描述

合理利用线程池能够带来什么样的好处:

  • 降低资源消耗。减少了线程的创建与销毁的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 提高了响应速度。当任务到达时,任务可以不需要等到线程的创建就能立即执行。
  • 提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器的宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,死机的风险也就更高)。
线程池的使用

java里面线程池的顶级接口是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): 获取线程池中的某一个线程对象,并执行

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

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

1.创建线程池对象

2.创建Runnable接口子类对象。(task)

3.提交Runnable接口子类对象。(take task)

4.关闭线程池(一般不做)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值