注解与多线程
任务1使用注解描述程序代码
关键步骤如下。
➢使用内建注解。
➢使用元注解。
➢自定义注解。
➢使用反射读取注解信息。
认识注解
Java注解也就是Annotation,是Java代码里的特殊标记,它为Java程序代码提了一种形式化的方法,用来表达额外的某些信息,这些信息代码本身是无法表示的。可以方便地使用注解修饰程序元素,这里的程序元素包括类、方法、成员变量等。
注解以标签的形式存在于Java代码中,注解的存在并不影响程序代码的编译和执行,它只是用来生成其他的文件或使我们在运行代码时知道被运行代码的描述信息。注解的语法很简单,只需在程序元素前面加上“@”符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。
注解使用的语法格式如下。
@Annotation(参数)
其中:
➢Annotation 为注解的类型。
➢注解的参数可以没有,也可以有一个或多个。
例如下面3行代码分别为不带参数的注解、带一个参数的注解及带两个参数的注@Override
@SuppressWarnings(value=“unused”)
@MyTag(name*“Jack”,age=20)
使用注解语法时,需要注意以下规范。
➢将注解置于所有修饰符之前。
➢通常将注解单独放置在一行。
➢默认情况下,注解可用于修饰任何程序元素,包括类、方法和成员变量等。
注解分类
在Java中,根据注解的使用方法和用途,可将注解分为3类,分别是内建注解、元注解以及自定义注解。
1.内建注解
在JDK 5.0版本的java.lang包下提供了3种标准的注解类型,称为内建注解,分别是@Override注解、@Deprecated 注解以及@SuppressWarnings注解。
(1) @Override注解
@Override注解被用来标注方法,它用来表示该方法是重写父类的某方法。@Override注解的用法非常简单,只要在重写的子类方法前加上@Override即可。如下程序中就使用了@Override注解来标识子类Apple的getObjectInfo()方法是重写的父类的方法。
public class Fruit {
public void getObjectInfoO
{
System.out.println(" 水果的getObjectInfo方法");
}
}
public class Apple extends Fruit {
//使用@Override指定下面的方法必须重写父类方法
@Override
public void getObjectInfo() {
System.out.println("苹果重写水果的getObjectInfo 方法");
}
}
(2) @Deprecated注解
@Deprecated注解标识程序元素已过时。如果一个程序元素被@Deprecated注解修饰,则表示此程序元素已过时,编译器将不再鼓励使用这个被标注的程序元素。如果使用,编译器会在该程序元素上画一条斜线,表示此程序元素已过时。例如下面的代码中,getObjectInfo()方法将被标识为已过时的方法。
@Deprecated
public void getObjectInfo(){
System.out.println("苹果重写水果的goOjediflo方法");
}
(3)@SuppressWarnings注解
@SuppressWarnings注解标识阻止编译器警告,被用于有选择地关闭编译器对类,方法和成员变量等程序元素及其子元素的警告。@SuppressWarnings注解会一直作用于该程序元素的所有子元素。例如下面的代码中,使用@SuppressWarnings(value=“unchecked”)注解来标识来Fruit()类取消类型检的编译器警告。·
//使用@ SuppressWarnings(value="unchecked");
@SuppressWarnings
public class Fruit {
....
}
上述代码中,“unchecked”是@SuppressWarnings注解的参数。@SuppressWarnings注解常用的参数如下。
➢deprecation:使用了过时的程序元素。
➢unchecked:执行了未检查的转换。
➢unused: 有程序元素未被使用。
➢fallthrough: switch 程序块直接通往下一种情况而没有break。
➢path:在类路径、源文件路径等中有不存在的路径。
➢Serial: 在可序列化的类上缺少seilVersionUID定义。
➢finally: 任何finally子句不能正常完成。
➢all: 所有情况。
注意
当注解类型里只有一个value成员变量,使用该注解时可以直接在注解后的括号中指定value成员变量的值,而无须使用name=value结构对的形式。在@SuppressWarnings注解类型中只有一个value成员变量,所以可以把"value=” 省略掉,例如:
@SuppressWarnings( {"unchecked","fallthrough"});
如果@SuppressWarnings注解所声明的被禁止的警告个数只有一个时,则可不用大括号,例如:
@SuppressWarnings("unchecked");
2.元注解
java.lang.annotaion包下提供了4个元注解,它们用来修饰其他的注解定义。这4个元注解分别是@Target注解、@Retention注解、@Documented注解、@Inherited注解
(1)@Target注解
@Target注解用于指定被其修饰的注解能用于修饰哪些程序元素,@Targt注解类
型有唯一的value作为成员变量。这个成员变量是Java.lang.annotaion.ElementType类型,ElementType类型是可以被标注的程序元素的枚举类型。@Target的成员变量value为如下值时,则可指定被修饰的注解只能按如下声明进行标注,当value为FIELD时,被修饰的注解符只能用来修饰成员变量。
➢ElemenTyp.ANNOTATION_TYPE:注解声明。
➢ElementType.CONSTRUCTOR:构造方法声明。
➢ElementType.FIELD:成员变量声明。
➢ElementType.LOCAL _VARIABLE:局部变量声明。
➢ElementType.METHOD:方法声明。
➢ElementType.PACKAGE: 包声明。
➢ElementType.PARAMETER: 参数声明。
➢ElemenType.TYPE: 类、接口(包括注解类型)或枚举声明。
(2) @Retention注解
@Retention注解描述了被其修饰的注解是否被编译器丢弃或者保留在class文件中。默认情况下,注解被保存在class文件中,但在运行时并不能被反射访问。
@Retention 包含一个RetentionPolicey 类型的value成员变量,其取值来自java.lang.anotio.RetentionPolicy的枚举类型值,有如下3个取值。
➢RetentionPolicy.CLASS: @Retention 注解中value成员变量的默认值,表示编译器会把被修饰的注解记录在class文件中,但当运行Java程序时,Java虚拟机不再保留注解,从而无法通过反射对注解进行访问。
➢RetentionPolicy.RUNTIME: 表示编译器将注解记录在class文件中,当运行Java程序时,Java虚拟机会保留注解,程序可以通过反射获取该注解。
RetentionPolicy.SOURCE:表示编译器将直接丢弃被修饰的注解。
下面是定义@Retention注解类型的示例代码,通过将value成员变量的值设为RetentionPolicy. RUNTIME,指定@Retention注解在运行时可以通过反射进行访问。@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_ TYPE)
public @interface Retention {
RetentionPolicy value();
}
(3) @Documented注解
@Documented注解用于指定被其修饰的注解将被JavaDoc工具提取成文档,如果在定义某注解时使用了@Documented修饰,则所有使用该注解修饰的程序元素的API文档中都将包含该注解说明。另外,@Documented 注解类型没有成员变量。
(4) @Inherited注解
@Inherited注解用于指定被其修饰的注解将具有继承性。也就是说,如果一个使用了@Inherited注解修饰的注解被用于某个类,则这个注解也将被用于该类的子类。
3. 自定义注解
前面介绍了JDK提供的3种内建注解及4种元注解。下面介绍自定义注解。
注解类型是一种接口,但它又不同于接口。 定义一个新的注解类型与定义一个接口非常相似,定义新的注解类型要使用@inteface关键字,如下代码定义了一个简单的注解类型。
public @interface AnnotationTest{}
注解类型定义之后,就可以用它来修饰程序中的类、接口、方法和成员变量等程序元素。自定义注解在实际的开发中使用的为频率并不是很多,能够理解其基本用法即可
读取注解信息
java.lang.reflect包主要包含一些实现反射功能的工具类, 另外也提供了对读取运行时注解的支持。java.lang.reflect包下的AnnotatedElement接口代表程序中可以接受时注解
的程序元素,该接口有如下几个实现类。
➢Class:类定义。
➢Constructor:构造方法定义。
➢Field:类的成员变量定义。
➢Method: 类的方法定义。
➢Package:类的包定义。
java.lang.reflect.AnnotatedElement接口是所有程序元素的父接口,程序通过反射获得某个类的AnnotatedElement对象(如类、方法和成员变量),调用该对象的3个方法就可以来访问注解信息。
➢getAnnotation()方法:用于返回该程序元素 上存在的、指定类型的注解, 如果类型的注解不存在,则返回null。
➢getAnnotations()方法:用来返回该程序元素上存在的所有注解。
➢isAnnotationPresent()方法:用来判断该程序元素上是否包含指定类型的注解,如果存在则返回true,否则返回false。
例如下面代码中,获取了MyAnnotation类的getObjectInfo()方法的所有注解,并输出。
public class MyAnnotation {
//获取MyAnnotation类的getObjectInfo()方法的所有注解
Annotation[] arr = Class.forName("MyAnnotation").getMethod("getObjectInfo").getAnnotations();
//遍历所有注解
for(Annotation an:arr)
{
System.out.println(an);
}
}
需要注意,这里得到的注解,都是被定义为运行时的注解,即都是用@Retention(RetentionPolicy.RUNTIME)修饰的注解,否则,通过反射得不到这个注解信息。因为当一个注解类型被定 义为运行时注解,该注解在运行时才是可见的。当class文件被装载时,保存在class文件中的注解才会被Java 虚拟机所读取。
有时候需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据。
任务2使用多线程输出数列
关键步骤如下。
➢创建线程。
➢启动线程。
➢调度线程。
认识线程
计算机的操作系统大多采用多任务和分时设计,多任务是指在一个操作 系统中可以同时运行多个程序,例如,可以在使用QQ聊天的同时听音乐,即有多个独立运行的任务,每个任务对应一个进程, 每个进程又可产生多个线程。
1.进程
认识进程先从程序开始。程序(Program)是对数据描述与操作的代码的集合,如Office中的Word、暴风影音等应用程序。
进程(Process) 是程序的次动态执行过程, 它对应了从代码加载、执行至执行完毕的一个完整过程, 这个过程也是进程本身从产生、发展至消亡的过程。操作系统同时管理一个计算机系统中的多 个进程,让计算机系统中的多个进程轮流使用CPU资源,或者共享操作系统的其他资源。
进程有如下特点。
➢进程是系统运行程序的基本单位。
➢每一个进程都有自己独立的一块内存空间、一组系统资源。
➢每一个进程的内部数据和状态都是完全独立的。
当一个应用程序运行的时候会产生一个进程, 如任务管理器中的进程。
2.线程
线程是进程中执行运算的最小单位,一个进程在其执行过程中可以产生多个线程,而线程必须在某个进程内执行。
线程是进程内部的一个执行单元,是可完成一个独立任务的顺序控制流程,如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程。
线程按处理级别可以分为核心级线程和用户级线程。
(1)核心级线程
核心级线程是和系统任务相关的线程,它负责处理不同进程之间的多个线程。允许不同进程中的线程按照同一相对优先 调度方法对线程进行调度, 使它们有条不紊地工作,可以发挥多处理器的并发优势,以充分利用计算机的软硬件资源。
(2)用户级线程
在开发程序时,由于程序的需要而编写的线程即用户级线程,这些线程的创建、执行和消亡都是在编写应用程序时进行控制的。对于用户级线程的切换,通常发生在一个应用程序的诸多线程之间,如迅雷中的多线程下载就属于用户线程。
多线程可以改善用户体验。具有多个线程的进程能更好地表达和解决现实世界的具体问题,多线程是计算机应用开发和程序设计的一项重要的实用技术。
线程和进程既有联系又有区别,具体如下:
➢一个进程中至少要有一个线程。
➢资源分配给进程,同一进程的所有线程共享该进程的所有资源。
➢处理机分配给线程,即真正在处理机上运行的是线程。
3.多线程的优势
多线程有着广泛的应用,下载工具“迅雷”是一款典型的多线程应用程序,在这个下载工具中,可以同时执行多个下载任务。这样不但能够加快下载的速度,减少等待时间,而且还能够充分利用网络和系统资源。
多线程的好处如下。
➢多线程程序可以带来更好的用户体验,避免因程序执行过慢而导致出现计算机死机或者白屏的情况。
➢多线程程序可 以最大限度地提高计算机系统的利用效率,如迅雷的多线程下载。
编写线程类
每个程序至少自动拥有一个线程,称为主线程。当程序加载到内存时启动主线程。Java程序中的public static void main()方法是主线程的入口,运行Java程序时,会先执行这个方法。
开发中,用户编写的线程一般都是 指除了主线程之外的其他线程。
使用一个线程的过程可以分为如下4个步骤。
(1)定义一个线程,同时指明这个线程所要执行的代码,即期望完成的功能。
(2)创建线程对象。
(3)启动线程。
(4)终止线程。
定义一个线程类通常有两种方法,分别是继承java.lang. Thread类和实现java.lang.Runnable接口。
1.使用Thread类创建线程
Java提供了java.lang.Thread类支持多线程编程,该类提供了大量的方法来控制和操作线程,常用方方法如表4-1所示。
表4-1 Thread类的常用方法
方法 | 说明 |
---|---|
void run() | 执行任务操作的方法 |
void start() | 使该线程开始执行 |
void seep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) |
String getName() | 返回该线程的名称 |
int getPriority() | 返回线程的优先级 |
void setPriority(int newPriority) | 更改线程的优先级 |
Thread.State getState() | 返回该线程的状态 |
boolean isAlive() | 测试线程是否处于活动状态 |
void join() | 等待该线程终止 |
void interrupt() | 中断线程 |
void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
创建线程时继承Thread类并重写Thread类的run()方法。Thread 类的run()方法是线程要执行操作任务的方法,所以线程要执行的操作代码都需要写在run()方法中,并通过调用start()方法来启动线程。
示例1
使用继承Thread类的方式创建线程,在线程中输出1~ 100的整数。
实现步骤如下。
(1)定义MyThread类继承Thread类,重写run()方法,在run()方法中实现数据输出。
(2)创建线程对象。
(3)调用start()方法启动线程。
关键代码:
public class MyThread extends Thread {
int count;
//通过继承Thread 类来创建线程
//重写run(方法
public void run() {
while (count < 100) {
count++;
System.out.println("count的值是: " + count);
}
}
//启动线程
public static void main(String[] args) {
MyThread mt = new MyThread();//实例化线程对象
mt.start();//启动线程
}
}
由示例1可以看出,创建线程对象时不会执行线程,必须调用线程对象的start()方法才能使线程开始执行。
2. 使用Runnable接口创建线程
使用继承Thread类的方式创建线程简单明了,符合大家的习惯,但它也有一个缺点,如果定义的类已经继承了其他类则无法再继承Thread类。使用Runnable接口创建线程的方式可以解决上述问题。
Runnable接口中声明了一个run()方法,即public void run()。一个类可以通过实现Runnable接口并实现其run()方法完成线程的所有活动,已实现的run()方法称为该对象的线程体。任何实现Runnable接口的对象都可以作为一个线程的目标对象。
示例2
使用Runnable接口的方式创建线程,在线程中输出1 ~ 100的整数。
实现步骤如下。
(1)定义MyThread类实现java.lang.Runnable接口,并实现Runnable接口的run()方法,在run()方法中输出数据。
(2)创建线程对象。
(3)调用start()方法启动线程。
关键代码:
//通过实现Runnable接口创建线程
public class MyThread implements Runnable {
private int count = 0;
//实现run0方法
public void run() {
while (count < 100) {
count++;
System.out.println("count的值是:" + count);
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyThread());//创建线程对象
t.start();//启动线程
}
}
示例2中,通过Thread thread new Thread(new MyThread()创建线程对象。
两种创建线程的方式有各自的特点和应用领域:直接继承Thread类的方式编写简单,可以直接操作线程,适用于单重继承的情况;实现Runnable接口的方式,当一个线程继承了另一个类时,就只能用实现Runnable接口的方法来创建线程,而且这种方式还可以使多个线程之间使用同一个 Runnable对象。
3.使用Callable接口创建线程
public class TestCallable implements Callable<Integer> {
public Integer call() throws Exception{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+(i+1));
}
return 3;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// TestCallable t=new TestCallable();
// FutureTask<Integer> f=new FutureTask<Integer>(t);
// Thread th=new Thread(f);
// th.start();
// System.out.println(f.get());//获取返回值
ExecutorService single=Executors.newSingleThreadExecutor();//单线程
ExecutorService executorService=Executors.newFixedThreadPool(3);//3个线程对象
single.execute(new Runnable() {
@Override
public void run() {
}
});
}
}
线程的状态
线程的生命周期可以分成4个阶段,即线程的4种状态,分别为新生状态、可运行状态、阻塞状态和死亡状态。一个具有生命的线程,总是处于这4种状态之一。线程的生命周期如图4.2所示。
1. 新生状态(New Thread )
创建线程对象之后,尚未调用其start()方法之前,这个线程就有了生命,此时线程仅仅是一个空对象,系统没有为其分配资源。此时只能启动和终止线程,任何其他操作都会引发异常。
2.可运行状态 ( Runnable)
当调用start()方法启动线程之后,系统为该线程分配除CPU外的所需资源,这个线程就有了运行的机会,线程处于可运行的状态,在这个状态当中,该线程对象可能正在运行,也可能尚未运行。对于只有一个CPU的机器而言,任何时刻只能有一个处于可运行状态的线程占用处理机,获得CPU资源,此时系统真正运行线程的run()方法。
3.阻塞状态( Blocked )
一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态。阻塞状态是一种“不可运行”的状态,而处于这种状态的线程在得到一个特定的事件之后会转回可运行状态。
导致一个线程被阻塞有以下原因:
➢调用了Thread类的静态方法sleep()。
➢一个线程执行到一个I/O操作时,如果I/O操作尚未完成,则线程将被阻塞。
➢如果一个线程的执行需要得到一个对象的锁,而这个对象的锁正被别的线程占用,那么此线程会被阻塞。
➢线程的suspend()方法被调用而使线程被挂起时,线程进入阻塞状态。但suspend()容易导致死锁,已经被JDK列为过期方法,基本不再使用。
处于阻塞状态的线程可以转回可运行状态,例如,在调用sleepO()法之后,这个线程的睡眠时间已经达到了指定的间隔,那么它就有可能重新回到可运行状态。或当一个线程等待的锁变得可用的时候,那么这个线程也会从被阻塞状态转入可运行状态。
4.死亡状态( Dead )
一个线程的run()方法运行完毕、stop()方法被调用或者在运行过程中出现未捕获的
异常时,线程进入死亡状态。
线程调度
当同一时刻有多个线程处于可运行状态,它们需要排队等待CPU资源,每个线程会自动获得一个线程的优先级(Priority),优先级的高低反映线程的重要或紧急程度。可运行状态的线程按优先级排队,线程调度依据建立在优先级基础上的“先到先服务”原则。
线程调度管理器负责线程排队和在线程间分配CPU,并按线程调度算法进行调度。当线程调度管理器选中某个线程时,该线程获得CPU资源进入运行状态。
线程调度是抢占式调度,即在当前线程执行过程中如果有一个更高优先级的线程进入可运行状态,则这个更高优先级的线程立即被调度执行。
1.线程优先级
线程的优先级用1~ 10表示,10 表示优先级最高,默认值是5。每个优先级对应一个Thread类的公用静态常量。例如:
public static final int NORM_ PRIORITY=5;
public static final int MIN PRIORITY=1;
public static final int MAX_ PRIORITY=10;
每个线程的优先级都介于Thread MIN PRIORITY和Threaed MAX PRIRITY之间。线程的优先级可以通过setPriority(int grade)方法更改,此方法的参数表示要设置的优先级,它必须是一个1~ 10的整数。例如,myThread setPriority(3);将线程对象myThread的优先级别设置为3。
2. 实现线程调度的方法
(1)join()方法
joinI() 方法使当前线程暂停执行,等待调用该方法的线程结束后再继续执行本线程。它有3种重载形式。
public final void join()
public final void join(long mill)
public final void join(long mills,int nanos)
下面通过示例具体介绍join()方法的应用。
示例3
使用join()方法阻塞线程。
实现步骤如下。
1)定义线程类,输出5次当前线程的名称。
2)定义测试类,使用join0方法阻塞主线程。
关键代码:
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
public void run() {
for (int i = 0; i < 5; i++) {
//输出当前线程的名称
System.out.println((Thread.currentThread().getName() + "" + i));
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
if (i == 5) {//主线程运行5次后,开始MyThread线程
MyThread tempjt = new MyThread(("MyThread"));
try {
tempjt.start();
tempjt.join();//把该线程通过join()方法插入到主线程前面
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "" + i);
}
}
}
}
在示例3中,使用join()方法阻塞指定的线程直到另一个线 程完成以后再继续执行。其中tempjt.join()表示让当前线程即主线程加到tempjt的末尾,主线程被阻塞,tempjt执行完以后主线程才能继续执行。Thread.currentThread().getName() 用于获取当前线程的名称。
从线程返回数据时也经常使用到join(方法。
示例4
使用join()方法实现两个线程间的数据传递。
实现步骤如下。
1)定义线程类,为变量赋值。
2)定义测试类。
关键代码:
public class MyThread extends Thread {
public String value1;
public String value2;
public void run() {
value1 = "value1已赋值";
value2 = "value2已赋值";
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
System.out.println("valuesl:" + thread.value1);
System.out.println("values2:" + thread.value2);
}
}
在示例4中,在run()方法中已经对value1和value2赋值,而返回的却是null,出现这种情况的原因是在主线程中调用start()方法后就立刻输出了value1和value2的值,而run()方法可能还没有执行到为value1和value2赋值的语句。要避免这种情况的发生,需要等到run()方法执行完后才执行输出value1和value2的代码,可以使用join() 方法来解决这个问题。修改示例4的代码,在“thread.start();“后添加“thread.join();”。
(2)sleep()方法
sleep()方法的语法格式如下。
public static void sleep(long milli)
sleep()方法会让当前线程睡眠(停止执行millis毫秒,线程由运行中的状态进入不可运行状态,睡眠时间过后线程会再次进入可运行状态。
示例5
使用sleep()方法阻塞线程。
实现步骤如下。
1)定义线程。
2)在run()方法中使用sleep()方法阻塞线程。
3)定义测试类。
关键代码:
public class Wait {
public static void bySec(long s) {
for (int i = 0; i < s; i++) {
System.out.println(i + 1 + "秒");
try {
Thread.sleep(1000);//睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
System.out.println("Wait");
//提示等待
Wait.bySec(5);
//让主线程等待5秒再执行
System.out.println("start");
//提示恢复执行
}
}
示例5的代码中,在执行主线程以后,首先输出了Wait,然后主线程等待5秒钟后继续执行。
(3)yield()方法
yield()方法的语法格式如下。
public static void yield()
yield()方法可让当前线程暂停执行,允许其他线程执行,但该线程仍处于可运行状态,并不变为阻塞状态。此时,系统选择其他相同或更高优先级线程执行,若无其他相同或更高优先级线程,则该线程继续执行。
示例6
使用yield()方法暂停线程。
实现步骤如下。
1)定义两个线程。
2)在run)方法中使用yield()方法暂停线程。
3)定义测试类。
关键代码:
public class FirstThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第一个线程的第" + (i + 1) + "次运行");
Thread.yield(); //暂停线程
}
}
}
public class SecThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第二个线程的第" + (i + 1) + "次运行");
Thread.yield(); //暂停线程
}
}
}
*/
public class Test {
public static void main(String[] args) {
FirstThread mt = new FirstThread();
SecThread mnt = new SecThread();
mt.start();
mnt.start();
}
}
在示例6中,调用了yield()方法之后,当前线程并不是转入被用阻塞状态,它可以与其他等待执行的线程竞争CPU资源,如果此时它,又抢占到CPU资源,就会出现连续运行几次的情况。
sleep()方法与yield()方法的区别。
表4-2sleep()方法和yield()方法的区别
sleep()方法 | yield()方法 |
---|---|
使当前线程进入被阻塞的状态 | 将当前线程转入暂停执行的状态 |
即使没有其他等待运行的线程,当前线程也会等待指定的时间 | 如果没有其他等待执行的线程,当前线程h会马上恢复执行 |
其他等待执行的线程的机会是均等的 | 会运行优先级相同或更高的线程 |
任务3使用线程同步模拟银行取款
关键步骤如下。
➢同步方法。
➢同步代码块。
线程同步的必要性
前面介绍的线程都是独立的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部资源或方法,也不必关心其他线程的状态或行为。但是经常有一些同时运行的线程需要共享数据,此时就需要考虑其他线程的状态和行为,否则就不能保证程序运行结果的正确性。
示例7
张三和他的妻子各拥有一张银行卡和存折,可以对同一个银行账户进行存取款的操作,请使用多线程模拟张三和他的妻子同时取款的过程。
实现步骤如下。
(1)定义银行账户Account类。
(2)定义取款线程类。
(3)定义测试类,实例化张三取款的线程实例和和他的妻子取款的线程实例。
关键代码:
//银行账户类
class Account {
private int balance = 500;//余额
public int getBalance() {
return balance;
}
//取款
public void withdraw(int amount) {
balance -= amount;
}
}
//取款的线程类
class TestAccount implements Runnable {
//所有用TestAccount对象创建的线程共享同一个账户对象
private Account acct = new Account();
public void run() {
for (int i = 0; i < 5; i++) {
makeWithdrawal(100);//取款
if (acct.getBalance() < 0) {
System.out.println(" 账户透支了!");
}
}
}
private void makeWithdrawal(int amt) {
if (acct.getBalance() >= amt) {
System.out.println(Thread.currentThread().getName() + "准备取款");
try {
Thread.sleep(500);//0.5秒后实现取款
} catch (InterruptedException ex) {
}
//如果余额足够,则取款
acct.withdraw(amt);
System.out.println(Thread.currentThread().getName() + "完成取款");
} else {
//余额不足给出提示
}
System.out.println("余额不足支付" + Thread.currentThread().getName() + "的取款,余额为" + acct.getBalance());
}
}
//测试类
class TestWithdrawal {
public static void main(String[] args) {//创建两个线程
TestAccount r = new TestAccount();
Thread one = new Thread(r);
Thread two = new Thread(r);
one.setName("张三");
two.setName("张三的妻子");//1启动线程
one.start();
two.start();
}
}
在示例7的代码中,首先定义了一个Account类模拟银行账户。然后定义了TestAccount类实现Runnable接口,在此类中有一个账户对象acct,即所有通过此类创建的线程都共享同一个账户对象。在测试类中,创建了两个线程,分别用于实现张三不他的妻子的取款操作。通过程序的运行结果可以看到,虽然在程序中对余额做了判断,但仍然出现了透支的情况。原因就是在取款方法中,先检查余额是否足够,如果余额够才取款,而有可能在查余额之后取款之前的这一小段时间里, 另外一个人已经完成了一次取款,因而此时的余额发生了变化,但是当前线程却还以为余额是足够的。例如,张三查询余额时发现还有100块钱,正当他打算取钱但是还没有取时他的妻子已经把这100块钱取走了,可张三并不知道,所以他也去取钱便发生了透支的情况。在开发中,要避免这种情况的发生,就要使用线程同步。
实现线程同步
当两个或多个线程需要访问同一资源时, 需要以某种顺序来确保该资源在某时刻只能被一个线程使用的方 式称为线程同步。
采用同步来控制线程的执行有两种方式,即同步方法和同步代码块。这两种方式都使用synchronized关键字实现。
1.同步方法
通过在方法声明中加入synchronized关键字来声明同步方法。
使用synchronized修饰的方法控制对类成员变量的访问。每个类实例对应一把锁,方法一旦执行,就独占该锁,直到该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对应每一个实例, 其所有声明为synchronized的方法只能有一个处于可执行状态,从而有效地避免了类成员变量的访问冲突。
同步方法的语法格式如下。
访问修饰符 synchronized 返回类型 方法名{}
或者
synchronized 访问修饰符 返回类型 方法名{}
➢synchronized 是同步关键字。
➢访问修饰符是指public、private 等。
示例8
使用同步,方法的方式解决示例7的访问冲突问题。
关键代码:
```javascript
//取款
private synchronized void makeWithdrawal amt){
//省略与示例7相同部分
}
在示例8中,使用synchronized修饰取款方法makeWithdrawal (),makeWithdrawal ()
方法成为同步方法后,当一个线程已经在执行此方法时,这个线程就得到了当前对象的锁,该方法执行完毕以后才会释放这个锁,在它释放这个锁之前其他的线程是无法同时执行此对象的makeWithdrawal ()方法的,这样就完成了对这个方法的同步。
同步方法的缺陷:如果将一个运行时间比较长的方法声明成synchronized将会影响效率。例如,将线程中的run()方法声明成synchronized,由于在线程的整个生命周期内它一直在运行,这样就有可能导致rum0)方法会执行很长时间,那么其他的线程就得直等到run()方法结束了才能执行。
2.同步代码块
同步代码块的语法格式如下。
synchronized(syncObject){
//需要同步访问控制的代码
synchronized块中的代码必须获得对象syncObject的锁才能执行,具体实现机制与同步方法一样。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
示例9
使用同步代码块的方式解决示例7的访问冲突问题。
关键代码:
private void makeWithdrawal ( int amt){
synchronized (acct) {
// 同步查询和取款的代码块
if (acct.getBalance( >= amt) {
System.out.println(Thread.currentThread().getName() + "准备取款");
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
}
//如果余额足够,则取款
acct.withdraw(amt);
System.out.println(Thread.currentThread().getName()
+ "完成取款");
} else {
//余额不足给出提示
System.out.println("余额不足支付"
+ Thread.currentThread().getName()
+ "的取款,余额为" + acct.getBalanceO);
}
}
3.死锁
多线程在使用同步机制时,存在“死锁”的潜在危险。如果多个线程都处于等待状态而无法唤醒时,就构成了死锁(Deadlock), 此时处于等待状态的多个线程占用系统资源,但无法运行,因此不会释放自身的资源。
在编程时应注意死锁的问题,避免死锁的有效方法是:线程因某个条件未满足而受阻,不能让其继续占有资源:如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁。
任务 4生产者消费者问题
关键步骤如下。
➢分析生产者消费者问题。
➢使用3种方法实现线程间通信。
线程间通信的必要性
在前面的介绍中,了解了多线程编程中使用同步机制的重要性,并介绍了如何通过同步来正确地访问共享资源。这些线程之间是相互独立的,并不存在任何的依赖关系。它们各自竞争CPU资源,互不相让,并且还无条件地阻止其他线程对共享资源的异步访问。然而,有很多现实问题要求不仅要同步地访问同一共享的资源,而且线程间还彼此牵制,相互通信。
在经典的生产者和消费者问题中,描述了如图4.9所示的情况。
显然这是一个线程同步的问题, 生产者和消费者共享同一个资源,并且生产者和消费者之间是相互依赖的。如何来解决这个问题呢?使用线程同步可以阻止并发更新同个共享资源,但是不能用它来实现不同线程之间的消息传递,要解决生产者消费者问题,需要使用线程通信。
实现线程间通信
Java提供了如下3个方法实现线程之间的通信。
➢wait()方法:调用wait()方法会挂起当前线程,并释放共享资源的锁。
➢notify()方法:调用任意对象的notify()方法会在因调用该对象的wait()方法而阻塞的线程中随机选择一个线程解除阻塞, 但要等到获得锁后才可真正执行。
➢notifyAll() 方法:调用了notifyAll() 方法会将因调用该对象的wait()方法而阻寒的所有线程一次性全部解除阻塞。
wait()、notify( 和notifyAll()这3个方法都是Object类中的final方法,被所有的类继承且不允许重写。这3个方法只能在同步方法或者同步代码块中使用,否则会抛出异常。
示例10
使用wait()方法和notify()方法实现线程间通信。
实现步骤如下。
(1)使用wait()方法挂起线程。
(2)使用notify(方法唤起线程。
关键代码:
//测试线程间通信
public class CommunicateThread implements Runnable {
public static void main(String[] args) {
CommunicateThread thread = new CommunicateThread();
Thread ta = new Thread(thread, "线程ta");
Thread tb = new Thread(thread, "线程tb");
ta.start();
tb.start();
}
//同步run()方法
synchronized public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + i);
if (i == 2) {
try {
wait();//退出运行状态,放弃资源锁,进入到等待队列
} catch (InterruptedException e) {
e.printStackTrace();
if (i == 1) {
notify();//从等待序列中唤起一个线程
if (i == 4) {
notify();
}
}
}
}
}
}
}
示例10的执行过程分析如下。
①在main()方法中启动线程ta和tb。
②由于run()方法加了同步,线程ta先执行run()方法,执行for循环输出3条数据。
③当i等于2时,执行wait()方法,挂起当前线程,并释放共享资源的锁。
④线程tb开始运行,执行for循环输出数据。
⑤当i等于1时,调用notify()方法,从等待队列唤醒一-个线程。
⑥ta等待tb释放对象锁,当i等于2时,线程tb输出完3行数据后执行wait()方法,挂起线程,释放对象锁。
⑦线程ta获得对象锁继续执行输出操作。
⑧当i等于4时,调用notify()方法唤醒线程tb。
⑨当ta执行完run()方法后释放对象锁,tb获得对象锁继续执行打印操作直至结束。
示例11
使用线程通信解决生产者消费者问题。
实现步骤如下。
(1)定义共享资源类。
面
(2)定义生产者线程类。
(3)定义消费者线程类。
(4)定义测试类。
关键代码:
//共享资源类
class SharedData {
private char c;
private boolean isProduced = false;//信号量
// 同步方法putShareChar()
public synchronized void putShareChar(char c) {
//如果产品还未消费,则生产者等待
if (isProduced) {
try {
System.out.println("消费者还未消费,因此生产者停止生产");
wait();// 生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.c = c;
isProduced = true;//标记已经生产
notify();//通知消费者已经生产,可以消费
System.out.println("生产了产品" + c + "通知消费者消费..");
}
//同步方法getShareChar()
public synchronized char getShareChar() {
//如果产品还未生产,则消费者等待
if (!isProduced) {
try {
System.out.println(" 生产者还未生产,因此消费者停止消费");
wait();//消费者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isProduced = false;//标记已经消费
notify();//通知需要生产
System.out.println("消费者消费了产品" + c + "通知生产者生产.. ");
return this.c;
}
}
// 生产者线程类
class Producer extends Thread {
private SharedData s;
Producer(SharedData s) {
this.s = s;
}
public void run() {
for (char ch = 'A'; ch < 'D'; ch++) {
try {
Thread.sleep((int) (Math.random()) * 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s.putShareChar(ch);//将产品放入仓库
}
}
}
//消费者线程类
class Consumer extends Thread {
private SharedData s;
Consumer(SharedData s) {
this.s = s;
}
public void run() {
char ch;
do {
try {
Thread.sleep((int) (Math.random()) * 3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ch = s.getShareChar();//从仓库中取出产品
}
while (ch != 'D');
}
}
class CommunicationDemo {
//测试类
public static void main(String[] args) {//共享同一个资源
SharedData s = new SharedData();//消费者线程
new Consumer(s).start();
//生产者线程
new Producer(s).start();
}
}