第7章 Java多线程.docx
《第7章 Java多线程.docx》由会员分享,可在线阅读,更多相关《第7章 Java多线程.docx(31页珍藏版)》请在冰豆网上搜索。
![第7章 Java多线程.docx](https://file1.bdocx.com/fileroot1/2023-4/20/e662c96d-90e7-4cc5-990d-05903ab58181/e662c96d-90e7-4cc5-990d-05903ab581811.gif)
第7章Java多线程
第7章Java多线程
核心内容:
1.多线程的概念以及与进程的区别
2.程序中创建线程的两种不同方法
3.线程的生命周期和控制
4.多线程的同步处理机制
7.1多线程的使用
面向对象编程使得程序划分成相互独立的模块执行。
但是还会碰到,不但需要把程序分解开来,而且还要让它的各个部分都能独立运行的问题。
这种能独立运行的子任务就是线程(Thread)。
我们可以认为线程都是独立运行的,有自己CPU的子任务。
实际上,是一些底层机制在为子任务分割CPU的时间,只是我们不知道。
单线程的执行就是老板从头到尾一个人做完,多线程就是老板在做的同时,拨出一部分事情让手下去做,这样会加快事情的进展。
7.2线程的概念
为了更好地理解什么是线程,我们有必要学习与之相关的概念——“进程”。
进程是指程序的一次动态执行过程,或者说进程是正在执行中的程序,其占用特定的地址空间。
以前古老的DOS操作系统是单任务的,系统每次只能做一件事。
比如你在复制文件的时候不能重命名文件。
现在的操作系统都是多任务操作系统,即将CPU的计算时间动态地划分给多个进程,每个运行的程序就是一个进程,比如听歌和QQ聊天可以同时进行,就是两个进程在同时工作。
如果使用的是windows系统,就可以在任务管理器中看到操作系统正在运行的进程信息。
相对于进程,线程是进程中一段连续的控制流程或是一段执行路径。
它不能独立存在,必须存在于某个进程中。
一个进程可以拥有多个线程,这些线程可以共享进程中的内存单元,可以访问相同的变量和对象,以实现线程间的通信,数据交换和同步操作等功能。
和进程相比,线程在管理费用,相互间通信等方面所需要的代价要小得多,所以也将线程称为“轻量级进程”。
进程和线程的区别主要体现在:
1.一个程序至少有一个进程,一个进程至少有一个线程;
2.进程在执行过程中拥有独立的内存单元,而多个线程共享内存,极大地提高了程序的运行效率;
3.线程不能独立执行,必须依附在应用程序中,由应用程序提供多个线程执行控制。
多线程的目的为了最大限度的利用CPU资源,例如,通过浏览器既可以听音乐,又可以下载文件。
一般常见的Java应用程序都是单线程的。
多线程大大简化了“让一个程序同时做几件事”这个难题。
在多线程环境下,CPU会不断地在线程之间进行切换,给它们分配时间。
这样能大大提高效率,可以一边等数据,一边做其他事情。
Java语言就是第一个语言本身就支持线程的主流编程语言,其对线程的支持主要通过Thread类和Runnable接口来实现。
7.3线程的创建
在Java语言中,线程的创建通常有两种方式:
(1)扩展Thread类。
(2)实现Runnable接口。
7.3.1扩展Thread类
要创建线程,首先定义一个Thread类的子类,在该类中重写thread类的run()方法,即定义线程所需完成的工作。
Thread类常用的构造方法如下:
publicThread():
创建新的Thread对象;
publicThread(Stringname):
分配名字为name的新thread对象。
Thread类常用的方法如下:
publicstaticvoidsleep(longmillis)throwsinterruptedException:
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
publicvoidstart():
使该线程开始执行;Java虚拟机调用该线程的run()方法。
publicvoidrun():
定义该线程需执行的任务。
publicfinalStringgetName():
返回该线程的名称。
publicfinalBooleanisAlive():
测试线程是否处于活动状态。
如果线程已经启动且尚未终止,则为活动状态。
publicstaticThreadcurrentThread():
返回当前正在执行的线程对象的引用。
【例7-1】通过扩展thread类的方法创建线程示例。
1publicclassTestMitiThread{
2publicstaticvoidmain(String[]args){
3System.out.println(Thread.currentThread().getName()+"线程运行开始!
");
4newMitiSay("A").start();
5newMitiSay("B").start();
6System.out.println(Thread.currentThread().getName()+"线程运行结束!
");
7}
8}
9classMitiSayextendsThread{
10publicMitiSay(StringthreadName){
11super(threadName);
12}
13publicvoidrun(){
14System.out.println(getName()+"线程运行开始!
");
15for(inti=0;i<10;i++){
16System.out.println(i+""+getName());
17try{
18sleep((int)Math.random()*1000);
19}catch(InterruptedExceptione){
20e.printStackTrace();
21}
22}
23System.out.println(getName()+"线程运行结束!
");
24}
25}
程序运行结果:
D:
\java\7>javacExample7_1.java
D:
\java\7>javaExample7_1
main线程运行开始!
A线程运行开始!
0A
main线程运行结束!
B线程运行开始!
1A
0B
2A
1B
3A
2B
4A
5A
4B
6A
5B
7A
6B
8A
7B
9A
8B
A线程运行结束!
9B
B线程运行结束!
程序说明:
(1)程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。
随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。
(2)在一个方法中调用Thread.currentThread().getName()方法,可以获取当前线程的名字。
在mian方法中调用该方法,获取的是主线程的名字。
(3)start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
从程序运行的结果可以发现,多线程程序是乱序执行。
因此,只有乱序执行的代码才有必要设计为多线程。
(4)Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
7.3.2实现runnable接口
上一节介绍了通过继承thread类创建线程的方法,现在再来介绍另一种创建线程的方法:
实现runnable接口。
在面向对象一章中,我们已经知道,在Java语言中只支持单继承,当所定义的线程类已经是某个类的子类,此时继承thread类创建线程的方法就行不通了,必须采用这一节的方法:
实现runnable接口创建线程。
实现runnable接口需要引用thread类的另一种构造方法:
PublicThread(Runnabletarget):
分配新的Thread对象。
【例7-2】通过实现runnable接口的方法创建线程。
1publicclassTestMitiThread1implementsRunnable{
2publicstaticvoidmain(String[]args){
3System.out.println(Thread.currentThread().getName()+"线程运行开始!
");
4TestMitiThread1test=newTestMitiThread1();
5Threadthread1=newThread(test);
6Threadthread2=newThread(test);
7thread1.start();
8thread2.start();
9System.out.println(Thread.currentThread().getName()+"线程运行结束!
");
10}
11publicvoidrun(){
12System.out.println(Thread.currentThread().getName()+"线程运行开始!
");
13for(inti=0;i<10;i++){
14System.out.println(i+""+Thread.currentThread().getName());
15try{
16Thread.sleep((int)Math.random()*10);
17}catch(InterruptedExceptione){
18e.printStackTrace();
19}
20}
21System.out.println(Thread.currentThread().getName()+"线程运行结束!
");
22}
23}
程序运行结果:
main线程运行开始!
Thread-0线程运行开始!
main线程运行结束!
0Thread-0
Thread-1线程运行开始!
0Thread-1
1Thread-1
1Thread-0
2Thread-0
2Thread-1
3Thread-0
3Thread-1
4Thread-0
4Thread-1
5Thread-0
6Thread-0
5Thread-1
7Thread-0
8Thread-0
6Thread-1
9Thread-0
7Thread-1
Thread-0线程运行结束!
8Thread-1
9Thread-1
Thread-1线程运行结束!
程序说明:
(1)TestMitiThread1类通过实现Runnable接口,使得该类有了多线程类的特征。
run()方法是多线程程序的一个约定。
所有的多线程代码都在run方法里面。
Thread类实际上也是实现了Runnable接口的类。
(2)在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnabletarget)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
(3)实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。
因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
7.3.3两种创建线程方法的比较
(1)扩展thread类
该方法较简单,采用了Java面向对象的继承机制,但有碍于Java语言只支持单继承的特点,使用比较单一,有很大的局限性。
在继承了thread后,不能再继承其他类。
(2)实现runnable接口
该方法采用实现接口的方式,具有很好的灵活型。
既避免了Java单继承性带来的局限,又适合多个相同程序代码的线程去处理统一资源的情况,实现了资源共享。
【例7-3】Thread类创建线程模拟铁路售票系统的差异演示。
说明:
该系统有四个售票点,发售某日某次列车的10张车票。
要求:
一个售票点用一个线程表示。
1publicclassExample7_3{
2publicstaticvoidmain(String[]agrs){
3newThreadTest().start();
4newThreadTest().start();
5newThreadTest().start();
6newThreadTest().start();
7}
8}
9classThreadTestextendsThread{
10privateintticket=10;
11publicvoidrun(){
12while(true){
13if(ticket>0){
14System.out.println(Thread.currentThread().getName()+"isselling15ticket"+ticket--);
15}
16else{
17break;
18}
19}
20}
21}
程序运行结果:
D:
\java\7>javacExample7_3.java
D:
\java\7>javaExample7_3
Thread-0issellingticket10
Thread-0issellingticket9
Thread-1issellingticket10
Thread-0issellingticket8
Thread-0issellingticket7
Thread-2issellingticket10
Thread-3issellingticket10
Thread-2issellingticket9
Thread-0issellingticket6
Thread-1issellingticket9
Thread-0issellingticket5
Thread-2issellingticket8
Thread-3issellingticket9
Thread-2issellingticket7
Thread-0issellingticket4
Thread-1issellingticket8
Thread-0issellingticket3
Thread-2issellingticket6
Thread-3issellingticket8
Thread-2issellingticket5
Thread-2issellingticket4
Thread-2issellingticket3
Thread-2issellingticket2
Thread-2issellingticket1
Thread-0issellingticket2
Thread-1issellingticket7
Thread-0issellingticket1
Thread-3issellingticket7
Thread-1issellingticket6
Thread-1issellingticket5
Thread-1issellingticket4
程序说明:
从结果上看每个票号都被打印了四次,即四个线程各自卖各自的10张票,而不去卖共同的10张票。
这种情况是怎么造成的呢?
我们需要的是,多个线程去处理同一个资源,一个资源只能对应一个对象,在上面的程序中,我们创建了四个ThreadTest对象,就等于创建了四个资源,每个资源都有10张票,每个线程都在独自处理各自的资源。
经过这些实验和分析,可以总结出,要实现这个铁路售票程序,我们只能创建一个资源对象,但要创建多个线程去处理同一个资源对象,并且每个线程上所运行的是相同的程序代码。
【例7-4】利用接口创建线程模拟铁路售票系统的差异演示。
1publicclassThreadDemo2{
2publicstaticvoidmain(String[]args){
3ThreadTestt=newThreadTest();
4newThread(t).start();
5newThread(t).start();
6newThread(t).start();
7newThread(t).start();
8}
9}
10classThreadTestimplementsRunnable{
11privateinttickets=10;
12publicvoidrun(){
13while(true){
14if(tickets>0){
15System.out.println(Thread.currentThread().getName()+
16"issalingticket"+tickets--);
17}
18}
19}
20}
程序运行结果:
D:
\java\7>javacExample7_4.java
D:
\java\7>javaExample7_4
Thread-0issellingticket10
Thread-0issellingticket7
Thread-1issellingticket8
Thread-2issellingticket9
Thread-1issellingticket4
Thread-0issellingticket5
Thread-3issellingticket6
Thread-0issellingticket1
Thread-1issellingticket2
Thread-2issellingticket3
程序说明:
上面的程序中,创建了四个线程,每个线程调用的是同一个ThreadTest对象中的run()方法,访问的是同一个对象中的变量(tickets)的实例,这个程序满足了我们的需求。
在Windows上可以启动多个记事本程序一样,也就是多个进程使用同一个记事本程序代码。
7.4线程的生命周期及调度
在java中每个线程都要经历五种状态:
新建,就绪,运行,阻塞和死亡。
线程从新生到死亡的状态变化过程称为生命周期。
下面结合图7-1来说明线程的生命周期。
执行完毕
解除阻塞
导致阻塞的事件
start()
调度
图7-1线程的生命周期
新建状态:
使用new创建线程对象,但此时线程并未执行。
就绪状态:
当线程对象调用了start方法以后,线程就处于就绪状态,JAVA虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有运行,只是表示可以运行而已。
启动线程使用start方法,而不是run方法,调用start方法来启动线程,系统会把该run方法当成线程执行体来处理,但如果直接调用线程对象的run方法,则run方法会被立即执行,而且在run方法之前的其他线程无法并行执行。
运行状态:
当线程处于就绪状态获得CPU,就开始执行run方法,程序进入运行状态。
一条线程开始运行以后,不可能一直处于运行状态,线程运行状态过程中也会被中断,目的是给其他线程获得执行的机会。
系统会给每个可执行的线程一小段时间来处理任务。
阻塞状态:
当发生如下情况,线程会进入阻塞状态:
1.线程调用sleep方法主动放弃所占用的资源。
2.线程调用了一个阻塞式IO方法,在该方法返回以前,该线程阻塞。
3.线程试图获得一个同步监视器,但该同步监视器正被别的线程所持有。
4.线程在等待某个通知。
5.线程调用了suspend方法将该线程挂起。
终止状态:
1.线程的run方法执行完成,线程正常结束。
2.线程抛出一个未捕获的exception或error。
3.直接调用线程的stop方法来结束该线程。
为了测试某线程是否已经死亡,可以调用线程的isAlive()方法,当线程处于就绪,运行,阻塞的时候返回true,当线程处于新建和死亡状态,该方法返回false。
当对已经死亡的线程调用start方法,会抛出illegalThreadStateException异常。
7.5线程的终止
有三种方法可以终止线程。
1.使用退出标志终止线程
当run方法执行完后,线程就会退出,但有时run方法时永远不会结束的,如在服务器程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。
在这种情况下,一般是将这些任务放在一个循环中,如while循环。
想使while循环在某一特定条件下退出,最直接的方法就是设计一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。
【例7-5】线程终止示例。
1publicclassExample7_5extendsThread{
2publicbooleanexit=false;
3publicvoidrun(){
4while(!
exit);
5}
6publicstaticvoidmain(String[]args)throwsException{
7Example7_5thread=newExample7_5();
8sleep(5000);
9thread.join();
10System.out.println("线程退出!
");
11}
12}
程序运行结果:
D:
\java\7>javacExample7_5.java
D:
\java\7>javaExample7_5
线程退出!
程序分析:
在上面代码中定义了一个退出exit,当exit为true时,while循环退出,exit的默认值为false。
2.使用stop方法终止线程
使用stop方法可以强行终止正在运行或挂起的线程。
我们可以使用如下的代码来终止线程:
Thread.stop();
虽然使用上面的代码可以终止线程,但使用stop方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
3.使用interrupt方法终止线程
使用interrupt方法来终止线程可分为两种情况:
(1)线程处于阻塞状态,如使用了sleep方法
(2)使用while(!
isInterrupted()){…}来判断线程是否被中断。
在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException异常,而在第二种情况下线程将直接退出
7.6线程同步
7.6.1线程同步问题
线程同步是为了防止多个线程同时访问一个数据对象时,对数据造成的破坏。
例如,一个线程可能尝试从一个文件中读取数据,而另一个线程则尝试在同一文件