并行与并发
- 并发:指的是两个或者多个事件(任务)在同一时间段内发生的。
- 并行:指的是两个或者多个事件(任务)在同一时刻发生(同时发生)。
图解如下:
线程与进程 - 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个线程;进程也是程序的一次执行过程,是系统 运行程序的基本单位;系统运行一个程序就是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的运行,一个程序中至少有一个线程。一个进程可以有多个线程,这个应用程序也可以称之为多线程程序。
- 简而言之,一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
备注:单核处理器的计算机肯定不能并行的处理多个任务的,只能是多个任务在单个cpu上并发的执行。同理,线程也是一样的,从宏观的角度上理解线程是一种并行运行的,但是从微观上分析并行运行不可能,即需要一个一个线程的去执行,当系统只有一个cpu的时候,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
线程调度: - 分时调度:所有的线程轮流使用CPU的使用权,平均分配给每个线程占用CPUP的时间
- 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机一个线程执行,java使用的就是抢占式调度方式来运行程序。
创建线程类
- java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或者Thread类的子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流,java使用线程执行体来代表这段程序流。
- java中通过继承Thread类来创建并启动多线程,步骤如下:
1、创建一个Thread类的子类
2、在Thread类的子类当中重写Thread类的run方法,设置线程任务(开启线程需要你做什么事情?)
3、创建Thread类的子类对象
4、调用Thread类中的方法start方法,开启新线程,执行run方法 -
void start()使线程开始执行;java虚拟机调用该线程的run方法。
-
结果是两个线程并发地运行;当前线程(从调用返回给start方法)和另一个线程(执行其run方法)。
-
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
- java程序属于抢占式调度,哪个线程的优先级高,哪个线程就有就优先执行,同一个优先级的线程,系统就随机一个线程执行
示例代码如下:
示例一:
public class Student {
private String name;
// 定义一个方法run
public void run() {
//定义一个循环,循环20次,分别打印循环的次数
for (int i = 0; i <20; i++) {
System.out.println(name + "---->" + i);
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student(String name) {
super();
this.name = name;
}
public Student() {
super();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Student [name=" + name + "]";
}
/*
* 主线程:执行主(main)方法的线程
*
* 单线程程序:java程序中只有一个线程,执行从main方法开始,程序是从上到下依次执行。
*
* JVM执行main方法,main方法会进入到栈内存当中
* JVM会调用操作系统开辟一条main方法通向cpu的路径
* cpu就可以通过这个路径来执行main方法
* 而这个路径有一个名字,叫主线程(main线程) Thread(main)
*
*/
public class ThreadDemo01 {
public static void main(String[] args) {
Student s1 = new Student("小孙");
s1.run();
System.out.println(0 / 0);// Exception in thread "main" ArithmeticException
Student s2 = new Student("小王");
s2.run();
}
}
示例二:
//创建一个Thread类的子类
public class MyOneThread extends Thread{
@Override
public void run() {
//循环20次,打印循环次数
for (int i = 0; i < 20; i++) {
System.out.println("run:------->"+i);
}
}
/**
* 创建多线程程序的第一种方式,创建Thread类的子类
* java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
*
* 实现步骤:
* 1、创建一个Thread类的子类
* 2、在Thread类的子类当中重写Thread类的run方法,设置线程任务(开启线程需要你做什么事情?)
3、创建Thread类的子类对象
4、调用Thread类的方法start方法,开启新线程,执行run方法
void start() 使该线程开始执行,开启新线程,执行run方法
结果是两个线程并发的运行,当前线程(从调用返回给start方法)和另一个线程(执行其run方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动
java程序属于抢占式调度,哪个线程的优先级高,哪个线程就有就优先执行,同一个优先级的线程,系统就随机一个线程执行
*/
public class Demo01Thread {
public static void main(String[] args) {
MyOneThread myOneThread = new MyOneThread();
myOneThread.start();
//在主线程中循环20次 打印
for (int i = 0; i < 20; i++) {
System.out.println("main:---->"+i);
}
System.out.println("-------------------------");
//再次开启线程
myOneThread.start();//报错信息 java.lang.IllegalThreadStateException
}
多线程的原理
多线程的执行流程原理图:
- 程序启动运行main时候,java虚拟机启动一个进程,主线程main在main调用的时候被创建。随着调用 oneThread对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程环境下运行着。
- 通过上面一张图可以发现多线程在内存当中的执行流程。
- 多个线程执行时,在栈内存当中,其实每一个线程都有一片属于自己的栈内存空间,进行方法的压栈和弹栈。
- 当执行线程的任务结束了,线程自动在栈内存当中释放了。当所有的执行线程都结束了,那么进程也就结束了。
Thread类
API帮助文档中定义了有关线程的一些方法,具体如下:
构造方法:
- 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():获取当前正在执行的线程对象的引用。
通过翻阅API的得知,创建线程有两种方式,一种是继承Thread类,一种是实现Runable接口,接下来讲解第二种方式.
代码示例如下:
/*
* 设置线程的名称:
* 1.使用Thread类的方法setName(名字)
* void setName(String name) 修改线程的名称
* 2.使用Thread类的带参构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给予子类线程起一个名字
* Thread(String name) 分配一个指定名字的新的Thread对象
*/
// 1.
public class MyThread extends Thread{
public MyThread(){}
public MyThread(String name) {
super(name);//把线程名称传递给父类,让父类(Thread)给予子线程一个名字
}
// 2.
@Override
public void run() {
// 获取线程的名称
System.out.println(Thread.currentThread().getName() + "在执行任务");
}
public static void main(String[] args) {
// 3. 开启新线程
new MyThread("小强").start();;
// 3. 开启新线程
MyThread two = new MyThread();
two.setName("小孙");
two.start();
}
创建线程方式二
- 采用
java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:- 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
- 创建Runnable接口实现类的实例,并以此实例作为Thread类的target来创建Thread类的对象,该Thread类的对象才是真正的线程对象。
- 调用线程对象的start()方法来启动新线程。
实现Runnable接口:
- java.lang.Runnable:
- Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
- lang.lang.Thread:
- Thread(Runnable target): 分配新的Thread类的对象
- Thread(Runnable target,String name):分配新的Thread类的对象
实现步骤:
1.定义一个Runnable接口的实现类
2.在实现类中重写Runnable接口当中的run方法,设置线程任务。
3.创建Runnable接口实现类的对象
4.构建Thread类的对象,在构造方法中传递Runnable接口的实现类对象
5.调用Thread类中的start方法,开启新线程执行run方法
实现Runnable接口创建多线程程序的好处:
1.避免了单继承的局限性 -
一个类只能直接继承一个父类,类继承了Thread类就不能继承其他的类
-
2.增强了程序的扩展性,降低了程序的耦合性(解耦)实现Runnable接口,还可以继承其他的类,还可以实现其他的接口
- 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
- 实现类中,重写了run方法:用来设置线程的任务
- 创建Thread类的对象,调用start方法:用来开启新的线程
代码如下:
// 1.定义一个Runnable接口的实现类
public class Demo02RunnableImpl implements Runnable{
// 2.在实现类中重写Runnable接口当中的run方法,设置线程任务。
@Override
public void run() {
// 循环20次,打印循环的次数
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
public class Demo01Runnable {
public static void main(String[] args) {
//3. 创建Runnable接口实现类的对象
Runnable target = new Demo02RunnableImpl();// 多态写法
//4. 构建Thread类的对象,在构造方法中传递Runnable接口的实现类对象
Thread thread = new Thread(target);
//5. 调用Thread类中的start方法,开启新线程执行run方法
thread.start();
Thread thread2 = new Thread(new Demo03RunnableImpl());
thread2.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
public class Demo03RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("hello World" + i);
}
}
}
Thread类和Runnable接口的区别
- 如果一个类继承了Thread类,则不适合资源的共享。但是如果实现了Runnable接口的话,则很容易实现资源共享。
实现Runnable接口比继承Thread类的所具有的优势:- 适合多个相同的程序代码的线程去共享同一个资源
- 可以避免java中单继承的局限性
- 增加了程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程可以实现分离。
- 线程池只能放入实现Runnable或者Callable类的线程,不能直接放入继承Thread的类。
- 备注:在java中,每次程序运行至少启动两个线程,一个是main线程,一个垃圾收集线程。因为每当使用java命令去执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实都是在操作系统中启动了一个进程。
匿名内部类方式实现多线程程序的创建
-
使用线程的匿名内部类方式,可以很方便的实现每个线程执行不同的线程任务操作。、
-
使用匿名内部类方式实现Runnable接口的run方法。
示例代码如下:
public static void main(String[] args) {
// 创建线程对象
//new Thread().start();
new Thread() {
// 重写run方法
@Override
public void run() {
// 循环20次,打印循环的次数
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}.start();
// 线程的接口Runnable
Runnable run = new Runnable() {
// 重写run方法
@Override
public void run() {
// 循环20次,打印循环的次数
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
};
new Thread(run).start();
// 简化接口的方式
new Thread(new Runnable() {
// 重写run方法
@Override
public void run() {
// 循环20次,打印循环的次数
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}).start();
线程安全
- 如果有多个线程在同时的运行,而这些线程可能同时在运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的值是一样的,就是线程安全的。
代码示例如下:
// 实现卖票案例
public class RunnableImpl implements Runnable {
// 定义一个多线程共享的资源 票
private int ticket = 100;
// 设置线程的任务:卖票 此时窗口--->线程
@Override
public void run() {
// 先判断票是否存在
while(true){
if (ticket > 0) {
// 提高卖票的体验感 ,让程序睡眠下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖出第ticket张票
System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票");
ticket--;
}
}
}
}
/*
* 模拟卖票
* 创建3个线程(窗口),同时开启,对共享的票进行售卖
*
*/
public class SaleTicketDemo {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
Runnable run = new RunnableImpl();// 多态
/*Runnable run02 = new RunnableImpl();// 多态
Runnable run03 = new RunnableImpl();// 多态
*/
// 创建Thread类的对象,构造方法中传递Runnable接口的实现类对象
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
synchronized (t3) {
}
// 调用start方法开启新的线程
t1.start();
t2.start();
t3.start();
}
}
- 通过卖票买票小案例发现,当多个线程去共享同一个资源的时候出现了线程的不安全的问题。
1.相同的票数,被卖了多次
2.不存在的票,也被卖出去了,比如说0和-1 - 这种问题,几个窗口(线程)票数不同步,这种问题我们称之为线程不安全
- 备注:线程安全问题一般都是由全局变量或者静态变量引起的,若每个线程中对全局变量、静态变量只有读操作,而无写的操作,这样的话,这个全局变量就是线程安全的;若有多个线程同时执行写操作,一般就需要考虑线程的同步,否则的话就很可能会引发线程的安全问题。
线程的同步
- 当我们使用多线程访问同一资源的时候,且这多个线程中对资源有的写的操作,就容器出现线程安全问题。
- 要解决多线程并发访问一个资源的安全问题,java中提供了同步机制(synchronized)来解决。
根据上一次卖票卖票分析
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外面等着,当窗口1线程操作结束,窗口1和窗口2和窗口3才有机会进入代码中去执行。也就是说某个线程修改共享资源的时候,其他线程不能修改共享资源,等待修改完毕同步后,才能去抢夺cpu的使用资源,完成对应的操作,保证了数据的同步性。解决了线程不安全的问题。 - 有三种方式实现同步机制:
1、同步代码块
2、同步方法
3、锁机制
同步代码块
同步代码块:synchronized关键字可以用于方法中的某个代码块中,表示只对这个代码块的资源实行互斥访问。
格式:
synchronized(同步锁) {
// 需要同步操作的代码。
}
同步锁
- 同步锁是一个对象,是一个抽象的概念,可以想象成在对象上标记了一个锁。
- 锁对象可以是任意类型的。Object
- 多个线程对象,要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到同步锁谁就拥有资格进入代码块中,其他线程只能在外面等待着。(Blocked阻塞状态)
同步原理图解:
同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面的等待着,排队
格式:
public synchronized void method(){
//可能会产生线程安全问题的代码
} - 备注:同步锁是谁?
1、对于非static方法,同步锁就是this
2、对于static方法,我们使用当前方法所在类的字节码对象(类名.class)
例子如下:
非静态的例子:
// 定义一个多线程共享的资源票
private static int ticket = 100;
// 设置线程的任务:卖票 此时窗口---->线程
@Override
public void run() {
System.out.println(this + "---------->");
// 先判断票是否存在
while (true) {
saleTicket();
}
}
/**
* 定义一个同步方法 同步方法也会把方法内部的代码锁住 只让一个线程访问 同步方法中的锁对象是谁? 就是实现类对象 new RunnableImpl()
* 也就是this
*/
public synchronized void saleTicket() {
if (ticket > 0) {
// 提高卖票的体验感,让程序睡眠下
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} // 票存在卖出第ticket张票
System.out.println(Thread.currentThread().getName() + "--------正在售卖" + ticket-- + "张票");
}
}
静态的例子:
// 定义一个多线程共享的资源票
private static int ticket = 100;
// 设置线程的任务:卖票 此时窗口---->线程
@Override
public void run() {
System.out.println(this + "---------->");
// 先判断票是否存在
while (true) {
saleTicket();
}
}
public static /* synchronized */ void saleTicket() {
synchronized (RunnableImpl.class) {
if (ticket > 0) {
// 提高卖票的体验,让程序睡眠下
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 票存在,卖出第ticket张票
System.out.println(Thread.currentThread().getName() + "--------->" + ticket + "张票");
ticket--;
}
}
}
Lock锁:
- java.util.concurrent.locks.lock机制提供了比synchronized代码块和synchronize同步方法更加广泛的锁操作,同步代码块/同步方法具有的功能,Lock都有,除此之外更加强大,更能体现出面向对象特征的
- Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:
1、public void lock():同步锁
2、public void unlock():释放锁 - 解决线程安全问题的第三种方式,使用Lock锁
- java.util.concurrent.locks.Lock接口
- Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作
- Lock锁接口中实现了两个操作
- public void lock();//获取锁
- pupblic void unlock();//释放锁
- java.util.concurrent.locks.ReentranLock implements Lock接口
使用步骤:
1、在成员的位置创建一个ReentranLock对象
2、在可能会引发线程问题的代码前,调用Lock接口中的lock方法获取锁。
3、在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
示例代码如下:
/**
* 实现买票案例 解决线程安全问题的第三种方式,使用Lock锁 java.util.concurrent.locks.Lock接口
*
* Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作 Lock锁接口中实现了两个操作 public void
* lock();//获取锁 pupblic void unlock();//释放锁
* java.util.concurrent.locks.ReentranLock implements Lock接口
*
* 使用步骤: 1、在成员的位置创建一个ReentranLock对象 2、在可能会引发线程问题的代码前,调用Lock接口中的lock方法获取锁。
* 3、在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
*
*/
public class RunnableImpl implements Runnable {
// 定义一个多线程共享的资源票
private int ticket = 100;
// 1、在成员的位置创建一个ReentranLock对象
Lock Lock = new ReentrantLock();
// 设置线程的任务:卖票 此时窗口---->线程
@Override
public void run() {
// 先判断票是否存在
while (true) {
// 2、在可能会引发线程问题的代码前,调用Lock接口中的lock方法获取锁。
Lock.lock();
if (ticket > 0) {
// 提高卖票的体验感,让程序睡眠下
try {
Thread.sleep(100);
// 票存在卖出第ticket张票
System.out.println(Thread.currentThread().getName() + "--------正在售卖" + ticket-- + "张票");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//无论程序出现异常,此时都会把锁释放掉
//在finally语句块中一般用于释放资源,关闭Io流,释放lock锁,关闭数据库连接等
//3、在可能会引发线程安全问题的代码后调用Lock接口中的unlock释放锁。
Lock.unlock();
}
}
}
}
}
/**
* 模拟卖票
* 创建3个线程(窗口),同时开启,对共享的票进行售卖
*/
public class SaleTicketDemo {
public static void main(String[] args) {
//创建Runnable接口的实现的对象
Runnable run = new RunnableImpl();
//创建Thread类的对象,构造方法中传递Runnable接口的实现类对象
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
//调用start方法开启新的线程
t1.start();
t2.start();
t3.start();
}
线程状态
线程状态概述:
当线程被创建并启动后,它既不是一启动就进入到了执行状态,也不是一直处于执行状态。在线程的生命周期中有6种状态,在javaAPI帮助文档中java.lang.Thread.State这个枚举给出了线程的6中状态。
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是还没有穷,还没有调用start方法 |
RUNNABL(可运行) | 线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决于操作系统处理器 |
BLOCKED(锁阻塞) | 当一个线程视图获取一个对象锁,而该对象所被其他线程所持有,则该线程进入到Blocked状态;当该线程持有锁时,该线程进入到Runnable状态 |
WAITING(无限等待) | 一个线程在等待另一个线程执行一个动作(新建时),该线程就进入到Waiting状态,进入这个Waiting状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒 |
TIMED_WAITING(计时器等待) | 同Waiting状态,有几个方法有超时参数,调用他们将进入Tiimed Waiting状态,这一状态将一直保持到超时期满或者是收到了唤醒通知,带有超市参数的常用方法Thread.sleep(),Object.wait() |
TERMINATED(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run()方法而死亡 |
六中状态切换描述图解:
Timed Vaiting(计时等待)
Timed Waitng在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态其实当我们调用了sleep方法之后,当前正在执行的线程就进入到了计时等待状态。
- 例子:如实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串。
public class MyThread extends Thread{
@Overried
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();
}
}
备注:
- 进入到Timed Waiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系
- 为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠
- sleep与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态。sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行
Blocked锁阻塞状态 - Blocked状态在JavaAPI中描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
- 比如:线程A与线程B代码中使用同一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态反之线程B进入到Blocked锁阻塞状态。
Waiting无限等待状态 - waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别(唤醒)动作的线程处于这一状态。
- 一个调用了某个对象的Object.wait()方法的线程,会等待另外一个线程调用此对象的Object.netify()或者Object.netifyAll()方法,其实waiting状态它并不是一个线程的操作,它体现的是多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。
等待唤醒机制
线程间通信
**概念:**多个线程在处理同一个资源,但是处理的动作(线程的任务)却又不相同。
比如说,线程A用来生产一个娃哈哈饮料,线程B用来消费娃哈哈饮料,娃哈哈饮料可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通讯问题。
- 图例如下:
为什么要处理线程之间的通信:
- 多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且我们希望他们有规律的执行,那么多线程之间就需要一些协调通信,以此来帮助我们达到多线程共同操作一份数据
- 如何保证线程间通信有效利用资源:
- 多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一变量的使用或者操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源。
- 而这种手段就是----->等待唤醒机制
等待唤醒机制 - 什么是等待唤醒机制呢?
- 这是多个线程间的一种协作机制。
- 就是一个线程进行了规定操作后,就进入到了等待状态(wait()),等待其他线程执行完它们的指定代码后再将其唤醒(nnotify())
- 在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程。
- wait/notify就是线程间的一种协作机制。
等待唤醒中的方法: - 等待唤醒机制就是用来解决线程间通信问题的。可以使用到的方法由三个如下:
1、wait():线程不在活动,不再参与调度,进入到waiting设置中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程状态就是WAITING。他还要等着别的线程执行一个特别的动作,就是**唤醒通知(notify)**在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中。
2、notify():选取所通知对象的wait set中的一个线程释放。例如:餐厅有空位置后,等待就餐最久的雇客最先入座。
3、notifyAll():释放所通知对象的wait set中的全部线程。 - 备注:哪怕之通知了一个等待线程,被通知的线程也不能立即回复执行,因为当初中断的地方是在同步块内,而它此刻已经不在持有锁了,所以他需要再次尝试着去获取锁(很有可能民艾琳者其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行。
- 总结下:
如果能获取到锁,线程就从WAITING状态转变成RUNNABLE状态
否则,从wait set中,又进入set中,线程就从WAAITING状态转变成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),需要通知包子铺线程(解除包子铺线程等待状态),此时吃货线程就进入到等待状态。包子铺线程能否进一步执行则取决于锁的获取情况。