一、线程概述
Windows操作系统是一个多任务的操作系统,所谓多任务就是指同时可以做不同的事情,比如,在使用Word软件编写文档的同时,还听着音乐,有时还在网上检索一些资料,这些就体现了操作系统的多任务的特点。每一个任务实际上是一个进程,多任务的操作系统是可以通过多进程实现的。
线程是一个程序内部的顺序控制流。线程和进程的区别:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销;线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程的切换开销小。
一个进程可以有多个线程,如视频中可以同时听声音、看图像、看弹幕等。很多线程都是模拟出来的,真正的线程是指多个CPU,即多核心。如果是模拟出来的多线程,即在一个CPU的情况下,在同一时间内,CPU只能执行一个代码,因为切换得很快,所以有同时执行的错觉。
多进程:在操作系统中能同时运行多个任务;多线程:在同一应用程序中有多个顺序流同时执行。
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
进程是一个动态过程,有它自身的产生、存在和消亡的过程——生命周期。
线程是独立的执行路径。在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程。main()称为主线程,为系统的入口,用于执行整个线程。在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度。
一个Java应用程序Java.exe,至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程。如果发生异常,将会影响主线程。
线程作为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,这就使得线程间通信更加简便、有效,但是多个线程操作共享的系统资源可能会带来安全隐患。
对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。线程会带来额外的开销,如CPU调度时间、并发控制开销。
二、多线程的串行和并行
多线程中有两个概念:串行、并行,了解这两个概念,才能更好地理解多线程。
1.串行
串行是相对单条线程来执行多个任务来说的,比如下载文件时,有文件A、B、C,在下载多个文件时,它是按照一定的顺序来进行下载的,只有将文件A下载下来,才能下载文件B,进而下载文件C,这三个文件在下载的时间上来说是不重叠的。
2.并行
并行是在下载多个文件时,开启多条线程。比如下载文件时,有文件A、B、C,在下载这三个文件时,可以同时下载,并行在时间上是重叠的。
了解完串行和并行之后,我们再来谈谈程序运行原理。
三、程序运行原理
1.分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间
2.抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
总结
大部分操作系统都支持多进程并发运行,比如我们可以一边听歌,一边操作Word,一边进行录屏,此时这些程序是在同时运行。CPU(中央处理器)使用抢占式调度模式在多个线程间进行高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程。其实,多线程程序并不能提高程序的运行速度,但能够提高程序的运行效率,让CPU的使用率更高。
四、创建线程
在Java中,实现线程的操作有两种,一种是通过继承Thread类来实现的,一种是通过实现Runnable接口来实现。
1、通过Thread子类实现
使用继承Thread类实现线程,实际上就是说每创建一个Thread类的子类就创建了一个线程。因为Java只支持单继承,因此使用继承Thread类来实现多线程时会存在一定的局限性。
(1)run()方法
在Thread类中,run()方法是每个继承Thread类的子类必须重写的方法,它是用来在子类中调用需要在线程上运行的代码,通常可以通过控制run()方法中的代码来控制线程的停止。
(2)start()方法
在Thread类中,start()是一个实例方法,通过创建Thread类的子类对象来调用该方法,start()方法用来调用run()方法,也就是启动线程的方法。
了解了上述两个线程中的方法,下面将传建一个线程类,并在main()方法中调用该线程。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: ExtendThreadDemo
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class ExtendThreadDemo extends Thread{
private int i=0;//i是实例变量,而不是局部变量
//通过继承Thread类来实现多线程时,由于每一个线程分别继承Thread类,导致实例变量i不能共享
//线程的调用是抢占式的,与优先级有关,如果优先级相同,则会随机选择一个进行执行
public void run(){
for (; i < 10; i++) {
//当继承Thread类时,可以直接使用this调用getName()方法,此时的this相当于Thread.currentThread()
System.out.println(this.getName()+" "+i);
}
}
//run()方法是线程执行体,需要在线程中执行的代码写入run()方法中
public static void main(String[] args) {
ExtendThreadDemo extendThreadDemo1=new ExtendThreadDemo();
ExtendThreadDemo extendThreadDemo2=new ExtendThreadDemo();
for(int j=0;j<10;j++){
System.out.println(Thread.currentThread().getName()+" "+j);
if(j==5){
extendThreadDemo1.start();
extendThreadDemo2.start();
}
}
}
}
//当程序开始执行后,程序会至少创建一个主线程(自动),该线程的执行体不是run()方法,而是main()方法
线程的执行过程是通过在main方法中调用run()方法,run()方法执行结束后,线程也结束。
2、通过Runnable接口实现
在实际应用中几乎都是通过实现Runnable接口来创建线程。与继承Thread类重写run()方法类似,实现Runnable接口也需要实现run()方法,用来调用在线程中执行的代码。Runnable接口不能直接创建其对象,用构造方法Thread(Runnable target)来创建线程对象。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: ImplementRunnableDemo
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class ImplementRunnableDemo implements Runnable{
private int i=0;
public void run(){
for (;i<20;i++) {
//当线程类实现Runnable接口时,只有通过Thread.currentThread()来获取当前线程对象
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
ImplementRunnableDemo implementRunnableDemo1=new ImplementRunnableDemo();
Thread thread1=new Thread(implementRunnableDemo1);
thread1.setName("线程1");
thread1.start();
Thread thread2=new Thread(implementRunnableDemo1);
thread2.setName("线程2");
thread2.start();
//线程1和线程2输出的i是连续的,可以说多线程共用了一个实例变量,因为多个线程都使用了同一个实现Runnable接口的实例对象
//如果资源没有加锁就会出现线程安全问题
//调用线程对象的start()方法来启动该线程
//Thread类的实例对象才是真正的线程对象
}
}
//实现Runnable接口的实例对象仅仅作为Thread实例的target,Runnable实现类的run()方法仅仅是线程执行体,而实际的线程对象依然是Thread实例对象,Thread实例负责执行其target中的run()方法
通过上面的代码可以看出,不论是使用Thread类还是实现Runnable接口,都要实现其中的run()方法来运行线程中要执行的代码,但是实现Runnable接口还要借助Thread类的构造方法才能创建线程。
3、Thread与Runnable的区别
(1)单重继承与实现多个接口
由于在Java中只支持单重继承,因此,在通过继承Thread类来创建线程时会受到一些限制,并且无法很好地实现数据共享。而通过实现Runnable接口来创建线程就不会受到单重继承的制约,可以很好地实现数据共享。
(2)实现数据共享时选用Runnable接口
由于通过Thread类创建线程时,每次都要创建一个新的线程的对象,无法直接实现线程的共享。而通过Runnable接口的方式创建线程时,可以为线程的构造方法中传递同一个接口实现类的对象。因此,可以很方便地实现线程的共享。
开发中优先选择实现Runnable接口的方式来创建多线程。原因:(1)实现的方式没有类的单继承的局限;(2)实现的方式更适合来处理多个线程共享数据的情况。联系:class Thread implements Runnable。相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()方法中。
4、实例演练:使用多线程实现多个闹钟叫醒服务
有时我们需要多个闹钟来叫醒,比如,使用手机闹钟、闹表等。下面介绍使用多线程的方式模拟多个闹钟的叫醒服务。
在本实例中,假设用1个手机闹钟和2个闹表来完成叫醒服务。启动闹钟后,闹钟不停地响起,直到每个闹钟都响了20次后停止。
完成上面的实例,需要如下2个类,一个是闹钟线程类,一个是测试闹钟类,即在main方法中生成3个闹钟线程,并完成调用。由于每个闹钟都需要响20次,这里既可以使用继承Thread类的方式也可以使用实现Runnable接口的方式来创建线程。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: AlarmClock
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class AlarmClock extends Thread {
private int i = 0;
public void run() {
for (; i < 20; i++) {
System.out.println(this.getName() + " " + i);
}
}
}
public class TestAlarmClock {
public static void main(String[] args) {
AlarmClock alarmClock=new AlarmClock();
alarmClock.setName("手机闹钟响了......");
alarmClock.start();
AlarmClock alarmClock1=new AlarmClock();
alarmClock1.setName("闹表1响了......");
alarmClock1.start();
AlarmClock alarmClock2=new AlarmClock();
alarmClock2.setName("闹表2响了......");
alarmClock2.start();
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: AlarmClock1
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class AlarmClock1 implements Runnable{
private int i=0;
public void run(){
for (;i<20;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestAlarmClock1
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestAlarmClock1 {
public static void main(String[] args) {
AlarmClock1 alarmClock1=new AlarmClock1();
Thread thread1=new Thread(alarmClock1);
thread1.setName("手机闹钟响了......");
thread1.start();
AlarmClock1 alarmClock2=new AlarmClock1();
Thread thread2=new Thread(alarmClock2);
thread2.setName("闹表1响了......");
thread2.start();
AlarmClock1 alarmClock3=new AlarmClock1();
Thread thread3=new Thread(alarmClock3);
thread3.setName("闹表2响了......");
thread3.start();
}
}
从上面的执行的效果可以看出,三个线程轮流执行,每一个闹钟都将执行20次闹铃服务。
五、线程的控制
前言
每个线程都有其生命周期,合理控制线程的生命周期是多线程程序编程的核心问题。
1、线程的生命周期
每一个程序都有自己的生命周期,比如,在main()方法中的代码执行完成后,程序结束。线程的生命周期不仅有开始和结束两个状态,还有就绪、阻塞、运行状态。
在创建线程实例后,需要通过start()方法启动线程,然后线程处于就绪状态,如果有多个线程同时执行,由于同一时间只能有一个线程,因此会出现阻塞状态。当线程处于阻塞状态后,就会出现暂时等待的情况,只有解除了等待才能再返回就绪状态,直到线程运行结束,线程才会进入中止状态。
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它具备了运行的条件,只是没分配到CPU资源
运行:当线程获取CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了自己的全部工作或线程被提前强制性地中止或出现异常导致结束
2、线程的优先级
在多线程的程序中是CPU通过优先级来选择线程的运行次序,在多线程的程序中,优先级高的线程,占用CPU的时间长。高优先级的线程要抢占低优先级线程CPU的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有高优先级的线程执行完以后,低优先级的线程才会被执行
Java的调度方法:同优先级线程组成先进先出队列(先到先服务),使用时间片策略;对高优先级,使用优先调度的抢占式策略。
线程的优先级:MIN_PRIORITY:1、NORM_PRIORITY:5、MAX_PRIORITY:10(1为最低优先级、5为默认优先级、10为最高优先级)
getPriority():返回线程优先级、setPriority(int newPriority):改变线程的优先级
线程创建时继承父线程的优先级
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: Priority
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class Priority implements Runnable{
enum DayOfWeek{
星期一,
星期二,
星期三,
星期四,
星期五,
星期六,
星期日
}
public void run(){
for (int i = 0; i < 7; i++) {
System.out.println(Thread.currentThread().getName()+" "+DayOfWeek.values()[i]);
}
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestPriority
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestPriority {
public static void main(String[] args) {
Priority priority1 = new Priority();
Thread thread1 = new Thread(priority1);
thread1.setPriority(Thread.MAX_PRIORITY);
thread1.setName("线程1");
thread1.start();
Priority priority2 = new Priority();
Thread thread2 = new Thread(priority2);
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.setName("线程2");
thread2.start();
}
}
从上面的执行效果可以看出,优先级高的线程大概率先执行并且占用CPU的时间会比较长。
3、线程的休眠与中断
在多线程程序中,会使用休眠或中断的方法来控制线程的生命周期。所谓休眠是指使线程的运行暂时停止,使用Thread类中的sleep()方法来实现,并且可以根据需要指定线程的休眠时间。如果需要让线程结束阻塞状态,可以使用stop()或interrupt()方法,由于stop()方法已经过时不能再使用,interrupt()方法是中断线程方法,但不是安全的方式。
interrupt()方法是改变中断状态,不会中断一个正在运行的线程,需要用户监护线程的状态并做处理。支持线程中断的方法(也就是线程中断后会抛出InterruptedException异常的方法)就是在监视线程的中断状态,一旦线程的“中断状态”被设置为中断状态,就会抛出中断异常。也就是说,给受阻线程发出一个中断标识,当受阻线程检测到该中断标识时,退出受阻状态。
如果线程被Object.wait()、Thread.sleep()或Thread.join()之一的方法阻塞时,此时调用该线程的interrupt()方法,那么此线程将会抛出一个InterruptedException异常(该线程必须事先预备好处理次异常),从而结束该线程的阻塞状态。如果线程没有被阻塞,调用interrupt()方法将不起作用,一旦遇到wait()、sleep()、join()方法,则立即抛出InterruptedException异常,从而结束该线程的阻塞状态。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: Interrupt
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class Interrupt {
public static void main(String[] args) {
Thread thread=new Thread(){
public void run(){
System.out.println("线程启动了");
try{
Thread.sleep(1000*2);
}catch(InterruptedException e){
System.out.println("将‘中断状态’设置为中断状态,从而使线程结束阻塞状态");
}
System.out.println("线程结束了");
}
};
thread.start();
try{
Thread.sleep(1000*2);
}catch(InterruptedException e){
e.printStackTrace();
}
thread.interrupt();//作用:在线程阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: Interrupt
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class Interrupt {
public static void main(String[] args) {
Thread thread=new Thread(){
public void run(){
System.out.println("线程启动了");
try{
Thread.sleep(1000*2);
}catch(InterruptedException e){
System.out.println("将‘中断状态’设置为中断状态,从而使线程结束阻塞状态");
}
System.out.println("线程结束了");
}
};
thread.start();
try{
Thread.sleep(1000*2);
}catch(InterruptedException e){
e.printStackTrace();
}
// thread.interrupt();//作用:在线程阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态
}
}
boolean isAlive():返回线程是否处于活动的状态,如果线程处于活动状态,则返回true,否则返回false
void interrupt():中断线程
static boolean isInterrupted():静态方法,用于返回线程是否被中断,如果线程被中断,则返回true,否则返回false
static void sleep(long millis):静态方法,用于让线程休眠指定时间,传入的休眠时间以毫秒为单位
static void yeild():静态方法,用于暂停正在运行的程序,让其他线程执行(让出CPU,该线程进入就绪队列等待调度)
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: NewYear
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class NewYear implements Runnable{
private int times=10;
public void run(){
while(times>0) {
System.out.println("还有" + times + "秒");
times--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新年快乐!!");
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestNewYear
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestNewYear {
public static void main(String[] args) {
NewYear newYear=new NewYear();
Thread thread=new Thread(newYear);
thread.start();
}
}
如果用户在控制台输入“q”则结束倒计时,代码如下:
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: NewYear1
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class NewYear1 implements Runnable{
public int times=10;
public boolean isFlag=true;
public void run(){
while(isFlag){
System.out.println("倒计时"+times+"秒");
times--;
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
if(times<=0){
isFlag=false;
System.out.println("新年快乐!!");
}
}
}
}
import java.util.Scanner;
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestNewYear1
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestNewYear1 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
String input="";//存取从控制台获取的字符串;
NewYear1 newYear=new NewYear1();
Thread thread=new Thread(newYear);
thread.start();
while(true){
System.out.println("输入q则结束倒计时");
input=sc.next();
if(input.equals("q")){
newYear.isFlag=false;
System.out.println("倒计时结束");
break;
}
}
}
}
通过上面的程序可以看出,该线程是通过isFlag变量标记来控制线程运行和停止状态的。
六、线程的同步和互斥
1、线程安全问题与死锁
在多线程程序中,共享资源时最容易出现安全问题。所谓共享资源就是指在程序中共享成员变量或文件,这时就容易出现不安全的问题。对于多线程的程序来说,每次访问成员变量的次数不同,那么运行的结果就会有很大的区别。
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己的同步资源,形成了线程的死锁。死锁问题不仅会在多线程程序中遇到,在日常生活中也经常会遇到这种问题。比如,去商场买衣服,准备交款时发现衣服只有一件,必须要从其他商场进行调货,你必须等到货到了才会付款,而商家认为你应该先付款再将衣服从其他商场中调过来,实际上这也是一种死锁现象,即商家需要等待你付款后再调货,而你需要先调货再付款。
以下两种情况会出现死锁现象:(1)相互排斥:当一个线程独占资源时,其他线程就不能使用该资源,如果这个线程永久占用资源,其他线程将永远不能使用该资源,将一直处于等待状态,这样就会产生死锁现象。例如,周末你去动物园,发现动物园只有一个售票口,但是有一个游客一直在售票口处与售票人员进行争执,导致其他人无法进行购票,等待的人越来越多。(2)循环等待:循环等待就是相互等待,比如,有三个线程A、B、C,如果A线程需要等待B线程,B线程需要等待C线程,而C线程又要等待A线程,这样就会出现循环等待的现象。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
避免死锁现象就是要在多线程中能够有效地控制多线程中每一个线程的生命周期,保证在同一时间内只能有一个线程访问同一资源,这就需要合理地使用线程同步的方法。
2、线程同步
在Java语言中,控制线程同步的方法是通过锁定资源的方式来实现的,就是可以让多个线程共享资源,完成资源的交替使用。
实现线程同步有同步方法和同步代码块两种方式。
(1)同步方法
同步方法的定义如下:
synchronized void method(){
}
通过在方法前面加上synchronized关键字,代表锁定的方法只能同时被一个线程调用,其他线程必须处于等待状态。另外,synchronized关键字修饰的方法中,调用的变量必须是使用private来修饰的,这样就能保证变量也被锁定。
(2)同步代码块
同步代码块的定义如下:
synchronized(Object){
//要同步的语句
}
通过同步代码块可以锁定代码块中的内容,而不用锁定整个方法中的内容。在run()方法中也可以使用同步块的方式来锁定需要同步的代码。
synchronized(同步监视器),同步监视器俗称锁,任何一个类的对象,都可以充当锁,并且多个线程要共用一把锁。在实现Runnable接口创建多线程的方式中,我们考虑使用this充当同步监视器;在继承Thread类创建多线程的方式中,慎用this充当同步监视器。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: SynchronizedMethod1
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class SynchronizedMethod1 implements Runnable{
private int num=0;
public boolean isFlag=true;
public void run(){
while(isFlag){
print();
}
}
public synchronized void print(){
if(num<10){
System.out.println(Thread.currentThread().getName()+" "+num);
num++;
}else{
isFlag=false;
}
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestSynchronizdMethod
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestSynchronizdMethod {
public static void main(String[] args) {
SynchronizedMethod1 synchronizedMethod=new SynchronizedMethod1();
Thread thread1=new Thread(synchronizedMethod);
thread1.setName("线程1");
thread1.start();
Thread thread2=new Thread(synchronizedMethod);
thread2.setName("线程2");
thread2.start();
}
}
如果需要控制每个线程在输出时是交替的或者控制线程的运行次序,那么就需要使用线程间的通信来完成。
将上面的实例,使用代码块的方式在run()方法中完成,代码如下:
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: SynchronizedLock
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class SynchronizedLock implements Runnable{
private int num=0;
public boolean isFlag=true;
public void run(){
while(isFlag){
synchronized(this){
if(num<100){
System.out.println(Thread.currentThread().getName()+" "+num);
num++;
}else{
isFlag=false;
}
}
}
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestSynchronizedLock
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestSynchronizedLock {
public static void main(String[] args) {
SynchronizedLock synchronizedLock=new SynchronizedLock();
Thread thread1=new Thread(synchronizedLock);
thread1.setName("线程1");
thread1.start();
Thread thread2=new Thread(synchronizedLock);
thread2.setName("线程2");
thread2.start();
}
}
一般情况下,如果整个方法中所使用的资源都需要同步,那就使用同步方法;如果在一个方法中大部分的代码不需要同步,那么就可以直接在方法中使用同步代码块的方式来实现资源同步。
(3)线程间的通信
所谓线程间的通信是指在多线程的程序中,线程之间可以通过等待和唤醒的方法相互通信、交替执行。同时,线程间的通信方法也可以有效地避免线程的阻塞状态。设置线程的等待方法是wait()方法,唤醒线程的方法是notify()方法。wait()方法通常要与notify()方法一同使用,否则线程将处于相互等待的状态。notify()方法可以唤醒正在等待的一个线程,如果有多个线程同时等待,那么使用notify()方法唤醒线程时,唤醒优先级高的线程。
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait()的线程,如果有多个线程被wait(),就唤醒优先级高的线程。
notifyAll():一旦执行此方法,就会唤醒所有被wait()的方法。
wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中,wait()、notify()、notifyAll()方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会报IllegalMonitorStateException异常。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: WaitNotify
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class WaitNotify implements Runnable {
private int num = 0;
public boolean isFlag = true;
public void run() {
print();
}
public synchronized void print() {
while (isFlag) {
if (num < 10) {
System.out.println(Thread.currentThread().getName() + " " + num);
num++;
notify();//通知另一个线程执行
try {
wait(1000);//当前线程处于等待状态,等待1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
isFlag = false;
}
}
}
}
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestWaitNotify
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestWaitNotify {
public static void main(String[] args) {
WaitNotify waitNotify=new WaitNotify();
Thread thread1=new Thread(waitNotify,"线程1");
thread1.start();
Thread thread2=new Thread(waitNotify,"线程2");
thread2.start();
}
}
从上面的执行可以看出,通过wait()和notify()方法控制两个线程之间交替执行,每个线程都只打印了5个数字。但是,在多线程的程序中,需要先唤醒其他的线程,然后再将该线程置为等待状态,否则就会出现相互等待的现象。另外使用wait()方法时,还可以指定等待的时间。
总结
实例:使用多线程实现打印ABC
创建3个线程,交替输出ABCABCABCAB…。根据题目要求,需要一个线程负责输出A,一个线程输出B、一个线程输出C。
/**
* @author MNH
* @version 1.0
* @project Name: 数据结构
* @file Name: TestPrintABC
* @desc 功能描述
* @by IDE: IntelliJ IDEA
*/
public class TestPrintABC {
public static void main(String[] args) {
PrintABC abc=new PrintABC();
PrintA a=new PrintA(abc);
Thread thread1=new Thread(a);
thread1.start();
PrintB b=new PrintB(abc);
Thread thread2=new Thread(b);
thread2.start();
PrintC c=new PrintC(abc);
Thread thread3=new Thread(c);
thread3.start();
}
}
class PrintABC{
public boolean isFlagA=true;
public boolean isFlagB=false;
public boolean isFlagC=false;
public synchronized void printA(){
if(isFlagA){
System.out.print("A");
notifyAll();
isFlagA=false;
isFlagB=true;
isFlagC=false;
try{
wait(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public synchronized void printB(){
if(isFlagB){
System.out.print("B");
notifyAll();
isFlagA=false;
isFlagB=false;
isFlagC=true;
try{
wait(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public synchronized void printC(){
if(isFlagC){
System.out.print("C");
notifyAll();
isFlagA=true;
isFlagB=false;
isFlagC=false;
try{
wait(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
class PrintA implements Runnable{
PrintABC abc;
public PrintA(PrintABC abc){
this.abc=abc;
}
public void run(){
while(true){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
abc.printA();
}
}
}
class PrintB implements Runnable{
PrintABC abc;
public PrintB(PrintABC abc){
this.abc=abc;
}
public void run(){
while(true){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
abc.printB();
}
}
}
class PrintC implements Runnable{
PrintABC abc;
public PrintC(PrintABC abc){
this.abc=abc;
}
public void run(){
while(true){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
abc.printC();
}
}
}
从上面的执行结果可以看出,3个线程分别负责打印A、B、C,最后出现一个交替打印3个字母的效果。