版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
本章学习目标:●
掌握线程创建的过程●
掌握线程的生命周期●掌了解线程同步机制以及线程通信●
了解线程的优先级●
掌握线程的同步与死锁第十二章多线程第1节part线程概述
线程(Thread)在多任务处理应用程序中起着至关重要的作用。之前所接触的应用程序都是采用单线程处理模式。单线程在某些功能方面会受到限制,无法同时处理多个互不干扰的任务,只有一个顺序执行流;而多线程是同时有多个线程并发执行,同时完成多个任务,具有多个顺序执行流,且执行流之间互不干扰。Java语言多多线程提供了非常优秀的支持,在程序中可以通过简便的方式创建多线程。线程概述本节概述
在操作系统中,每个独立运行的程序就是一个进程(Process),当一个程序进入内存运行时,即变成一个进程。进程是操作系统进行资源分配和调度的一个独立单位,是具有独立功能且处于运行过程中的程序。在Windows操作系统中,右击任务栏,选择“启动任务管理器”菜单命令,可以打开“Windows任务管理器”窗口,该窗口中的“进程”选项卡中显示系统当前正在运行的进程,如图12.1所示。12.1.1线程和进程线程和进程
进程具有如下三个特征:
(1)独立性:进程是操作系统中独立存在的实体,拥有自己独立的资源,每个进行都拥有自己私有的地址空间,其他进程不可以直接访问该地址空间,除非进程本身允许的情况下才能进行访问。
(2)动态性:程序只是一个静态的指令集合,只有当程序进入内存运行时,才变成一个进程。进程是一个正在内存中运行的、动态的指令集合,进程具有自己的生命周期和各种不同状态。
(3)并发性:多个进程可以在单个处理器上并发执行,多个进程之间互不影响。12.1.1线程和进程
目前的操作系统都支持多线程的并发,但在具体的实现细节上会采用不同的策略。对于一个CPU而言,在某一时间点只能执行一个进程,CPU会不断在多个进程之间来回轮换执行。并发性(concurrency)和并行性(parallel)是两个相似但又不同的概念:并发是指多个事件在同一时间间隔内发生,其实质是在一个CPU上同时运行多个进程,CPU要在多个进程之间切换。并发不是真正的同时发生,而是对有限物理资源进行共享以便提高效率。并行是指多个事件在同一时刻发生,其实质是多个进程同一时刻可在不同的CPU上同时执行,每个CPU运行一个进程。12.1.1线程和进程
并发就像一个人喂两个孩子吃饭,轮换着每人喂一口,表面上两个孩子都在吃饭;而并行就是两个人喂两个孩子吃饭,两个孩子也同时在吃饭。并发和并行之间的区别如图12.2所示。12.1.1线程和进程
线程是进程的组成部分,一个线程必须在一个进程之内,而一个进程可以拥有多个线程,一个进程中至少有一个线程。线程是最小的处理单位,线程可以拥有自己的堆栈、计数器和局部变量,当不能拥有系统资源,多个线程共享其所在进程的系统资源。线程可以完成一定的任务,使用多线程可以在一个程序中同时完成多个任务,在更低的层次中引入多任务处理。
多线程在多CPU的计算机中可以实现真正物理上的同时执行;而对于单CPU的计算机实现的只是逻辑上的同时执行,在每个时刻,真正执行的只有一个线程,由操作系统进行线程管理调度,但由于CPU的速度很快,让人感到像是多个线程在同时执行。12.1.1线程和进程
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。因此,线程也被称作轻量级进程。多进程与多线程是多任务的两种类型,两者之间的主要区别如下:
(1)进程之间的数据块是相互独立的,彼此互不影响,进程之间需要通过信号、管道等进行交互。
(2)多线程之间的数据块可以共享,一个进程中的多个线程可以共享程序段、数据段等资源。多线程比多进程更便于资源共享,同时Java提供的同步机制还可以解决线程之间的数据完整性问题,使得多线程设计更易发挥作用。多线程编程的优点如下:
(1)多线程之间共享内存,节约系统资源成本;
(2)充分利用CPU,执行并发任务效率高;
(3)Java内置多线程功能支持,简化编程模型
(4)GUI应用通过启动单独线程收集用户界面事件,简化异步事件处理,使GUI界面的交互性更好。12.1.1线程和进程Java线程模型提供线程所必需的功能支持,基本的Java线程模型有Thread类、Runnable接口、Callable接口和Future接口等,这些线程模型都是面向对象的。Thread类将线程所必需的功能进行封装,其常用的方法如表12-1所示。12.1.2Java线程模型Java线程模型Thread类的run()方法是线程中最重要的方法,该方法用于执行线程要完成的任务;当创建一个线程时,要完成自己的任务,则需要重写run()方法。此外,Thread类还提供了start()方法,该方法用于负责线程的启动;当调用start()方法成功地启动线程后,系统会自动调用Thread类的run()方法来执行线程。因此,任何继承Thread类的线程都可以通过start()方法来启动。Runnable接口用于标识某个Java类可否作为线程类,该接口只有一个抽象方法run(),即线程中最重要的执行体,用于执行线程中所要完成的任务。Runnable接口定义在java.lang包中,定义代码如下所示。packagejava.lang;publicinterfaceRunnable{ publicabstractvoidrun();}12.1.2Java线程模型Callable接口是Java5新增的接口,该接口中提供一个call()方法作为线程的执行体。call()方法比run()方法功能更强大,call()方法可以有返回值,也可以声明抛出异常。Callable接口定义在java.util.concurrent包中,定义代码如下所示。packagejava.util.concurrent;publicinterfaceCallable<V>{ Vcall()throwsException;}12.1.2Java线程模型Future接口用来接收Callable接口中call()方法的返回值。Future接口提供一些方法用于控制与其关联的Callable任务。Future接口提供的方法如表12-2所示。12.1.2Java线程模型Callable接口有泛型限制,该接口中的泛型形参类型与call()方法返回值的类型相同;而且Callable接口是函数式接口,因此从Java8开始可以使用Lambda表达式创建Callable对象。
每个能够独立运行的程序就是一个进程,每个进程至少包含一个线程,即主线程。在Java语言中,每个能够独立运行的Java程序都至少有一个主线程,且在程序启动时,JVM会自动创建一个主线程来执行该程序中的main()方法。因此,主线程有以下两个特点:
(1)一个进程肯定包含一个主线程
(2)主线程用来执行main()方法
下述程序在main()方法中,调用Thread类的静态方法currentThread()来获取主线程,代码如下所示。12.1.3主线程主线程【代码12.1】MainThreadExample.javapackagecom;publicclassMainThreadExample{ publicstaticvoidmain(String[]args){ //调用Thread类的currentThread()获取当前线程 Threadt=Thread.currentThread(); //设置线程名 t.setName("MyThread"); System.out.println("主线程是:"+t); System.out.println("线程名:"+t.getName()); System.out.println("线程ID:"+t.getId()); }}12.1.3主线程
上述代码中,通过Thread.currentThread()静态方法来获取当前线程对象,由于是在main()方法中,所以获取的线程是主线程。调用setName()方法可以设置线程名,调用getId()方法可以获取线程的Id号,调用getName()方法可以获取线程的名字。
程序运行结果如下:
主线程是:Thread[MyThread,5,main]
线程名:MyThread
线程ID:112.1.3主线程第2节part线程的创建和启动
基于Java线程模型,创建线程的方式有三种:
(1)第一种方式是继承Thread类,重写Thread类中的run()方法,直接创建线程。
(2)第二种方式是实现Runnable接口,再通过Thread类和Runnable的实现类间接创建一个线程。
(3)第三种方式是使用Callable接口或Future接口间接创建线程。
上述三种方式从本质上是一致的,最终都是通过Thread类来建立线程。提供Runnable、Callable和Future接口模型是由于Java不支持多继承,如果一个线程类继承了Thread类,则不能再继承其他的类,因此可以通过实现接口的方式间接创建线程。
采用Runnable、Callable和Future接口的方式创建线程时,线程类还可以继承其他类,且多个线程之间可以共享一个target目标对象,适合多个相同线程处理同一个资源的情况,从而可以将CPU、代码和数据分开,形成清晰的数据模型。线程的启动和创建本节概述12.2.1继承Thread类
通过继承Thread类来创建并启动线程的步骤如下:
(1)定义一个子类继承Thread类,并重写run()方法。
(2)创建子类的实例,即实例化线程对象。
(3)调用线程对象的start()方法启动该线程。Thread类的start()方法将调用run()方法,该方法用于启动线程并运行。因此start()方法不能多次调用,当多次调用td.start()方法时会抛出一个IllegalThreadStateException异常。下述案例示例通过继承Thread类来创建并启动线程的步骤,代码如下所示。继承Thread类继承Thread类【代码12.2】ThreadExample.javapackagecom;//继承Thread类publicclassThreadExampleextendsThread{ //重写run()方法 publicvoidrun(){ for(inti=0;i<10;i++){ //继承Thread类时,直接使用this即可获取当前线程对象 //调用getName()方法返回当前线程的名字 System.out.println(this.getName()+":"+i); } }12.2.1继承Thread类 publicstaticvoidmain(String[]args){ //创建线程对象 ThreadExampletd=newThreadExample(); //调用start()方法启动线程 td.start(); //主线程任务 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()获取主线程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.1继承Thread类
因为线程在CPU中的执行是由操作系统所控制,执行次序是不确定的,除非使用同步机制强制按特定的顺序执行,所以程序代码运行的结果会因调度次序不同而不同。程序执行结果可能如下:main:1101Thread-0:1main:1102Thread-0:2main:1103……..
在创建td线程对象时并未指定该线程的名字,因此所输出的线程名是系统的默认值“Thread-0”。对于输出结果,不同机器所执行的结果可能不同,在同一机器上多次运行同一个程序也可能生成不同结果。12.2.112.2.2实现Runable接口
创建线程的第二种方式是实现Runnable接口。Runnable接口中只有一个run()方法,一个类实现Runnable接口后,并不代表该类是个“线程”类,不能直接启动线程,必须通过Thread类的实例来创建并启动线程。通过Runnable接口创建并启动线程的步骤如下:
(1)定义一个类实现Runnable接口,并实现该接口中的run()方法;
(2)创建一个Thread类的实例,将Runnable接口的实现类所创建的对象作为参数传入Thread类的构造方法中;
(3)调用Thread对象的start()方法启动该线程。
下述案例示例通过实现Runnable接口创建并启动线程的步骤,代码如下所示。实现Runable接口实现Runable接口【代码12.3】RunnableExamble.javapackagecom;//实现Runnable接口publicclassRunnableExambleimplementsRunnable{ //重写run()方法 publicvoidrun(){ //获取当前线程的名字 for(inti=0;i<10;i++) //实现Runnable接口时,只能使用Thread.currentThread()获取当前线程对象 //再调用getName()方法返回当前线程的名字 System.out.println(Thread.currentThread().getName()+":"+i); }12.2.2实现Runable接口 publicstaticvoidmain(String[]args){ //创建一个Thread类的实例,其参数是RunnableExamble类的对象 Threadtd=newThread(newRunnableExamble()); //调用start()方法启动线程 td.start(); //主线程任务 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()获取主线程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.2实现Runable接口
上述代码定义了一个RunnableExamble类,该类实现了Runnable接口,并实现run()方法,这样的类可以称为线程任务类。直接调用Thread类或Runnable接口所创建的对象的run()方法是无法启动线程的,必须通过Thread的start()方法才能启动线程。程序执行的结果可能如下:main:1100Thread-0:0Thread-0:1Thread-0:2……..12.2.212.2.3使用Callable和Future接口
创建线程的第三种方式是使用Callable和Future接口。Callable接口提供一个call()方法作为线程的执行体,该方法的返回值使用Future接口来代表。从Java5开始,为Future接口提供一个FutureTask实现类,该类同时实现了Future和Runnable两个接口,因此可以作为Thread类的target参数。使用Callable和Future接口的最大优势在于可以在线程执行完成之后获得执行结果。
使用Callable和Future接口创建并启动线程的步骤如下:
(1)创建Callable接口的实现类,并实现call()方法,该方法将作为线程的执行体,并具有返回值;然后创建Callable实现类的实例。
(2)使用FutureTask类来包装Callable对象,在FutureTask对象中封装了Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target,创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
下述案例示例通过Callable和Future接口创建并启动线程的步骤,代码如下所示。使用Callable和Future接口使用Callable和Future接口【代码12.4】CallableFutureExample.javapackagecom;importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;//创建Callable接口的实现类classTaskimplementsCallable<Integer>{ //实现call()方法,作为线程执行体 publicIntegercall()throwsException{ inti=0; for(;i<10;i++) System.out.println(Thread.currentThread().getName()+":"+i); //call()方法可以有返回值 returni; }}12.2.3使用Callable和Future接口publicclassCallableFutureExample{ publicstaticvoidmain(String[]args){ //使用FutureTask类包装Callable实现类的实例 FutureTask<Integer>task=newFutureTask<>(newTask()); //创建线程,使用FutureTask对象task作为Thread对象的target, //并调用start()方法启动线程 Threadtd=newThread(task,"子线程"); td.start(); //调用FutureTask对象task的get()方法获取子线程执行结束后的返回值 try{ System.out.println("子线程返回值:"+task.get()); }catch(Exceptione){ e.printStackTrace(); } //主线程任务 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()获取主线程名字 System.out.println(Thread.currentThread().getName()+":"+i); }}12.2.3使用Callable和Future接口
上述代码先定义一个Task类,该类实现Callable接口并重写call()方法,call()的返回值为整型,因此Callable接口中对应的泛型限制为Integer,即Callable<Integer>。在main()方法中,先创建FutureTask<Integer>类的对象task,该对象包装Task类;再创建Thread对象并启动线程;最后调用FutureTask对象task的get()方法获取子线程执行结束后的返回值。整个程序所实现的功能与前两种方式一样,只是增加了子线程返回值。
程序运行结果如下:
子线程:0
子线程:1
子线程:2……..
子线程返回值:10main:1100main:1101……12.2.3使用Callable和Future接口
从Java8开始,可以直接使用Lambda表达式创建Callable对象,下述案例示例通过Lambda表达式创建Callable对象,代码如下所示。【代码12.5】LambdaCallableExample.javapackagecom;importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;publicclassLambdaCallableExample{publicstaticvoidmain(String[]args){ //使用Lambda表达式创建Callable<Integer>对象 //使用FutureTask类包装Callable对象 FutureTask<Integer>task=newFutureTask<>(
(Callable<Integer>)()->{ inti=0; for(;i<10;i++) System.out.println(Thread.currentThread().getName()+":"+i); //call()方法可以有返回值 returni; });12.2.3使用Callable和Future接口 //创建线程,使用FutureTask对象task作为Thread对象的target, //并调用start()方法启动线程 Threadtd=newThread(task,"子线程"); td.start(); //调用FutureTask对象task的get()方法获取子线程执行结束后的返回值 try{ System.out.println("子线程返回值:"+task.get()); }catch(Exceptione){ e.printStackTrace(); } //主线程任务 for(inti=1100;i<1110;i++) //使用Thread.currentThread().getName()获取主线程名字 System.out.println(Thread.currentThread().getName()+":"+i);}}
上述代码加粗部分就是Lambda表达式,可以直接使用Lambda表达式创建Callable对象,而无须先创建Callable实现类,但Lambda表达式必须在jdk1.8版本后才可以运行。
在JavaAPI中,定义的FutureTask类实际上直接实现RunnableFuture接口,而RunnableFuture接口继承Runnable和Future两个接口,因此FutureTask类即实现了Runnable接口,又实现了Future接口。12.2.3第3节part线程的生命周期
线程具有生命周期,当线程被创建并启动后,不会立即进入执行状态,也不会一直处于执行状态。在线程的生命周期中,要经过5种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。线程状态之间的转换如图12-3所示。线程的生命周期本节介绍12.3.1新建和就绪状态
当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时与其他Java对象一样,仅由JVM为其分配内存并初始化。新建状态的线程没有表现出任何动态特征,程序也不会执行线程的执行体。当线程对象调用start()方法之后,线程就处于就绪状态,相当于“等待执行”。此时,调度程序就可以把CPU分配给该线程,JVM会为线程创建方法调用栈和程序计数器。处于就绪状态的线程并没有开始运行,只是表示该线程准备就绪等待执行。
注意只能对新建状态的线程调用start()方法,即new完一个线程后,只能调用一次start()方法,否则将引发IllegalThreadStateException异常。
下述案例示例了新建线程重复调用start()方法引发异常,代码如下所示。新建和就绪状态新建和就绪状态【代码12.6】IllegalThreadExample.javapackagecom;publicclassIllegalThreadExample{ publicstaticvoidmain(String[]args){ //创建线程 Threadt=newThread(newRunnable(){ publicvoidrun(){ for(inti=0;i<10;i++) System.out.print(i+""); } }); t.start(); t.start(); }}
上述代码三次调用start()方法,多次启动线程,因此会引发IllegalThreadStateException异常。运行结果可能如下所示:Exceptioninthread"main"java.lang.IllegalThreadStateException0123456789 atjava.lang.Thread.start(UnknownSource) atcom.IllegalThreadExample.main(IllegalThreadExample.java:12)12.3.112.3.2运行和阻塞状态
处于就绪状态的线程获得CPU后,开始执行run()方法的线程执行体,此时该线程处于运行状态。如果计算机的CPU是单核的,则在任何时刻只有一个线程处于运行状态。一个线程开始运行后,不可能一直处于运行状态。线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。
目前UNIX系统采用的是时间片算法策略,Windows系统采用的则是抢占式策略,另外一种小型设备(手机)则可能采用协作式调度策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。运行和阻塞状态运行和阻塞状态当线程出现以下情况时,会进入阻塞状态:(1)调用sleep()方法,主动放弃所占用的处理器资源;(2)调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;(4)执行条件还未满足,调用wait()方法使线程进入等待状态,等待其他线程的通知;(5)程序调用了线程的suspend()方法将该线程挂起,但该方法容易导致死锁,因此应该尽量避免使用。12.3.2运行和阻塞状态
正在执行的线程被阻塞之后,其他线程就可以获得执行的机会,被阻塞的线程会在合适的时机重新进入就绪状态,等待线程调度器再次调度。
当线程出现如下几种情况时,线程可以解除阻塞进入就绪状态:
(1)调用sleep()方法的线程经过了指定的时间;
(2)线程调用的阻塞式IO方法以经返回;
(3)线程成功地获得了同步监视器;
(4)线程处于等待状态,其他线程调用notify()或notifyAll()方法发出了一个通知时,则线程回到就绪状态;
(5)处于挂起状态的线程被调用了resume()恢复方法。12.3.2运行和阻塞状态
在线程运行的过程中,可以通过sleep()方法使线程暂时停止运行,进入休眠状态。在使用sleep()方法时需要注意以下两点:
(1)sleep()方法的参数是以毫秒为基本单位,例如sleep(2000)则休眠2秒钟;
(2)sleep()方法声明了InterruptedException异常,因此调用sleep()方法时要么放在try…catch语句中捕获该异常并处理,要么在方法后使用throws显式声明抛出该异常。
可以通过Thread类的isAlive()方法来判断线程是否处于运行状态。当线程处于就绪、运行和阻塞三种状态时,isAlive()方法的返回值为true;当线程处于新建、死亡两种状态时,isAlive()方法的返回值为false。
下述案例示例了线程的创建、运行和死亡三个状态,代码如下所示。12.3.2运行和阻塞状态【代码12.7】ThreadLifeExample.javapackagecom;publicclassThreadLifeExampleextendsThread{ publicvoidrun(){ intsum=0; for(inti=0;i<=100;i++) sum+=i; System.out.println("sum="+sum); } publicstaticvoidmain(String[]args)throwsInterruptedException{ ThreadLifeExampletle=newThreadLifeExample(); System.out.println("新建状态isAlive():"+tle.isAlive()); tle.start(); System.out.println("运行状态isAlive():"+tle.isAlive()); Thread.sleep(2000); System.out.println("线程结束isAlive():"+tle.isAlive()); }}12.3.2运行和阻塞状态
程序运行结果如下:
新建状态isAlive():false
运行状态isAlive():truesum=5050
线程结束isAlive():false
注意:线程调用wait()方法进入等待状态后,需其他线程调用notify()或notifyAll()方法发出通知才能进入就绪状态。使用suspend()和resume()方法可以挂起和唤醒线程,但这两个方法可能会导致不安全因素。如果对某个线程调用interrupt()方法发出中断请求,则该线程会根据线程状态抛出InterruptedException异常,对异常进行处理时可以再次调度该线程。12.3.2死亡状态线程结束后就处于死亡状态,结束线程有以下三种方式:(1)线程执行完成run()或call()方法,线程正常结束;(2)线程抛出一个未捕获的Exception或Error;(3)调用stop()方法直接停止线程,该方法容易导致死锁,通常不推荐使用。死亡状态12.3.2死亡状态
主线程结束时,其他子线程不受任何影响,并不会随主线程的结束而结束。一旦子线程启动起来,子线程就拥有和主线程相同的地位,子线程不会受主线程的影响。
为了测试某个线程是否死亡,可以通过线程对象的isAlive()方法来获得线程状态,当方法返回值为false时,线程处于死亡或新建状态。不要试图对一个已经死亡的线程调用start()方法使其重新启动,线程死亡就是死亡,该线程不可再次作为线程执行。Thread类中的join()方法可以让一个线程等待另一个线程完成后,继续执行原线程中的任务。当在某个程序执行流中调用其他线程的join()方法时,当前线程将被阻塞,直到另一个线程执行完为止。join()方法通常由使用线程的程序调用,当其他线程都执行结束后,再调用主线程进一步操作。
下述案例示例了join()方法的使用,代码如下所示。12.3.2死亡状态【代码12.8】JoinExample.javapackagecom;classJoinThreadextendsThread{ publicJoinThread(){ super(); } publicJoinThread(Stringstr){ super(str); } publicvoidrun(){ for(inti=0;i<10;i++) System.out.println(this.getName()+":"+i); }}12.3.2死亡状态publicclassJoinExample{ publicstaticvoidmain(String[]args){ //创建子线程 JoinThreadt1=newJoinThread("被Join的子线程"); //启动子线程 t1.start(); //等待子线程执行完毕 try{ t1.join(); }catch(InterruptedExceptione){ //TODOAuto-generatedcatchblock e.printStackTrace(); } //输出主线程名 System.out.println("主线程名为:"+Thread.currentThread().getName()); //子线程已经处于死亡状态,其isAlive()方法返回值为false System.out.println("子线程死亡状态isAlive():"+t1.isAlive()); //再次启动子线程,抛出异常 t1.start(); }}12.3.2死亡状态
上述代码中开始调用了线程的join()方法,最后又对死亡状态的线程再次调用start()方法,运行结果如下所示:…..
被Join的子线程:8
被Join的子线程:9
主线程名为:main
子线程死亡状态isAlive():falseExceptioninthread"main"java.lang.IllegalThreadStateException atjava.lang.Thread.start(UnknownSource) atcom.JoinExample.main(JoinExample.java:33)
在上述代码中,注销掉join()方法的调用和对死亡状态线程的start()方法的再次调用,运行结果可能如下:
主线程名为:main
被Join的子线程:0
被Join的子线程:1
子线程死亡状态isAlive():true
被Join的子线程:2
被Join的子线程:3……12.3.2第4节part线程的优先级
每个线程执行时都具有一定的优先级,线程的优先级代表该线程的重要程度。当有多个线程同时处于可执行状态并等待获得CPU处理器时,系统将根据各个线程的优先级来调度各线程,优先级越高的线程获得CPU时间的机会越多,而优先级低的线程则获得较少的执行机会。
每个线程都有默认的优先级,其优先级都与创建该线程的父线程的优先级相同。在默认情况下,主线程具有普通优先级,由主线程创建的子线程也具有普通优先级。Thread类提供三个静态常量来标识线程的优先级:
(1)MAX_PRIORITY:最高优先级,其值为10;
(2)NORM_PRIORITY:普通优先级,其值为5;
(3)MIN_PRIORITY:最低优先级,其值为1。线程的优先级线程的优先级Thread类提供了setPriority()方法来对线程的优先级进行设置,而getPriority()方法来获取线程的优先级。setPriority()方法的参数是一个整数(1~10),也可以使用Thread类提供的三个优先级静态常量。
线程的优先级高度依赖于操作系统,并不是所有的操作系统都支持Java的10个优先级,例如Windows2000仅提供7个优先级。因此,尽量避免直接使用整数给线程指定优先级,提倡使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三个优先级静态常量。另外,优先级并不能保证线程的执行次序,因此应避免使用线程优先级作为构建任务执行顺序的标准。
下述案例示例了线程优先级的设置及使用,代码如下所示。线程的优先级【代码12.9】PriorityExample.javapackagecom;classMyPriorityThreadextendsThread{ publicMyPriorityThread(){ super(); } publicMyPriorityThread(Stringname){ super(name); } publicvoidrun(){ for(inti=0;i<10;i++){ System.out.println(this.getName()+",其优先级是:"+this.getPriority() +",循环变量的值为:"+i); } }}线程的优先级publicclassPriorityExample{ publicstaticvoidmain(String[]args){ //输出主线程的优先级 System.out.println("主线程的优先级:"+Thread.currentThread().getPriority()); //创建子线程,并设置不同优先级 MyPriorityThreadt1=newMyPriorityThread("高级"); t1.setPriority(Thread.MAX_PRIORITY); MyPriorityThreadt2=newMyPriorityThread("普通"); t2.setPriority(Thread.NORM_PRIORITY); MyPriorityThreadt3=newMyPriorityThread("低级"); t3.setPriority(Thread.MIN_PRIORITY); MyPriorityThreadt4=newMyPriorityThread("指定值级"); t4.setPriority(4); //启动所有子线程 t1.start(); t2.start(); t3.start(); t4.start(); }}线程的优先级程序运行执行结果可能如下:主线程的优先级:5普通,其优先级是:5,循环变量的值为:0高级,其优先级是:10,循环变量的值为:0高级,其优先级是:10,循环变量的值为:1高级,其优先级是:10,循环变量的值为:2高级,其优先级是:10,循环变量的值为:3普通,其优先级是:5,循环变量的值为:1高级,其优先级是:10,循环变量的值为:4指定值,其优先级是:4,循环变量的值为:0…….通过运行结果可以看出,优先级越高的线程提前获得执行机会就越多。
线程的优先级第5节part线程的同步
多线程访问同一资源数据时,很容易出现线程安全问题。以多窗口出售车票为例,一旦多线程并发访问,就可能出现问题,造成一票多售的现象。在Java中,提供了线程同步的概念以保证某个资源在某一时刻只能由一个线程访问,以此保证共享数据的一致性。Java使用监控器(也称对象锁)实现同步。每个对象都有一个监控器,使用监控器可以保证一次只允许一个线程执行对象的同步语句。即在对象的同步语句执行完毕前,其他试图执行当前对象的同步语句的线程都将处于阻塞状态,只有线程在当前对象的同步语句执行完毕后,监控器才会释放对象锁,并让优先级最高的阻塞线程处理同步语句。
线程同步通常采用三种方式:同步代码块、同步方法和同步锁。线程的同步本节介绍12.5.1同步代码块
使用同步代码块实现同步功能,只需将对实例的访问语句放入一个同步块中,其语法格式如下:synchronized(object){ //需要同步的代码块}
其中:synchronized是同步关键字;object是同步监视器,其数据类型不能是基本数据类型。线程开始执行同步代码之前,必须先获得同步监视器的锁定,并且,任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
下述案例示例了同步代码块的声明和使用,代码如下所示。同步代码块同步代码块【代码12.10】SynBlockExample.javapackagecom;//银行帐户类classBankAccount{ //银行账号 privateStringbankNo; //银行余额 privatedoublebalance; //构造方法 publicBankAccount(StringbankNo,doublebalance){ this.bankNo=bankNo; this.balance=balance; } publicStringgetBankNo(){ returnbankNo; } publicvoidsetBankNo(StringbankNo){ this.bankNo=bankNo; } publicdoublegetBalance(){ returnbalance; } publicvoidsetBalance(doublebalance){ this.balance=balance; }}12.5.1同步代码块
publicclassSynBlockExampleextendsThread{ //银行账户 privateBankAccountaccount; //操作金额,正数为存钱,负数为取钱 privatedoublemoney; publicSynBlockExample(Stringname,BankAccountaccount,doublemoney){ super(name); this.account=account; this.money=money; } //线程任务 publicvoidrun(){ synchronized(this.account){ //获取目账户的金额 doubled=this.account.getBalance(); //如果操作的金额money<0,则代表取钱操作,//同时判断账户金额是否低于取钱金额 if(money<0&&d<-money){ System.out.println(this.getName()+"操作失败,余额不足!"); return; }else{12.5.1同步代码块 //对账户金额进行操作 d+=money; System.out.println(this.getName()+"操作成功,目前账户余额为:"+d); try{ //休眠10毫秒 Thread.sleep(10); }catch(InterruptedExceptione){ e.printStackTrace(); } //修改账户金额 this.account.setBalance(d);
} } }
publicstaticvoidmain(String[]args){ //创建一个银行账户实例 BankAccountmyAccount=newBankAccount("101",5000); 12.5.1同步代码块 //创建多个线程,对账户进行存取钱操作 SynBlockExamplet1=newSynBlockExample("T1",myAccount,-3000); SynBlockExamplet2=newSynBlockExample("T2",myAccount,-3000); SynBlockExamplet3=newSynBlockExample("T3",myAccount,1000); //启动线程 t1.start(); t2.start(); t3.start(); //等待所有子线程完成 try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } //输出账户信息 System.out.println("账号:"+myAccount.getBankNo()+",余额:" +myAccount.getBalance()); }}12.5.1同步代码块
上述代码在run()方法中,使用“synchronized(this.account){}”对银行账户的操作代码进行同步,保证某一时刻只能有一个线程访问该账户,只有{}里面的代码执行完毕,才释放对该账户的锁定。
程序运行结果如下所示:T1操作成功,目前账户余额为:2000.0T2操作失败,余额不足!T3操作成功,目前账户余额为:3000.0
账号:101,余额:3000.012.5.112.5.2同步方法
同步方法是使用synchronized关键字修饰的方法,其声明的语法格式如下:[访问修饰符]synchronized返回类型方法名([参数列表]){ //方法体}
其中:synchronized关键字修饰的实例方法无须显式地指定同步监视器,同步方法的同步监视器是this,即该方法所属的对象。一旦一个线程进入一个实例的任何同步方法,其他线程将不能进入该实例的所有同步方法,但该实例的非同步方法仍然能够被调用。
使用同步方法可以非常方便地实现线程安全,一个具有同步方法的类被称为“线程安全的类”,该类的对象可以被多个线程安全地访问,且每个线程调用该对象的方法后都将得到正确的结果。下述案例示例了同步方法的声明和使用,代码如下所示。同步方法同步方法【代码12.11】SynMethodExample.javapackagecom;//增加有同步方法的银行帐户类classSynMethod{ //银行账号 privateStringbankNo; //银行余额 privatedoublebalance; //构造方法 publicSynMethod(StringbankNo,doublebalance){ this.bankNo=bankNo; this.balance=balance; } //同步方法,存取钱操作 publicsynchronizedvoidaccess(doublemoney){ //如果操作的金额money<0,则代表取钱操作,//同时判断账户金额是否低于取钱金额 if(money<0&&balance<-money){ System.out.println(Thread.currentThread().getName()+"操作失败,余额不足!"); return;//返回 }else{12.5.2同步方法 //对账户金额进行操作 balance+=money; System.out.println(Thread.currentThread().getName() +"操作成功,目前账户余额为:"+balance); try{ //休眠1毫秒 Thread.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); } } } publicStringgetBankNo(){ returnbankNo; } publicdoublegetBalance(){ returnbalance; }}12.5.2同步方法publicclassSynMethodExampleextendsThread{ //银行账户 privateSynMethodaccount; //操作金额,正数为存钱,负数为取钱 privatedoublemoney; publicSynMethodExample(Stringname,SynMethodaccount,doublemoney){ super(name); this.account=account; this.money=money; } //线程任务 publicvoidrun(){ //调用account对象的同步方法 this.account.access(money); } publicstaticvoidmain(String[]args){ //创建一个银行账户实例 SynMethodmyAccount=newSynMethod("1001",5000); //创建多个线程,对账户进行存取钱操作 SynMethodExamplet1=newSynMethodExample("T1",myAccount,-3000); 12.5.2同步方法 SynMethodExamplet2=newSynMethodExample("T2",myAccount,-3000); SynMethodExamplet3=newSynMethodExample("T3",myAccount,1000); //启动线程 t1.start(); t2.start(); t3.start();
//等待所有子线程完成 try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } //输出账户信息 System.out.println("账号:"+myAccount.getBankNo()+",余额:" +myAccount.getBalance()); }}12.5.2同步方法
程序运行结果如下:T1操作成功,目前账户余额为:2000.0T2操作失败,余额不足!T3操作成功,目前账户余额为:3000.0
账号:1001,余额:3000.0
注意:synchronized锁定的是对象,而不是方法或代码块;synchronized也可以修饰类,当用synchronized修饰类时,表示这个类的所有方法都是synchronized的。12.5.212.5.3同步锁
同步锁Lock是一种更强大的线程同步机制,通过显式定义同步锁对象来实现线程同步。同步锁提供了比同步代码块、同步方法更广泛的锁定操作,实现更灵活。Lock是控制多个线程对共享资源进行访问的工具,能够对共享资源进行独占访问。每次只能有一个线程对Lock对象加锁,线程访问共享资源之前需要先获得Lock对象。某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。Lock和ReadWriteLock是Java5提供的关于锁的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。从Java8开始,又新增了StampedeLock类,可以替代传统的ReentrantReadWriteLock类。同步锁同步锁ReentrantLock类是常用的可重入同步锁,该类对象可以显式地加锁、释放锁。使用ReentrantLock类的步骤如下:
(1)定义一个ReentrantLock锁对象,该对象是final常量;privatefinalReentrantLocklock=newReentrantLock();
(2)在需要保证线程安全的代码之前增加“加锁”操作;lock.lock();
(3)在执行完线程安全的代码后“释放锁”。lock.unlock();12.5.3同步锁下述代码示例了使用ReentrantLock锁的基本步骤://1.定义锁对象privatefinalReentrantLocklock=newReentrantLock();...//定义需要保证线程安全的方法publicvoidmyMethod(){//2.加锁lock.lock();try{//需要保证线程安全的代码...}finally{//3.释放锁lock.unlock();}}
其中:加锁和释放锁都需要放在线程安全的方法中;lock.unlock()放在finally语句中,不管发生异常与否,都需要释放锁。12.5.3同步锁下述案例示例了ReentrantLock同步锁的使用,代码如下所示。【代码12.12】SynLockExample.javapackagecom;importjava.util.concurrent.locks.ReentrantLock;classSynLock{ privateStringbankNo; //银行账号 privatedoublebalance;//银行余额 //定义锁对象 privatefinalReentrantLocklock=newReentrantLock(); //构造方法 publicSynLock(StringbankNo,doublebalance){ this.bankNo=bankNo; this.balance=balance; } //存取钱操作 publicvoidaccess(doublemoney){ //加锁 lock.lock(); try{ //如果操作的金额money<0,则代表取钱操作,12.5.3同步锁 //同时判断账户金额是否低于取钱金额 if(money<0&&balance<-money){ System.out.println(Thread.currentThread().getName() +"操作失败,余额不足!"); //返回 return; }else{ //对账户金额进行操作 balance+=money; System.out.println(Thread.currentThread().getName() +"操作成功,目前账户余额为:"+balance); try{ //休眠1毫秒 Thread.sleep(1); }catch(InterruptedExceptione){ e.printStackTrace(); } } }finally{ 12.5.3同步锁 //释放锁 lock.unlock(); } } publicStringgetBankNo(){ returnbankNo; } publicdoublegetBalance(){ returnbalance; }}12.5.3同步锁//使用同步锁的类publicclassSynLockExampleextendsThread{ //银行账户 privateSynLockaccount; //操作金额,正数为存钱,负数为取钱 privatedoublemoney; publicSynLockExample(Stringname,SynLockaccount,doublemoney){ super(name); this.account=account; this.money=money; } //线程任务 publicvoidrun(){ //调用account对象的access()方法 this.account.access(money); } publicstaticvoidmain(String[]args){ //创建一个银行账户实例 SynLockmyAccount=newSynLock("1001",5000); //创建多个线程,对账户进行存取钱操作12.5.3同步锁 SynLockExamplet1=newSynLockExample("T1",myAccount,-3000); SynLockExamplet2=newSynLockExample("T2",myAccount,-3000); SynLockExamplet3=newSynLockExample("T3",myAccount,1000); //启动线程 t1.start(); t2.start(); t3.start(); //等待所有子线程完成 try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } //输出账户信息 System.out.println("账号:"+myAccount.getBankNo()+",余额:" +myAccount.getBalance()); }}12.5.3同步锁程序运行结果如下:T1操作成功,目前账户余额为:2000.0T2操作失
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2024年南通客运资格证考试题目
- 吉首大学《家具设计》2021-2022学年第一学期期末试卷
- 吉首大学《插画设计B》2021-2022学年第一学期期末试卷
- 吉林艺术学院《艺术概论》2021-2022学年第一学期期末试卷
- 吉林艺术学院《曲式基础》2021-2022学年第一学期期末试卷
- 吉林艺术学院《行草临摹与创作》2021-2022学年第一学期期末试卷
- 吉林艺术学院《CG美宣图创作实践》2021-2022学年第一学期期末试卷
- 2024年大众帕萨特购买协议书模板
- 引进外劳协议书范文模板范文
- 吉林艺术学院《节奏训练II》2021-2022学年第一学期期末试卷
- 期中易错卷(第1-4单元)(试题)-2024-2025学年三年级上册数学人教版
- 中国消费名品申报书;“数字三品”应用场景典型案例申报书
- 《逻辑的力量-采用合理的论证方法》教学设计 2023-2024学年统编版高中语文选择性必修上册
- 2025届广西南宁市、玉林市、贵港市等高考仿真模拟物理试卷含解析
- 法律服务投标方案(技术方案)
- 【语文】第一单元《阅读综合实践》课件-2024-2025学年七年级语文上册(统编版2024)
- 办公室改造合同协议书
- 中医内科学重点知识
- 技能人才评价新职业考评员培训在线考试(四川省)
- 2023-2024学年北京市101中学八年级(上)期中数学试卷【含解析】
- 部编版四年级上册《第5单元 习作例文:我家的杏熟了 小木船》课件
评论
0/150
提交评论