线程

本文介绍了Java中线程的基础知识,包括线程的创建与启动、线程的阻塞及同步机制,通过实例演示了如何使用Runnable接口和Thread类创建线程,以及如何利用synchronized关键字和wait/notify方法实现线程间的同步。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目标一 实例化和启动线程

通过使用java.lang.Thread java.lang.Runnable 在代码中定义,实例化,和启动新线程。

什么是线程?

线程是表面上看似和主程序并行运行的轻量级进程。与进程不同的是它与程序的其他部分共享存储空间和数据。在这里线程的英文单词thread实际上是“thread of execution” 的缩写,you might like to imagine a rope from which you have frayed the end and taken one thread.它依然是主线程的一部分,但它可以独立出来,自己完成操作。这里请注意,启动一个多线程的程序和仅仅启动一个程序的多个同一程序是有区别的,因为一个多线程的程序将会对统一程序内的数据进行读取和存储。

 

一个可以显示多线程用处的例子就是打印,当你按下打印按钮的时候,你肯定不希望主程序直到打印完成才开始响应。最棒的就是你可以让打印进程“在后台”悄悄的运行,同时你可以使用主程序的其他部分。

 

而且当打印线程出现故障的时候主程序可以对此做出响应,一个讲解多线程最佳的通用例子就是创建一个每当你按下按钮的时候弹出一个弹球的图形用户界面程序。因为现在处理器速度快的原因,导致表面上看每个线程似乎是独享CPU,这是由于处理器在各个线程之间的切换速度很快,控制弹球跳动的代码更像是在处理器上运行唯一一个程序。不像大部分程序那样,线程并不是嵌入到Java语言的最核心部分,它的大部分,依然是继承自最原始的类——Object,旧一点的语言如C/C++并没有为线程设定标准。

 

当你在为Java程序员认证考试学习的时候,你必须要对当一个程序启动一个新线程有一定认识,这个时候程序不再是在单一的路径上执行。因为当一个线程A先于线程B执行,并不意味着线程A一定会比线程B先结束,当然线程B也不一定是在线程A结束后才开始运行。因此你有可能会遇到这样的问题,如“最有可能输出以下哪段代码?”,最准确的输出结果决定于底层的操作系统和同一时间正在一起运行的其它程序。

 

正是因为一个多线程程序在你的机器操作系统的组合上产生一个特定的输出,因此它不能保证在其他不同的系统上也能有同样的输出结果。出考题的人会凭空假设程序在一个更加通用的平台上运行的(如Windows),考题可以考察底层操作系统对Java线程的影响。

 

不要只把注意力放在考试的关于线程的考点上,因为它仅仅是考察你对一小部分线程知识是否掌握的很牢固。有很多线程相关的概念考试并没有覆盖到,如果你仅仅是为考试做打算,那么你可以对线程组,线程池,线程优先级等概念不做了解。当然,对这些概念的了解对更深层次领会Java语言编程是有好处的,如果你只想把精力集中在应付考试的考点上,那么你只需要对该指南上列出考点进行学习就行了。

两种创建线程的方式

在这两种方法中,使用Runnable似乎更常见一些,但是出于考试的原因,你必须对这两种方法都了解。下面这种方法就是让对象在创建的时候,用实现Runnable接口的方法来实现创建线程。

 

class MyClass implements Runnable{

public void run(){//Blank Body}

}

创建两个线程并运行它们。

 

MyClass mc = new MyClass();

MyClass mc2 = new MyClass();

Thread t = new Thread(mc);

Thread t2 = new Thread(mc2);

t.start();

t2.start();

这里需要注意的是,线程 t 并不肯定是比线程 t2先结束运行,当然由于run()方法里面并没有任何代码,因此线程 t很有可能比线程 t2先结束运行。即使你让该段代码在你的机器上运行上千次,无法改变它最终的运行结果,当然无法保证在其他的系统上运行结果也是一样的,或者当你更改系统的环境配置时,也有可能发生变化。

 

注意到在用实现Runnble接口的方法来创建线程时,必须要求创建一个Thread对象的实例,并且必须要在创建的时候,把实现该Runnable接口的对象作为构造方法的参数传递进去。

 

任何一个类在它实现一个接口的时候,它必须同时要创建该接口中已经定义的方法。该方法不一定非要有任何意义,比如,方法里面的内容可以为空,但是它必须在这个类的内部出现。因此在上面那个例子里出现了空的run()方法,不包含run()将会导致程序在编译期报错。

 

当你需要在一个类里面创建一个有某种用途的线程的时候,你需要在我上面的

 

//Blank Body

 

部分写一些东西进去。

 

另外一种创建线程的方法就是直接使类继承自Thread。这样做非常简单,但同时你也无法再继承其他的对象,因为Java只支持单继承。因此当你创建一个Button对象的时候你无法使用这种方法来添加线程的功能,因为它已经是继承了AWT Button这个类的,这使你不得不在继承方面动一点脑筋。不过一些反对的声音认为这种创建线程的方法更符合面向对象的思想。但不管怎么说,你必须为了Java考试对这种方法有一定了解。

实例化和启动一个线程

尽管在线程中运行的方法是run(),你并不需要调用这个方法来启动一个线程,而是调用start() 方法来启动一个线程。这点很关键,因为它极有可能在考试中出现。这点很有可能让你栽跟头,因为这和大多数往常你所遇见的Java编程的惯例不一样。通常如果你会把一段代码放到一个方法里面去,当你需要执行它的时候,你只需要调用该方法。如果你直接调用run()方法,当然这也不会造成什么错误,只是它会象一个普通的方法那样运行,而不是作为线程的一部分来执行。

 

Runnable接口并不包括start()方法,同样也不包含其它一些线程中有用的方法(如sleep(),suspend()等等),你只需将你已经实现了Runnable接口的对象作为一个构造方法参数传递给一个已经实例化的Thread 对象。

 

当你需要一个已经实现了Runnable接口的对象执行多线程任务的时候,你需要使用以下的代码。

 

MyClass mc = new MyClass();

Thread t = new Thread(mc);

t.start();

 


尽管是run方法在运行, 但一个线程的启动是调用start 方法。

 

再一次强调你不是调用run()方法,而是start()方法来启动一个线程,尽管在run()方法里面的代码才是线程执行的时候所运行的。

 

如果你的对象是继承Thread,你可以简单的调用start()方法来启动它。缺点就是Thread的子类因为单继承的原因再无法继承其它功能的类。

 

 

练习题

习题1)当你试图编译运行下列代码的时候会发生什么?

public class Runt implements Runnable{

public static void main(String argv[]){

Runt r = new Runt();

Thread t = new Thread(r);

t.start();

}

 

public void start(){

for(int i=0;i<100;i++)

System.out.println(i);

}

}

1) Compilation and output of count from 0 to 99
2) Compilation and no output
3) Compile time error: class Runt is an abstract class. It can't

习题2)下列哪一项表述是正确的?

Which of the following statements are true?

1) Directly sub classing Thread gives you access to more functionality of the Java threading capability than using the Runnable interface
2) Using the Runnable interface means you do not have to create an instance of the Thread class and can call
run directly
3) Both using the Runnable interface and subclassing of Thread require calling
start to begin execution of a Thread
4) The Runnable interface requires only one method to be implemented, this is called
run

习题3)当你试图编译运行下列代码的时候会发生什么?

public class Runt extends Thread{

public static void main(String argv[]){

Runt r = new Runt();

r.run();

}

public void run(){

for(int i=0;i<100;i++)

System.out.println(i);

}

}

1) Compilation and output of count from 0 to 99
2) Compilation and no output
3) Compile time error: class Runt is an abstract class. It can't be instantiated.
4) Compile time error, method
start has not been defined

习题4)下列哪一项表述是正确的?

1)To implement threading in a program you must import the class java.io.Thread
2) The code that actually runs when you start a thread is placed in the run method
3) Threads may share data between one another
4) To start a Thread executing you call the start method and not the run method

 

习题5) 下列哪一项是让线程开始运行的正确代码?

1)

public class TStart extends Thread{

public static void main(String argv[]){

TStart ts = new TStart();

ts.start();

}

public void run(){

System.out.println("Thread starting");

}

}

2)

public class TStart extends Runnable{

public static void main(String argv[]){

TStart ts = new TStart();

ts.start();

}

public void run(){

System.out.println("Thread starting");

}

}

3)

public class TStart extends Thread{

public static void main(String argv[]){

TStart ts = new TStart();

ts.start();

}

public void start(){

System.out.println("Thread starting");

}

}

4)

public class TStart extends Thread{

public static void main(String argv[]){

TStart ts = new TStart();

ts.run();

}

public void run(){

System.out.println("Thread starting");

}

}

 

练习题答案

答案 1

3Compile time error: class Runt is an abstract class.它不能被实例化.

这个类实现了Runnable接口,但是没有定义run()方法。

 

答案 2

3)不管是继承Thread对象还是实现Runnable接口,都要使用start()方法来让该线程开始运行。

4) 实现Runnable接口只需要定义一个run()的方法。

 

答案 3

1) 编译输出从0-99

尽管如此,注意到这段代码并没有让线程运行,run()方法不应该这样被调用。

 

答案 4

2)当你让线程跑起来的时候运行的实际上是run()方法里面的代码。
3)
线程之间可以彼此共享数据信息。
4)
当你需要一个线程开始运行的时候调用的是start()方法而不是run()方法。

你不需要导入额外的类,因为线程是Java语言的一部分。

 

 

答案 5

1) 仅选项1是一个有效的方式开始一个新的线程执行。 选项2的代码继承Runnable接口但没意义,因为Runnable是接口不是类,接口使用implements关键字。 选项3的代码直接地调用起动方法。 如果您运行这个代码您将发现文本输出,但由于直接调用方法,并不是因为一个新的线程在运行。 选项4也一样,直接地调用运行线程仅是另一个方法,并且象其他的一样执行。

目标二 何时线程会被阻止运行

对什么情况下线程会被阻止运行有一定认识。

关于该目标的解释

“线程会被阻止运行”的表述看上去很笼统,它的意思是该线程已经被人为的暂停?还是这个线程已经被彻底销毁?其实“线程会被阻止运行”的意思是线程被阻塞了。

可能造成线程阻塞的原因

线程阻塞的原因可能是

  1. 线程已经被设置了一定长度的睡眠时间。

  2. 调用了suspend()方法,它将一直保持阻塞直到resume()方法被调用。

  3. 该线程因为被调用了wait()方法被暂停了,当收到notify或者notifyAll消息的时候该线程会重新被激活。

出于对付考试的原因,sleep()notifynotifyAll()是这些造成线程组塞的原因非常需要掌握的。

 

sleep()方法是一个静态的可以暂停线程一定毫秒时间长度的方法。还有一个版本可以支持设定睡眠的时间单位为十亿分之一秒的版本。我认为没有多少人会在有如此精确的机器或者实现Java的平台上进行工作。下面是一个展示线程如何进入睡眠状态的例子,注意这个sleep()方法是如何抛出InterruptedException异常的。

public class TSleep extends Thread{

public static void main(String argv[]){

TSleep t = new TSleep();

t.start();

}

 

public void run(){

try{

while(true){

this.sleep(1000);

System.out.println("looping while");

}

}catch(InterruptedException ie){}

}

}

 

 

Java2版本发布的时候,Thread类里面的stop(),suspend()resume()方法已经被认为是过时的了(不提倡继续再使用,并且在编译期会报出警告提示)。同时JDK文档认为

//Quote

这种方式因为它固有的造成死锁可能的原因也不再提倡使用了。当目标线程正在所锁定保护系统的临界资源的监视器时候因为被暂停而保持阻塞状态,其他线程将不能再读写该临界资源直到该目标线程解除死锁状态。如果一个线程解除该目标线程的组塞,而同时又试图在调用resume()之前保持该监视器的锁定状态,那么将会造成一个死锁。这样的死锁具有代表性,就像”frozen”进程一样。需要更多的信息请参考为什么Thread.stop, Thread.suspendThread.resume不提倡再被继续使用的原因。

//End Quote

线程的通过wait/notify的协议来进行阻塞操作将在下一个目标中进行表述。

 

 

使用Thread包中的yield方法

由于Java线程对平台依赖的本质,你不能保证一个线程会把对CPU资源的使用权移交给另一个线程。某些操作系统的线程调度规则会自动给不同的线程分配CPU的占有时间。而另一些操作系统则仅仅是让线程独享处理器资源。因为上述原因,JavaThread包里面构造了一个静态的名叫yield()的方法可以让当前正在运行状态的线程让出正在占用的CPU周期。该进程则返回“准备运行”状态,这样线程规划系统可以有机会让其他线程进来调用CPU资源运行。如果没有其他的线程在“准备运行”运行状态,则刚刚让出CPU资源的线程马上重新恢复到运行状态。

 

 

 

限制/抢占

每一个线程都有一个设定好的CPU占用周期来运行。一旦它用完了设定好的一个CPU占用周期时间,那么它将停止占用CPU资源以让其他正在等待中的线程获得机会运行。当一个线程进入之前设定好的CPU占用时间那么它的一个新的运行周期就又开始了。这种机制的好处就在于你可以让所有的线程都跑起来而花费最少的时间。

 

 

 

没有时间 限制/共享

优先级系统将会决定哪个线程将会运行。一个相对来说最高优先级的线程将会获得时间来运行。一段运行在该系统中的程序必须使自己能够自动地让每个线程让出CPU资源的占用,让所有线程共享CPU资源。

 

 

 

Java线程的优先级

Java考试并不认为你需要对系统如何设置线程的优先级。尽管知道这些机制是非常有的。同时这样的局限性让你意识到Thread包中的yield()方法的重要性是非常有用的。你可以通过Thread包中Thread.setPriority来设置线程的优先级,你可以通过getPriority来获得线程的优先级,一个新建线程的默认优先级是Thread.NORM_PRIORITY

 

 

 

练习题

习题1)当你试图编译运行下列代码的时候会发生什么?

public class TGo implements Runnable{

public static void main(String argv[]){

TGo tg = new TGo();

Thread t = new Thread(tg);

t.start();

}

public void run(){

while(true){

Thread.currentThread().sleep(1000);

System.out.println("looping while");

}

}

}

1) Compilation and no output
2) Compilation and repeated output of "looping while"
3) Compilation and single output of "looping while"
4) Compile time error

习题2)下面哪种方式是推荐的让线程阻塞的方式?

1) sleep()
2) wait/notify
3) suspend
4) pause

 

习题3)下列哪一项表述是正确的?

1) The sleep method takes parameters of the Thread and the number of seconds it should sleep
2) The sleep method takes a single parameter that indicates the number of seconds it should sleep
3) The sleep method takes a single parameter that indicates the number of milliseconds it should sleep
4) The sleep method is a static member of the Thread class

 

习题4)下列哪一项表述是正确的?

Which of the following statements are true?

1) A higher priority Thread will prevent a lower priorty Thread from getting any access to the CPU.

2) The yield method only allows any higher priority priority thread to execute.

3) The Thread class has a static method called yield

4) Calling yield with an integer parameter causes it to yield for a specific time

 

练习题答案

答案1

Compile time error

sleep()方法将会抛出InterruptedException异常。除非让这个代码段放到try/catch块里面去,否则这段代码将无法编译。

 

 

答案2


1) sleep()
2) wait/notify

Java2
版本里面suspend()方法已不推荐再继续使用。

 

答案3

3) sleep()方法只需要一个表示线程睡眠时间长度的参数。

4) sleep()方法是Thread类里面的一个静态方法。

 

答案4

线程类有一个静态方法yield,调用它可以允许任何等待的线程按照底层操作系统的计划安排执行.没有带一个整数型参数的yield方法.是否高优先级的线程比低优先级能获得更多的CPU时间与平台有关,并不确定。

目标三 何时线程会被阻止运行

编写代码的时候使用同步的wait ,notify notifyAll方法,以防止并行读取问题的发生,同时保证各个线程之间的正常通信。当执行同步的wait,notify notifyAll方法的时候对线程和线程之间以及线程和对象锁之间的内部交互进行定义。

为什么你需要wait/notify 法则?

一 个更容易理解的方式,比如你想象一下数据库中的一条整型的变量数据,如果你没有一些锁定数据的措施的话,你将会面临数据污染的危险。这样一个用户可以将这 条数据取出来,经过一定运算后再放回去。期间如果其他的用户也将该数据取出来进行运算后返回,那么第一个用户运算后返回的数据将会失效。就像数据库在任何 事先不可知的情况下要处理更新一样,所以一个多线程程序必须要有应付这种可能性的机制。为了考试,你十分有必要对本目标的内容进行研究,一些十分有经验的Java程序员对wait/notify法则也并不是十分了解,这是一个普遍现象,强烈建议读者写一些简单的程序来熟悉这个法则,并对后面的模仿考试的练习题进行针对性的练习。

 

 

 

一个银行/帐户 的例子

下面的代码讲解了同步的线程之间对同一个数据进行操作。它一个名叫bank的类,它主要用来驱动多个运行着Business类中的数据处理方法的线程。Bussiness线程实际上就是对 Account里面的金额进行加减操作。下面代码的思想展示了多线程是如何“踩到对方的脚”并造成数据污染的,但是是有代码可以阻止这类事情发生的。为了“修复”已经存在的这个数据污染我调用了sleep()方法,你可以认为是等同于暂停,当bank中有代码写入操作数据库的时候。如果没有调用这个sleep()方法,数据污染发生的可能性就依然存在。你不得不运行很多次程序,这样才能让这个毛病显现出来。

public class Account{

private int iBalance;

public void add(int i){

iBalance = iBalance + i;

System.out.println("adding " +i +" Balance = "+ iBalance);

}

public void withdraw(int i){

if((iBalance - i) >0 ){

try{

Thread.sleep(60);

}catch(InterruptedException ie){}

iBalance = iBalance - i;

}else{

System.out.println("Cannot withdraw, funds would be < 0");

}

if(iBalance < 0){

System.out.println("Woops, funds below 0");

System.exit(0);

}

 

System.out.println("withdrawing " + i+ " Balance = " +iBalance);

}

public int getBalance(){

return iBalance;

}

}

 

 

 

关键字synchronized

关键字synchronized可以用在标记一段声明或者锁定一段代码,保证在同一时间只有一个线程能够运行它的一个实例。进入这段代码将会受到负责监视它的监视器的保护。这个过程是由一个锁定系统实现的。你也可以看到用监控,或者使用互斥来形容(互不相关)。一个锁分配给一个对象以保证同一时间只能有一个线程的进入,因此当一个线程试图进入的时候必须试图获得这个锁的许可。其它的线程将无法进入这段代码,知道第一个进入的线程完成然后释放这个锁。请注意的是这里的锁是基于对象而不是基于方法的。

 

 关键字synchronized 放在方法的名字之前,如:

synchronized void amethod() { /* method body */}

 

关键字synchronized也可放在代码段的括号之前,如:

 

synchronized (ObjectReference) { /* Block body */ }

 

注释的部分是指对象或者类的里面某段需要监视器需要锁定的部分。大部分情况下我们使用的是前者,而不是后者。

 

当一个被关键字synchronized标记的代码开始执行以后,拥有它的对象将保持锁定状态,它将不能被调用直到锁定状态被解除。

 

synchronized void first();

synchronized void second();

 

 

有更好的办法比在一个代码块前加上关键字synchronized能获得串行化的好处,它必须用于联接管理可串行化代码锁的代码 。



wait/notify

除了锁可以获得和释放以外,每个对象都会暂停或者进入等待当其它的线程获得这个锁的时候。这使得线程之间沟通情况随时运行。由于Java语言的单继承性,每一个子类都是继承自最原始的Object对象,从它那获得这个线程级的通信能力。


wait notify 应该放在Synchronized 关键字标记的代码中以保证当前的代码在监视器的监控之中。


在一个标记为synchronized的代码中调用wait()方法,会造成运行这段代码的这个线程交出锁的权限并进入睡眠状态。这种情况通常是为了其他的线程来接管这个锁以进行下一步操作。如果没有让线程唤醒并重新进入运行状态的notify()或者notifyAll()方法的话,那么wait()方法也变得毫无意义。一个典型的使用wait()/notify()法则来让线程之间进行通信的例子,看上去它似乎陷入了死循环。

//producing code

while(true){

try{

wait();

}catch (InterruptedException e) {}

}

 

//some producing action goes here

notifyAll();

如果真的是这样,那这段代码真的是垃圾。当你第一眼看到这段代码的时候你会感觉它会一直这样运行下去。其实wait()会告诉它交出锁让其它线程运行,直到你调用了notify或者notifyAll方法。


线程调度不是独立的,不能依靠虚拟机让它用同一种方式运行。

不象Java的大部分特性,线程在不同的平台上会有不同的表现。这两点分别是线程的优先级和线程的调度。线程调度的两种途径是:抢占和时间片

在一个可以进行抢占的系统上,程序可以通过抢占来获得CPU独享周期。在一个实行时间片分配的系统上,每个线程都会获得一个CPU独享周期,然后进入准备运行状态。这样可以确保不会让一个线程一直独享CPU。缺点在于你无法预测这个线程会运行多长时间才会结束,也无法预测什么时候这个线程会再运行,因此通常建议你使用notify或者notifyAll方法。尽管Java把线程的优先级按1-10从低到高来分配。一些平台能够识别这个优先级的属性,但其它的却不能。


notify方 法会唤醒一个线程让它进入重新要求获得某对象的监控权限。你不能确定哪个线程被唤醒了。如果你只是有一个线程被唤醒了当然不会存在这种问题。如果你有很多 线程等待唤醒,那么等待最长时间的那个将被唤醒。尽管如此,你依然不能确定,线程的优先级对最后结果也有影响。因此一般推荐你使用notifyAll而不是notify,不要对线程的优先级和调度进行任何假设。你可能要让你的代码在尽量多的平台上运行来测试一下,当然,并不总是这样的。

练习题

问题 1)下列哪一个关键字表示该线程放弃了该对象的锁?

1) release
2) wait
3) continue
4) notifyAll

问题 2)下列哪一是关于关键字synchronized的表述是最合适的?

1) Allows more than one Thread to access a method simultaneously
2) Allows more than one Thread to obtain the Object lock on a reference
3) Gives the notify/
notifyAll keywords exclusive access to the monitor
4) Means only one thread at a time can access a method or block of code

问题 3)当你试图编译运行下列代码的时候会发生什么?

public class WaNot{

int i=0;

public static void main(String argv[]){

WaNot w = new WaNot();

w.amethod();

}

 

public void amethod(){

while(true){

try{

wait();

}catch (InterruptedException e) {}

i++;

}//End of while

 

}//End of amethod

}//End of class

 

1)Compile time error, no matching notify within the method

2)Compile and run but an infinite looping of the while method

3)Compilation and run

4)Runtime Exception "IllegalMonitorStatException"

问题 4)你如何使用wait/notify法则指定某个线程被唤醒?

1) Pass the object reference as a parameter to the notify method
2) Pass the method name as a parameter to the
notify method
3) Use the
notifyAll method and pass the object reference as a parameter
4) None of the above

问题 5)下列哪项表述是正确的?

1) Java uses a time-slicing scheduling system for determining which Thread will execute
2) Java uses a pre-emptive, co-operative system for determining which Thread will execute
3) Java scheduling is platform dependent and may vary from one implementation to another
4) You can set the priority of a Thread in code

练习题答案

答案 1

Wait

答案 2

以为着在同一时间内只有一个线程对该代码段或方法进行操作。

答案 3

Runtime Exception “IllegalMonitorStateException”

wait/notify法则只能在被标记为synchronized的代码段里面使用,在该题这种情况下调用代码会抛出异常。

答案 4

4) None of the above.

wait/notify法则没有为哪个线程将被激活提供方法。

答案 5

3) Java 的调度平台不是独立的,它最终的实现结果是不确定的。
4)
你可以在代码中为代码设定优先级。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值