线程调度
分时调度
所有的线程轮流占有CPU的使用权,平均分配每个线程占用的CPU的时间
抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个去执行–线程的随机性
Java使用的就是抢占式调度
Thread类是的包类路径是:java.lang.Thread。其中有需要我们掌握的常用API如下:
构造方法:
常用方法:
获取线程名称的两种方式案例:
public class YourThread extends Thread{
/*
获取线程的名称:
1、使用Thread类中的方法getName();,返回当前正在执行线程的名称
2、使用线程的currentThread();得到当前正在执行的线程
再通过线程的getName();获取线程的名称
*/
//重写Thread中的run方法,设置线程任务
@Override
public void run() {
//方式1:
String name = getName();
System.out.println("方式1新线程的名称是:"+name);
//方式2
//1、获取当前正在执行的线程的引用
Thread thread = Thread.currentThread();
//2、获取当前正在执行线程的名称
String threadName = thread.getName();
System.out.println("方式2新线程的名称是:"+threadName);
}
}
public class Demo02_ThreadName {
public static void main(String[] args) {
//1、创建Thread类的子类对象
YourThread thread = new YourThread();
//2、调用start()方法,开启新线程,执行run()方法
thread.start();
//3、获取主线程的名称
System.out.println("主线程的名称是:"+Thread.currentThread().getName());//main
}
}
public class Demo03_SleepMethod {
/*
线程的sleep()方法
使当前正在执行的线程以指定的毫秒数暂停,毫秒结束之后,线程继续执行
public static void sleep(long millis){}
*/
public static void main(String[] args) {
//模拟计时器
for (int i = 1; i < 60; i++) {
System.out.println(i);
//使用Thread类的sleep方法让线程睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Person {
private String name;
//定义成员方法
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "----: " + i);
}
}
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Demo06_MainThread {
/*
主线程:执行主方法main()的线程
单线程程序:
java程序中只有一个线程,执行从main()方法开始,从上到下一次执行
单线程程序执行步骤:
1、JVM执行main()方法,main方法进入栈内存
2、JVM会找操作系统开辟一条main()方法通向CPU的执行路径
3、CPU通过此路径来执行main方法,此路径被称为main(主)线程
4、当主线程执行过程中出现异常,那么在异常之后的代码将不会被执行
*/
public static void main(String[] args) {
//创建Person对象
Person p1 = new Person("Tom");
Person p2 = new Person("Anny");
//调用方法--按照顺序执行---即单线程程序
p1.run();
//手动设定数学异常
System.out.println(1/0);
p2.run();
}
}
单线程程序在无异常的顺序执行的情况
单线程程序在有异常的顺序执行的情况
案例讲述多线程原理:
自定义线程类:
public class MyThread extends Thread {
/*
1、创建Thread线程的一个子类
2、在其子类中重写run()方法,设置线程任务
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("run线程"+i);
}
}
}
线程测试类:
public class Demo01_Thread {
public static void main(String[] args) {
//1、创建MyThread类的子类对象
MyThread mt = new MyThread();
//2、调用线程中的start()方法开启线程,执行线程中的run()方法
mt.start();
//3、主线程执行主方法中的代码
for (int i = 0; i < 10; i++) {
System.out.println("main主线程" + i);
}
}
}
执行原理图:
**创建线程类:**Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread的子类或者子类的实例
每个线程的作用是完成一定的任务,实际上就是执行一段程序。Java使用线程执行体来代表这段程序,
Java通过集成Thread类来创建并启动多线程的步骤如下:
public class ThreadA extends Thread{
/*
1、创建Thread的子类
2.重写Thread的run()方法,设置线程任务
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("run线程执行"+i);
}
}
}
public class Demo07_MoreThread1 {
/*
创建多线程程序的方式1:继承Thread类,重写run()方法
实现步骤:
1、创建Thread类的子类
2、在子类中重写run()方法,设置线程任务,即开启线程做什么
3、创建子类对象
4、子类对象调用start()方法,开启新的线程,执行run()方法
start()方法的解释:
1、void start()方法,是使该线程开始执行,Java虚拟机调用线程的run()方法
2、结果是两个线程并发的运行:即当前线程(main线程)和另一个线程(新创建的线程,执行其run()方法)
3、多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动
注:Java程序是抢占式调度,哪个线程的优先级高,就先执行哪个,同级别,则随机选择一个执行
*/
public static void main(String[] args) {
//3、创建Thread子类对象
ThreadA ta = new ThreadA();
//4、子类对象调用start()方法,开启新的线程,执行run()方法
ta.start();
//主线程执行
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() +" :"+ i);
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1DfKSzTW-1692323226160)(photo/JavaSE17_线程.assest/1672459030717.png)]
创建线程的方式有两种,
方式一的继承方式已经在上面使用过,接下来使用第二种方式创建线程,采用了java.lang.Runable
实现Runable接口,重写run方法
public class MyRunable implements Runnable{
//1、创建接口实现类,并重写run方法
@Override
public void run() {
//在run方法中设置线程任务
for (int i = 1; i < 10; i++) {
//获取当前正在执行线程的名称
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public class Demo04_Runable {
/*
实现Runable接口,创建线程对象
public interface Runnable
Runnable接口应由任何类实现,其实例将由线程执行。
该类必须定义一个无参数的方法,称为run 。
java.lang.Thread的构造方法,可以传递Runable实现类的对象作为参数
public Thread(Runnable target)
分配一个新的 Thread对象。
public Thread(Runnable target, String name)
分配一个新的 Thread对象。
实现步骤:
1、创建一个Runable接口的实现类
2、在实现类中重写Runable接口总的run方法,设置线程任务
3、创建一个Runable接口的实现类对象
4、创建Thread类的对象,构造方法中传递Runable接口的实现类对象
5、调用Thread类中的start()方法,开启新的线程,执行run方法
*/
public static void main(String[] args) {
//3、创建一个Runable接口的实现类对象
MyRunable runable = new MyRunable();
//4、创建Thread类的对象,构造方法中传递Runable接口的实现类对象
Thread thread = new Thread(runable);
//5、调用Thread类中的start()方法,开启新的线程,执行run方法
thread.start();
for (int i = 1; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
继承Thread类,创建线程对象,不适合资源共享,因为java是单继承的
实现Runable接口,重写run方法,适合资源共享,因为接口可以多实现
实现Runable接口比继承Thread类的优势:
在java中,每次程序运行至少要启动两个线程,一个是main线程,另一个是垃圾回收线程,因为每次使用java执行一个类的时候,实际上就会启动一个JVM,每一个JVM其实就是在操作系统中启动了一个进程。
使用线程的匿名内部类方式,可以方便的实现每个线程执行不同的任务操作
使用匿名内部类实现Runable接口,重写Runable接口中的额run方法
public class Deomo05_InnerClassThread {
/*
匿名内部类实现线程的创建
1、子类继承父类,重写父类的方法,创建子类对象,一步完成
2、实现类实现接口,重写接口中的方法,创建实现类对象,一步完成
匿名内部内的最终产物是:子类/实现类对象,并且这个类没有名字
语法格式:
new 父类/接口(){
重写父类/接口中的方法
}
*/
public static void main(String[] args) {
//方式1:父类Thread
new Thread() {
//重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("新线程1启动执行:"+Thread.currentThread().getName()+"---"+i);
}
}
}.start();
//方式2:接口Runnable
Runnable runnable = new Runnable(){
//重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("新线程2启动执行:"+Thread.currentThread().getName()+"---"+i);
}
}
};
//启动线程
new Thread(runnable).start();
//方式2:接口Runnable简化写法
new Thread(
new Runnable(){
//重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("新线程2启动执行:"+Thread.currentThread().getName()+"---"+i);
}
}
}
).start();
}
}
如果有多个线程同时运行,而这些线程可能会同时运行这些代码,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值和预期的也是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题
模拟电影院售票过程,假设本次播放电影的放映室有100个座位,也就是本场次电影能卖100张票
模拟电影售票窗口,多个窗口同时卖票,总票数是100张,
售票窗口:使用线程对象来模拟
售票数:使用Runnable接口的实现类来模拟
售票案例之线程不安全:
package saleticket;
/*
实现售票案例
*/
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();
}
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
}
}
}
}
package saleticket;
/**
* 模拟卖票案例:创建3个线程,同时开启对共享的票进行出售
*/
public class Demo01_Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
//构造方法创建线程传递实现类对象
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
运行结果,出现了线程安全问题,:
售票线程安全问题出现的原理分析图:
售票线程安全问题的解决方案:
让1个线程在访问共享数据的时候,无论是否失去了CPU的执行权,都让其他的线程进行等待,直到当前线程卖完票,代码执行完毕,其他线程再进行卖票,保证始终只有一个线程在共享数据中取票。
但我们使用多个线程访问同一个资源的时候,且多个线程多共享资源有写的操作,就容易出现线程安全问题,
需要解决上述多线程访问同一资源的安全性问题,也就是解决重复票与不存在票的问题。
在java中提供了同步机制 synchronized 来解决
根据案例简述:
同步代码块:
synchronized(同步数){
需要同步操作的代码
}
同步锁
使用同步代码块解决售票线程安全问题:
package synchronizedticket;
/*
解决售票案例卖出重复的票和不存在的票的线程安全问题
解决方案1:
使用同步代码块
格式:
synchronized(锁对象){
可能出现线程安全问题的代码;
}
*/
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票数
private int ticket = 100;
//创建一个锁对象
Object object = new Object();
//设置线程任务:卖票
@Override
public void run() {
//设置死循环,一直卖票
while (true) {
//创建同步代码块
synchronized (object) {
//判断票号是否存在
if (ticket > 0) {
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
}
}
}
}
}
运行结果:不会出现相同票和不存在票的现象
同步的原理分析:
同步方法:使用synchronized关键字修饰的方法,就叫做同步方法,即保证A线程在执行该方法的时候,其他线程只能在方法外等待。
语法格式:
public synchronized 返回值类型 方法名(参数列表){
可能产生线程安全问题的代码;
}
同步方法的原理
同步锁的问题
案例:
package synchronizedmethod;
/*
实现售票案例,同步方法解决线程安全问题
使用同步方法解决线程安全问题的步骤:
1、抽取出访问了共享数据的代码,放在一个方法中
2、在方法上条件synchronized修饰符
*/
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票数
private int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//验证锁对象
System.out.println("锁对象是" + this);
//设置死循环,一直卖票
while (true) {
//调用方法
saleTicket();
}
}
/*
定义一个同步方法,将访问共享数据的代码放在其中
原理:
1、同步方法会将方法内部的代码锁住
2、只让一个线程执行
3、同步方法的锁对象
对于普通方法(非static)方法而言,其同步锁就是this,即接口的实现类对象
对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
*/
//同步方法实现解决线程安全问题
public synchronized void saleTicket() {
//判断票号是否存在
if (ticket > 0) {
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
}
}
//同步代码块实现解决线程安全问题:
/*public void saleTicket() {
synchronized (this) {//this可以是代替单独创建的Object对象,
//判断票号是否存在
if (ticket > 0) {
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
}
}
}*/
}
package synchronizedmethod;
/**
* 模拟卖票案例:
* 创建3个线程,同时开启对共享的票进行出售
* 使用同步方法解决线程安全问题
*/
public class Demo01_Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
System.out.println("this就是锁对象"+run);
//构造方法创建线程传递实现类对象
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
package staticsynchronizedmethod;
/*
实现售票案例,同步静态方法解决线程安全问题
使用静态同步方法解决线程安全问题的步骤:
1、抽取出访问了共享数据的代码,放在一个方法中
2、在方法上static和synchronized修饰符
3、此时的锁对象是:当前方法的调用类的字节码对象(类名.class)
*/
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票数
private static int ticket = 100;
//设置线程任务:卖票
@Override
public void run() {
//设置死循环,一直卖票
while (true) {
//调用方法
saleTicketStatic();
}
}
/*
定义一个同步方法,将访问共享数据的代码放在其中
原理:
1、同步方法会将方法内部的代码锁住
2、只让一个线程执行
3、同步方法的锁对象
对于普通方法(非static)方法而言,其同步锁就是this,即接口的实现类对象
对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
*/
//静态同步方法实现解决线程安全问题
/* public static synchronized void saleTicketStatic() {
//判断票号是否存在
if (ticket > 0) {
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
}
}*/
//上述无法输出this,使用同步代码块实现
public static void saleTicketStatic() {
synchronized (RunnableImpl.class) {
//static修饰方法,此时的this不是对象本身,是方法调用者的类的字节码对象、或者父类接口的字节码对象
//判断票号是否存在
if (ticket > 0) {
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
}
}
}
}
package staticsynchronizedmethod;
/**
* 模拟卖票案例:
* 创建3个线程,同时开启对共享的票进行出售
* 使用同步静态方法解决线程安全问题
*/
public class Demo01_Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
//构造方法创建线程传递实现类对象
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
java.util.concurrent.locks。Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
同步代码块和同步方法所具有的功能,Lock锁都有,并且其功能更强大,更能体现面向对象
Lock锁也称同步锁,加锁和释放锁的方法如下:
Lock接口的实现类:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLock implements Lock, java.io.Serializable {
类成员;
}
使用步骤:
注意:
Lock锁解决售票案例的线程安全问题:
package lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*
售票案例出现了线程安全问题,出现课不存在的票和重复的票
解决线程安全问题的第三种方式,使用Lock锁
Lock接口中的方法
1、添加锁:public void lock();
2、释放锁:public void unlock();
Lock接口的实现类:
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤:
1、在成员位置创建一个ReentrantLock对象
2、在可能出现安全问题的代码前调用Lock接口中的方法,lock添加锁
3、在可能出现安全问题的代码后调用unLock接口中的方法,lock释放锁
*/
public class RunnableImpl implements Runnable {
//定义一个多线程共享的票数
private int ticket = 100;
//1、在成员位置创建Lock接口实现类对象
Lock lock = new ReentrantLock();
//设置线程任务:卖票
@Override
public void run() {
//设置死循环,一直卖票
while (true) {
//2、在可能出现安全问题的代码前调用Lock接口中的方法,lock添加锁
lock.lock();
//判断票号是否存在
if (ticket > 0) {
try {
//提高安全问题出现的概率,让程序睡眠
Thread.sleep(10);
//当前正在执行的线程的名称
String name = Thread.currentThread().getName();
//票存在,进行售卖
System.out.println(name + "正在卖第" + ticket + "张票");
//售出之后,总票数-1
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//3、在可能出现安全问题的代码后调用unLock接口中的方法,lock释放锁
lock.unlock();//无论程序是否异常,都会释放锁
}
}
}
}
}
package lock;
/**
* 模拟卖票案例:
* 创建3个线程,同时开启对共享的票进行出售
* 线程不安全问题出现。
*/
public class Demo01_Ticket {
public static void main(String[] args) {
//创建接口实现类对象
RunnableImpl run = new RunnableImpl();
//构造方法创建线程传递实现类对象
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
概念:多个线程在处理同一个资源,但是线程的任务不同。
如:线程A是司机开公交车,线程B是乘客乘坐公交车,公交车可以理解成同一个资源,线程A和线程B的动作,一个是生产,一个是消费,那么线 程A和线程B之间就存在线程通信问题。
为什么需要处理线程通信?
多个线程并发执行的时候,在默认情况下CPU是需要随机切换线程的,当需要多个线程来共同完成一个任务,并且希望它们有规律的执行,那么多线程之间就需要协调通信,以此来完成多线程共同操作一份数据。
如何保证线程之间通信有效利用资源?
多个线程在处理同一个资源,并且任务不同时,需要线程通信来解决线程之间对同一个变量的使用或操作,就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是需要通过一定的手段使得各个线程能够有效的利用资源,这种手段叫做----等待线程唤醒机制。
什么是等待线程唤醒机制?
这是多个线程之间的协作机制,说到线程想到的就是线程之间的竞争,比如线程去争夺锁对象,但这不是线程的全部,不仅有竞争也有协作,线程之间也会有协作机制、
线程等待机制,就是线程在进行了规定操作后,就进入等待机制wait()。等待其它线程执行完自身的代码后,再将其唤醒notify(),在有多个线程进行等待时,如果需要,可以使用notifyAll(),来唤醒所有等待的线程。
wait();和notify()/notifyAll();是线程的一种协作机制
等待唤醒机制就相当于解决线程间通信的问题,使用到的三个方法的功能如下:
注意事项:
哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为当初中断的地方在同步块中,而此刻它已经不再持有锁,所以会再次尝试去获取锁,可能面临其它线程的竞争,成功之后才能在当初调用wait方法之后的地方进行执行。
总结:
调用wait和notify方法需要注意的细节:
等待唤醒机制其实就是经典的生产者和消费者的问题。
举例实现等待唤醒机制如何实现有效利用资源:
司机生产座位,乘客消费座位,当座位没有时,座位的状态为false,乘客线程等待,司机线程生产座位,产出座位,此刻座位的状态为true,并通知乘客线程,解除乘客线程的等待状态,因为已经有了座位,那么司机线程进入等待状态,紧接着,乘客线程能否进一步执行取决于锁对象的获取状况,如果乘客获得到锁,那么就执行乘车的动作,座位用完,座位状态为false,并通知司机线程解除生产座位的等待状态.
通信:对座位状态进行判断
案例
package waitnotify;
/**
* 资源座位类:设置座位的属性
*/
public class ZuoWei {
//设置是否有座位:true有,false没有,初始值是false没有
boolean flag = false;
}
package waitnotify;
/**
* 司机类:生产者生产座位
* 1、是一个线程类,继承Thread
* 2、对座位的状态进行判断
* true:有座位,司机线程进入wait等待状态
* false:没有座位,司机线程生产座位,
* 3、座位生产完毕,修改座位的状态为true
* 4、唤醒乘客线程,让乘客使用座位
*
* 注意事项:
* 1、司机线程和乘客线程-->通信互斥
* 2、必须同时同步,保证两个线程只有一个在执行
* 3、锁对象必须保证唯一,可以使用座位对象座位锁对象
* 4、司机类和乘客类就需要把座位对象座位参数传递
* a、在类的成员位置创建座位类的变量
* b、使用有参构造为座位变量赋值
*/
public class Driver extends Thread {
//1、在成员位置定义座位变量
private ZuoWei zw;
//2、有参数构造方法,为座位变量赋值
public Driver(ZuoWei zw) {
this.zw = zw;
}
//3、设置线程任务run()生产作为
@Override
public void run() {
//定义作为数量变量
int count = 0;
while (true) {//死循环,让司机一直生产座位
//4、必须同时同步,保证两个线程只有一个在执行,将座位对象作为锁对象
synchronized (zw) {
//5、对作为的状态进行判断
if (zw.flag == true) {
//6、true:有座位,司机线程进入wait等待状态
try {
zw.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码,生产座位
//7、false:没有座位,司机线程被唤醒,生产座位,
if (count % 2 == 0)
count++;
System.out.println("司机正在生产座位");
//8、生产座位需要3秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//9、司机线程生产好作为,修改座位的状态为false
zw.flag = true;
//10、唤醒乘客线程,使用座位
zw.notify();
System.out.println("司机完成座位生产");
}
}
}
}
package waitnotify;
/**
* 乘客类:消费者消费座位
* 1、设置线程run()的任务是使用座位
* 2、对座位的状态进行判断
* false:没有座位,乘客调用wait方法进入等待状态
* true:有座位,乘客使用座位,作为被用完,修改作为的状态为没有false
* 3、乘客唤醒司机线程,生产座位
*/
public class ChengKe extends Thread{
//1、在成员位置创建座位变量
private ZuoWei zw;
//2、生成带参数构造方法,并为此座位变量赋值
public ChengKe(ZuoWei zw) {
this.zw = zw;
}
//3、设置线程的任务
@Override
public void run() {
//设置死循环,让乘客一直使用座位
while (true){
//必须同时同步,保证两个线程只有一个在执行
synchronized (zw){
//对座位的状态进行判断
if (zw.flag==false){
//乘客调用wait方法进入等待状态
try {
zw.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//被唤醒之后执行的代码,使用座位
System.out.println("乘客正在使用座位");
//乘客使用完座位。修改座位的状态为false
zw.flag = false;
//乘客唤醒司机线程生产座位
zw.notify();
System.out.println("乘客使用完毕座位");
System.out.println("------------------------------");
}
}
}
}
package waitnotify;
/**
* 测试类:
* 1、创建座位对象
* 2、创建司机线程,开启,生产座位
* 3、创建乘客线程,开启,使用座位
*/
public class TestZW {
public static void main(String[] args) {
//1、创建座位对象
ZuoWei zw = new ZuoWei();
//2、创建司机线程,开启,生产座位
Driver driver = new Driver(zw);
driver.start();
//3、创建乘客线程,开启,使用座位
ChengKe chengKe = new ChengKe(zw);
chengKe.start();
}
}
当线程被创建并启动以后,其不是一启动就进入了执行的状态,也不是一直处于执行状态,在线程的声明周期中,
有以下六种状态:
线程状态 | 导致状态发生的条件 |
---|---|
New 新建 | 线程刚被创建,但是并未启动,还没有调用run方法 |
Runnable 可运行 | 线程可以在java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,取决于操作系统处理器 |
Blocked 锁阻塞 | 当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入Blocked状态,当该线程持有锁时,该线程状态将变成Runnable状态 |
Waitting 无限等待 | 一个线程在等待另一个线程执行唤醒动作时,该线程进入Waitting状态,进入这个状态之后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒 |
Timed Waitting 计时等待 | 同Wattting状态,有几个方法有超时参数,调用他们将进入Timed Watting状态,这一状态将一直保持到朝时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep(); Object.wait(); |
Terminated 退出线程的状态 | 已退出的线程处于这个状态,也就是死亡状态,即因run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
面试题:
1、sleep和wait方法的区别:
2、yield和join的方法的区别:
3、notify和notifyAll方法的区别
Timed Waitting的定义:一个正在限时等待另一个线程执行一个唤醒动作的线程处于这一状态
类似于在售票案例中,为了避免线程执行太快,售票不明显等问题,在run方法中条件了sleep语句,这样就强制当前正在执行的线程休眠,即暂停执行,以减慢线程
其实当调用了sleep方法之后,当前执行的线程就进入了休眠状态,其实就是所谓的Timed Waitting计时等待。
**案例:**实现计数器,计数到100,每个数字之间间隔1秒。隔10个数字输出一个字符串
package timedwaitting;
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);
try {
//调用线程的sleep方法让程序睡眠1秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package timedwaitting;
public class TestSleep {
public static void main(String[] args) {
//创建线程
MyThread myThread = new MyThread();
//开启线程
myThread.start();
}
}
通过案例可以发现,sleep方法的使用注意事项
Timed Watting 线程状态图:
Blocked状态的定义:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
在同步机制中,线程A和线程B在使用同一个锁,如果线程A获得到锁,线程A就进入Runnable(可运行)状态。那么线程B就进入了Blocked阻塞状态。
这是由于Runnable的状态进入Blocked状态,除此Timed Waitting状态也会在某种情况下进入阻塞状态。
Blocked阻塞状态图:
Waittting状态的定义:一个正在无限期等待另一个线程执行一个特别的唤醒动作的线程处于这一状态
案例:
package waitandnotify;
import javafx.beans.binding.When;
/**
* 等待线程唤醒:线程通信案例
* 1、创建一个乘客线程(消费者),告知司机需要座位的数量,调用wait方法,摒弃CPU的执行,进入到Waitting状态
* 2、创建一个司机线程(生产者),花了3秒生产座位,座位生产完毕后,调用notify方法,唤醒乘客线程使用座位
* 3、注意:乘客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
* 同步使用的锁对象必须保证唯一
* 只有锁对象才能调用wait()和notify()方法
* 4、方法解析
* void wait();在其他线程调用此对象的notify()或notifyAll()方法之前,导致当前线程等待
* void notify();唤醒在此对象监视器上等待的单个线程,或继续执行wait()方法之后的代码
*/
public class WaitAndNotify {
public static void main(String[] args) {
//创建锁对象,保证唯一,为什么使用Object,因为Object是所有类的超类(多态思维)
Object object = new Object();
//创建一个乘客线程--消费者
new Thread() {
@Override
public void run() {
//设置死循环,让乘客一直等待使用座位
while (true) {
//客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
synchronized (object) {
System.out.println("乘客告知司机需要座位的数量");
//调用wait方法,摒弃CPU的执行,进入到Waitting状态
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("座位已经生产完成了,咱们可以使用了");
System.out.println("----------------------------");
}
}
}
}.start();
//创建一个司机线程--生产者
new Thread() {
@Override
public void run() {
//设置死循环,让司机一直生产座位
while (true) {
//花了3秒生产座位
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
synchronized (object) {
System.out.println("司机3秒之后生产完座位,通知乘客使用");
//座位生产完毕后,调用notify方法,唤醒乘客线程使用座位
object.notify();
}
}
}
}.start();
}
}
进入到Timed Waitting状态的两种方式
方式一:
方式二:
package waitandnotify;
/**
* 进入到TimedWaitting状态的两种方式
* 1、使用Thread.sleep(long time)方法,在毫秒值时间结束之后,
* 线程进入到Runnable/Blocked状态
* 2、使用wait(long time)方法,wait方法在毫秒值时间结束之后,
* 还没有被notify唤醒,就会自动醒来,线程进入到Runnable/Blocked状态
*/
public class TimedWaitting {
public static void main(String[] args) {
//创建锁对象,保证唯一.为什么使用Object,因为Object是所有类的超类(多态思维)
Object object = new Object();
//创建一个乘客线程--消费者
new Thread() {
@Override
public void run() {
//设置死循环,让乘客一直等待使用座位
while (true) {
//客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
synchronized (object) {
System.out.println("乘客告知司机需要座位的数量");
//调用wait方法,摒弃CPU的执行,进入到Waitting状态
try {
//设置等待时间后,毫秒时间结束,还未notify,就自动苏醒
object.wait(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("座位已经生产完成了,咱们可以使用了");
System.out.println("----------------------------");
}
}
}
}.start();
}
}
唤醒线程的两种方式:
package waitandnotify;
/**
* 唤醒的方法:
* 1、void notify();唤醒在此对象监视器上等待的单个线程
* 2、void notifyAll();唤醒再次对象监视器上等待的所有线程
*/
public class NotifyAndNotifyAll {
public static void main(String[] args) {
//创建锁对象,保证唯一.为什么使用Object,因为Object是所有类的超类(多态思维)
Object object = new Object();
//创建一个乘客线程--消费者
new Thread() {
@Override
public void run() {
//设置死循环,让乘客一直等待使用座位
while (true) {
//客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
synchronized (object) {
System.out.println("乘客1: 告知司机需要座位的数量");
//调用wait方法,摒弃CPU的执行,进入到Waitting状态
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("座位已经生产完成了,乘客1:可以使用了");
System.out.println("----------------------------");
}
}
}
}.start();
//创建一个乘客线程--消费者
new Thread() {
@Override
public void run() {
//设置死循环,让乘客一直等待使用座位
while (true) {
//客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
synchronized (object) {
System.out.println("乘客2: 告知司机需要座位的数量");
//调用wait方法,摒弃CPU的执行,进入到Waitting状态
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("座位已经生产完成了,乘客2:可以使用了");
System.out.println("----------------------------");
}
}
}
}.start();
//创建一个司机线程--生产者
new Thread() {
@Override
public void run() {
//设置死循环,让司机一直生产座位
while (true) {
//花了3秒生产座位
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
synchronized (object) {
System.out.println("司机3秒之后生产完座位,通知乘客使用");
//座位生产完毕后,调用notify方法,唤醒乘客线程使用座位
//object.notify();//唤醒等待线程中的额其中一个
object.notifyAll();//唤醒所有在等待的线程
}
}
}
}.start();
}
}
notify()的结果:乘客1和乘客2随机被唤醒
notifyAll()的结果:乘客1和乘客2都被唤醒
使用线程的时候就创建线程,这样实现起来非常简便,但是会有一个问题。
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程都需要时间,
那么线程可否重复使用,也就是执行完任务之后,不被销毁,而是可以继续执行其他的任务。
在java中可以通过线程池来达到这样的效果
java中的线程池的顶级接口是java.util.concurrent.Executors,但是严格意义上讲,Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池的接口是:java.util.comcurrent.ExecutorService.
配置一个线城池是比较复杂的,尤其是对线程池的原理不是很清楚的情况下,很可能配置的线程池不是较好的,因此在java.util.concurrent.Executors线程工厂中提供了一个静态工厂,生成一些常用的线程池,
package threadpool;
/**
* 创建接口的实现类对象,重写run方法
*/
public class RunnableImpl implements Runnable{
@Override
public void run() {
//设置线程任务
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
}
}
package threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 1、java.uitl.concurrent.Executors:线程池的工厂类。用来生成线程池
* Executors类中的静态方法:
* static ExecutorService newFixedThreadPool(int nThreads)
* 创建一个可重用固定线程数的线程池
* 参数:创建线程池中包含的线程数量
* 返回值:返回的是ExecutorService接口的实现类对象,可以使用ExecutorService接口来接收
* 2、java.util.concurrent.ExecutorService线程池接口中:
* 1、从线程池中获取线程的方法,并调用start()方法,执行线程任务
* submit(Runnable task); 提交一个Runnable任务用于执行
* 2、关闭/销毁线程池的方法
* void shutdown();
* 3、线程池的使用步骤:
* 1、使用线程池工厂类Executors中提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
* 2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
* 3、调用ExecutorService接口中的submit方法,传递线程任务(Runnable接口实现类对象),开启线程,执行run方法
* 4、调用ExecutorService接口中的shutdown方法销毁线程,但是不建议使用
*/
public class ThreadPool {
public static void main(String[] args) {
//1、使用线程池工厂类Executors中提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
RunnableImpl runnable = new RunnableImpl();
//3、调用ExecutorService接口中的submit方法,传递线程任务(Runnable接口实现类对象),开启线程,执行run方法
//线程池一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续被调用
es.submit(runnable);
es.submit(runnable);
es.submit(runnable);
//4、调用ExecutorService接口中的shutdown方法销毁线程,但是不建议使用
es.shutdown();
//线程池被关闭,无法调用线程,就会报异常
//es.submit(runnable);
}
}
线程池关闭之后,再次调用线程执行任务,就会出现异常