Java多线程编程.docx
- 文档编号:5812751
- 上传时间:2023-01-01
- 格式:DOCX
- 页数:54
- 大小:332.73KB
Java多线程编程.docx
《Java多线程编程.docx》由会员分享,可在线阅读,更多相关《Java多线程编程.docx(54页珍藏版)》请在冰豆网上搜索。
Java多线程编程
Java多线程编程介绍
1.基础一
基础篇
(一)
[写在前面]
随着计算机技术的发展,编程模型也越来越复杂多样化.但多线程编程模型是目前计算机
系统架构的最终模型.随着CPU主频的不断攀升,X86架构的硬件已经成为瓶,在这种架构的CPU主频最
高为4G.事实上目前3.6G主频的CPU已经接近了顶峰.
如果不能从根本上更新当前CPU的架构(在很长一段时间内还不太可能),那么继续提高CPU
性能的方法就是超线程CPU模式.
那么,作业系统,应用程序要发挥CPU的最大性能,就是要改变到以多线程编程模型为主的并
行处理系统和并发式应用程序.
所以,掌握多线程编程模型,不仅是目前提高应用性能的手段,更是下一代编程模型的核心
思想.
多线程编程的目的,就是"最大限度地利用CPU资源",当某一线程的处理不需要占用CPU而只
和I/O,OEMBIOS等资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源.从根本上说,这就是多
线程编程的最终目的.
[第一需要弄清的问题]
如同程序和进程的区别,要掌握多线程编程,第一要弄清的问题是:
线程对象和线程的区别.
线程对象是可以产生线程的对象.比如在java平台中Thread对象,Runnable对象.
线程,是指正在执行的一个指令序列.在java平台上是指从一个线程对象的start()开始.
运行run方法体中的那一段相对独立的过程.
鉴于作者的水平,无法用更确切的词汇来描述它们的定义.但这两个有本质区别的概念请初
学者细细体会,随着介绍的深入和例程分析的增加,就会慢慢明白它们所代表的真实含义.
天下难事必始于易,天下大事必始于细.
让我们先从最简单的"单线程"来入手.
(1)带引号说明只是相对而言的单线程.
(2)基于java
class BeginClass{
public static void main(String[] args){
for(int i=0;i<100;i++)
System.out.println("Hello,World!
");
}
}
如果我们成功编译了该java文件,然后在命令行上敲入:
java BeginClass
现在发生了什么呢?
每一个java程序员,从他开始学习java的第一分钟里都会接触到这个问
题,但是,你知道它到底发生发什么?
JVM进程被启动,在同一个JVM进程中,有且只有一个进程,就是它自己.然后在这个JVM环境中,
所有程序的运行都是以线程来运行.JVM最先会产生一个主线程,由它来运行指定程序的入口点.在这个
程序中,就是主线程从main方法开始运行.当main方法结束后,主线程运行完成.JVM进程也随之退出.
我们看到的是一个主线程在运行main方法,这样的只有一个线程执行程序逻辑的流程我们称
之为单线程.这是JVM提供给我们的单线程环境,事实上,JVM底层还至少有垃圾回收这样的后台线程以
及其它非java线程,但这些线程结我们而言不可访问,我们只认为它是单线程的.
主线程是JVM自己启动的,在这里它不是从线程对象产生的.在这个线程中,它运行了main方法
这个指令序列.理解它,但它没有更多可以研究的内容.
[接触多线程]
class MyThread extends Thread{
public void run(){
System.out.println("Thread say:
Hello,World!
");
}
}
public class MoreThreads{
public static void main(String[] args){
new MyThread();
new MyThread().start();
System.out.println("Main say:
Hello,World");
}
}
执行这个程序,main方法第一行产生了一个线程对象,但并没有线程启动.
main方法第二行产生了一个线程对象,并启动了一个线程.
main方法第三行,产生并启动一个线程后,主线程自己也继续执行其它语句.
我们先不研究Thread对象的具体内容,稍微来回想一下上面的两个概念,线程对象和线程.
在JAVA中,线程对象是JVM产生的一个普通的Object子类.
而线程是CPU分配给这个对象的一个运行过程.我们说的这个线程在干什么,不是说一个线
程对象在干什么,而是这个运行过程在干什么.如果一时想不明白,不要急,但你要记得它们不是一回
事就行了.
累了吧?
为不么不继续了?
基于这种风格来介绍多线程,并不是每个人都喜欢和接受的,如果你不喜欢,正好不浪费你的
时间了,而如果你接受的话,那就看下一节吧.
2.基础二
在进入java平台的线程对象之前,基于基础知识
(一)的一些问题,我先插入两个基本概念.
[线程的并发与并行]
在单CPU系统中,系统调度在某一时刻只能让一个线程运行,虽然这种调试机制有多种形式
(大多数是时间片轮巡为主),但无论如何,要通过不断切换需要运行的线程让其运行的方式
就叫并发(concurrent).
而在多CPU系统中,可以让两个以上的线程同时运行,这种可以同时让两个以上线程同时运行
的方式叫做并行(parallel).
在上面包括以后的所有论述中,请各位朋友谅解,我无法用最准确的词语来定义储如并发和
并行这类术语,但我以我的经验能通俗地告诉大家它是怎么一回事,如果您看到我说的一些
"标准"文档上说的不一样,只要意思一致,那您就不要挑刺了.
[JAVA线程对象]
现在我们来开始考察JAVA中线程对象.
在JAVA中,要开始一个线程,有两种方式.一是直接调用Thread实例的start()方法,二是
将Runable实例传给一个Thread实例然后调用它的start()方法.
在基础知识
(一)中已经说过,线程对象和线程是两个完全不同的概念.这里我们再次
深入一下,生成一个线程的实例,并不代表启动了线程.而启动线程是说在某个线程对象上启动
了该实例对应的线程,当该线程结束后,并不会就立即消失.
对于从很多书籍上可以看到的基础知识我就不用多说了.既然是基础知识,我也着重
于从普通文档上读不到的内容.
所以本节我重点要说的是两种线程对象产生线程方式的区别.
class MyThread extends Thread{
public int x = 0;
public void run(){
for(int i=0;i<100;i++){
try{
Thread.sleep(10);
}catch(Exception e){}
System.out.println(x++);
}
}
}
如果我们生成MyThread的一个实例,然后调用它的start();方法,那么就产生了这个实例对应
的线程:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
}
}
不用说,最终会打印出0到99,现在我们稍微玩一点花样:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
System.out.println(101);
}
}
也不用说,在基础知识
(一)中我们知道由于单CPU的原因,一般会先打印101,然后打印
0到99.不过我们可以控制线程让它按我们的意思来运行:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
mt.join();
System.out.println(101);
}
}
好了,我们终于看到,mt实例对应的线程(假如我有时说mt线程请你不要怪我,不过我尽量不这么说)
在运行完成后,主线程才打印101.因为我们让主当前线程(这里是主线程)等待mt线程的运行结束.
"在线程对象a上调用join()方法,就是让当前正在执行的线程等待线程对象a对应的线程运行完成后
才继续运行." 请大家一定要深刻理解并熟记这句话,而我这里引出这个知识点的目的是为了让你继
续看下面的例子:
public class Test {
public static void main(String[] args) throws Exception{
MyThread mt = new MyThread();
mt.start();
mt.join();
Thread.sleep(3000);
mt.start();
}
}
当线程对象mt运行完成后,我们让主线程休息一下,然后我们再次在这个线程对象上启动线程.结果我
们看到:
Exception in thread "main" java.lang.IllegalThreadStateException
也就是这种线程对象一时运行一次完成后,它就再也不能运行第二次了.
我们可以看一下它有具体实现:
public synchronized void start() {
if (started)
throw new IllegalThreadStateException();
started = true;
group.add(this);
start0();
}
一个Thread的实例一旦调用start()方法,这个实例的started标记就标记为true,事实中不管这个线程
后来有没有执行到底,只要调用了一次start()就再也没有机会运行了,这意味着:
[通过Thread实例的start(),一个Thread的实例只能产生一个线程]
那么如果要在一个实例上产生多个线程(多个线程共同访问同一实例的一些共同资源),我们应该如何做呢?
这就是Runnable
接口给我们带来的伟大的功能.
class R implements Runnable{
private int x = 0;
public void run(){
for(int i=0;i<100;i++){
try{
Thread.sleep(10);
}catch(Exception e){}
System.out.println(x++);
}
}
}
正如它的名字一样,Runnable的实例是可运行的,但它自己并不能直接运行,它需要被Thread对象来
包装才行运行:
public class Test {
public static void main(String[] args) throws Exception{
new Thread(new R()).start();
}
}
当然这个结果和mt.start()没有什么区别.但如果我们把一个Runnable实例给Thread对象多次包装,我
们就可以看到它们实际是在同一实例上启动线程:
public class Test {
public static void main(String[] args) throws Exception{
R r = new R();
for(int i=0;i<10;i++)
new Thread(r).start();
}
}
x是实例对象,但结果是x被加到了999,说明这10个线程是在同一个r对象上运行的.请大家注意,因为这个
例子是在单CPU上运行的,所以没有对多个线程同时操作共同的对象进行同步.这里是为了说明的方便而
简化了同步,而真正的环境中你无法预知程序会在什么环境下运行,所以一定要考虑同步.
到这里我们做一个完整的例子来说明线程产生的方式不同而生成的线程的区别:
package debug;
import java.io.*;
import java.lang.Thread;
class MyThread extends Thread{
public int x = 0;
public void run(){
System.out.println(++x);
}
}
class R implements Runnable{
private int x = 0;
public void run(){
System.out.println(++x);
}
}
public class Test {
public static void main(String[] args) throws Exception{
for(int i=0;i<10;i++){
Thread t = new MyThread();
t.start();
}
Thread.sleep(10000);//让上面的线程运行完成
R r = new R();
for(int i=0;i<10;i++){
Thread t = new Thread(r);
t.start();
}
}
}
上面10个线程对象产生的10个线程运行时打印了10次1.
下面10个线程对象产生的10个线程运行时打印了1到10.
我们把下面的10个线程称为同一实例(Runnable实例)的多个线程.
下节我们将研究线程对象方法,还是那句话,一般文档中可以读到的内容我不会介绍太多
请大家自己了解.
3.基础三
线程对象的几个重要的方法
尽管线程对象的常用方法可以通过API文档来了解,但是有很多方法仅仅从API说明是无法详细了解的.
本来打算用一节的篇幅来把线程方法中一些重要的知识说完,但这样下来估计要很长的篇幅,可能要用
好几节才能说把和线程方法相关的一些重要的知识说完.
首先我们接基础篇
(二)来说明
start()方法.
一个线程对象生成后,如果要产生一个执行的线程,就一定要调用它的start()方法.在介绍这个方法时
不得不同时说明run方法.其实线程对象的run方法完全是一个接口回调方法,它是你这个线程对象要完
成的具体逻辑.简单说你要做什么就你在run中完成,而如何做,什么时候做就不需要你控制了,你只要调
用start()方法,JVM就会管理这个线程对象让它产生一个线程并注册到线程处理系统中(线程栈).
从表面上看,start()方法调用了run()方法,事实上,start()方法并没有直接调用run方法.在JDK1.5以前
start()方法是本地方法,它如何最终调用run方法已经不是JAVA程序员所能了解的.而在JDK1.5中,原来的
那个本地start()方法被start0()代替,另个一个纯JAVA的start()中调用本地方法start0(),而在start()
方法中做了一个验证,就是对一个全局变量(对象变量)started做检验,如果为true,则start()抛出异常,不
会调用本地方法start0(),否则,先将该变量设有true,然后调用start0()
从中我们可以看到这个为了控制一个线程对象只能运行成功一次start()方法.这是因为线程的运行要获
取当前环境,包括安全,父线程的权限,优先级等条件,如果一个线程对象可以运行多次,那么定义一个static
的线程在一个环境中获取相应权限和优先级,运行完成后它在另一个环境中利用原来的权限和优先级等属
性在当前环境中运行,这样就造成无法预知的结果.简单说来,让一个线程对象只能成功运行一次,是基于
对线程管理的需要.
start()方法最本质的功能是从CPU中申请另一个线程空间来执行run()方法中的代码,它和当前的线程是
两条线,在相对独立的线程空间运行,也就是说,如果你直接调用线程对象的run()方法,当然也会执行,但
那是在当前线程中执行,run()方法执行完成后继续执行下面的代码.而调用start()方法后,run()方法的
代码会和当前线程并发(单CPU)或并行(多CPU)执行.
所以请记住一句话[调用线程对象的run方法不会产生一个新的线程],虽然可以达到相同的执行结果,但执
行过程和执行效率不同.
[线程的interrupt()方法,interrupted()和isInterrupted()]
这三个方法是关系非常密切而且又比较复杂的,虽然它们各自的功能很清楚,但它们之间的关系有大多数
人不是真正的了解.
先说interrupt()方法,它是实例方法,而它也是最奇怪的方法,在java语言中,线程最初被设计为"隐晦难懂"的东西,直到现在它的语义也不象它的名字那样准确.
大多数人以为,一个线程象调用了interrupt()方法,那它对应的线程就应该被中断而抛出异常,事实中,当一个线程对象调用interrupt()方法,它对应的线程并没有被中断,只是改变了它的中断状态.使当前线程的状态变以中断状态,如果没有其它影响,线程还会自己继续执行.
只有当线程执行到sleep,wait,join等方法时,或者自己检查中断状态而抛出异常的情况下,线程才会抛出异常.
如果线程对象调用interrupt()后它对应的线程就立即中断,那么interrupted()方法就不可能执行.因为interrupted()方法是一个static方法,就是说只能在当前线程上调用,而如果一个线程interrupt()后,它已经中断了,那它又如何让自己interrupted()?
正因为一个线程调用interrupt()后只是改变了中断状态,它可以继续执行下去,在没有调用sleep,wait,join等法或自己抛出异常之前,它就可以调用interrupted()来清除中断状态(还会原状)。
interrupted()方法会检查当前线程的中断状态,如果为"被中断状态"则改变当前线程为"非中断状态"并返回true,如果为"非中断状态"则返回false,它不仅检查当前线程是否为中断状态,而且在保证当前线程回来非中断状态,所以它叫"interrupted",是说中断的状态已经结束(到非中断状态了)。
isInterrupted()方法则仅仅检查线程对象对应的线程是否是中断状态,并不改变它的状态.
目前大家只能先记住这三个方法的功能,只有真正深入到多线程编程实践中,才会体会到它们为什么是对象方法,为什么是类方法.
线程到底什么时候才被中断抛出InterruptedException异常,我们将在提高篇中详细讨论.
[sleep(),join(),yield()方法]
在现在的环节中,我只能先说明这些方法的作用和调用原则,至于为什么,在基础篇中无法深入,只能在提高篇中详细说明.
sleep()方法中是类方法,也就是对当前线程而言的,程序员不能指定某个线程去sleep,只能是当前线程执行到sleep()方法时,睡眠指定的时间(让其它线程运行).事实上也只能是类方法,在当前线程上调用.试想如果你调用一个线程对象的sleep()方法,那么这个对象对应的线程如果不是正在运行,它如何sleep()?
所以只有当前线程,因为它正在执行,你才能保证它可以调用sleep()方法.
原则:
[在同步方法中尽量不要调用线程的sleep()方法],或者简单说,对于一般水平的程序员你基本不应该调用sleep()方法.
join()方法,正如第一节所言,在一个线程对象上调用join方法,是当前线程等待这个线程对象对应的线程结束,比如有两个工作,工作A要耗时10秒钟,工作B要耗时10秒或更多.我们在程序中先生成一个线程去做工作B,然后做工作A.
new B().start();//做工作B
A();//做工作A
工作A完成后,下面要等待工作B的结果来进行处理.如果工作B还没有完成我就不能进行下面的工作C,所以
B b = new B();
b.start();//做工作B
A();//做工作A
b.join();//等工作B完成.
C();//继续工作C.
原则:
[join是测试其它工作状态的唯一正确方法],我见过很多人,甚至有的是博士生,在处理一项工作时如果另一项工作没有完成,说让当前工作线程sleep(x),我问他,你这个x是如何指定的,你怎么知道是100毫秒而不是99毫秒或是101毫秒?
其实这就是OnXXX事件的实质,我们不是要等多长时间才去做什么事,而是当等待的工作正好完成的时候去做.
yield()方法也是类方法,只在当前线程上调用,理由同上,它主是让当前线程放弃本次分配到的时间片
原则:
[不是非常必要的情况下,没有理由调用它].调用这个方法不会提高任何效率,只是降低了CPU的总周期。
上面介绍的线程一些方法,基于(基础篇)而言只能简单提及.以后具体应用中我会结合实例详细论述.
线程本身的其它方法请参看API文档.下一节介绍非线程的方法,但和线程密切相关的两[三]个对象方法:
[wait(),notify()/notifyAll()]
这是在多线程中非常重要的方法.
4.基础四
[wait(),notify()/notityAll()方法]
关于这两个方法,有很多的内容需要说明.在下面的说明中可能会有很多地方不能一下子明白,但在看完本节后,即使不能完全明白,你也一定要回过头来记住下面的两句话:
[wait(),notify()/notityAll()方法是普通对象的方法(Object超类中实现),而不是线程对象的方法]
[wait(),noti
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 多线程 编程