注解与多线程
1.认识注解
Java注解也就是Annotation,是Java代码里的特殊标记,它为Java程序代码提供了一种形式化的方法,用来表达额外的某些信息,这些信息代码本身是无法表示的。可以方便地使用注解修饰程序元素,这里的程序元素包括类、方法、成员变量等。
注解一标签的形式存在于Java代码中,注解的存在并不影响程序代码的编译和执行,它只是用来生成其他的文件或使我们在运行代码时知道被运行代码的描述信息。
-
注解的语法很简单,只需在程序元素前面加上“@”符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。
//注解使用的语法格式 @Annotation(参数) //Annotation为注解的类型 //注解的参数可以没有,也可以有一个或多个 @Override @Suppress Warning(value="unused") @Mytag(name="Jack",age=20) //以上三个代码分别为不带参数的注解、带一个参数的注解及带两个参数的注解
- 使用注解语法时,需要注意以下规范
- 将注解置于所有修饰符之前
- 通常将注解单独放置在一行
- 默认情况下,注解可用于修饰任何程序元素,包括类、方法和成员变量
- 使用注解语法时,需要注意以下规范
2.注解的分类
- 根据注解的使用方法和用途,可将注解分为3类,分别时内建注解、元注解以及自定义注解
(1)在JDK5.0版本的java.lang包下提供了3中标准的注解类型,称为内建注解,分别是@Override注解、@Deprecate的注解以及@SuppressWaning注解。
@Override注解
-
@Override注解被用来标注方法,它用来表示该方法是重写父类的某方法。@Override注解的用法非常简单,只要在重写的子类方法前加上@Override即可
//用@Override注解来标识子类Apple的getObjectInfo()方法是重写的父类的方法 public class Fruit{ public void getObjectInfo(){ System.out.println("水果的getObjectInfo方法"); } } public class Apple extends Fruit{ //使用@Override指定下面的方法必须重写父类方法 @Override public void getObjectInfo(){ System.out.println("苹果重写水果的getObjectInfo方法"); } }
-
@Deprecated注解
-
@Deprecated注解标识程序已过时。如果一个程序元素被@Deprecated注解修饰,则表示此程序已过时,编译器将不再鼓励使用这个被标注的程序元素。如果使用,编译器会在该元素上画一条斜线,表示此元素已过时
//使用@Deprecated指定下面的方法已过时 @Deprecated public void getObjectInfo(){ System.out.println("苹果重写水果的getObjectInfo 方法"); }
-
-
@SuppressWarning注解
-
@SuppressWarning注解表示阻止编译器警告,被用于有选择地关闭编译器对类、方法和成员变量等程序元素及其子元素的警告。@SuppressWarning注解会一直作用于该程序元素的子元素
//使用@SuppressWarning抑制编译器警告信息 @SuppressWarning(vlaue="unchecked"); public class Fruit{ ...... }
-
@SuppressWarnings注解常用的参数
- deprecation:使用了过时的程序元素
- unchecked:执行了未检查的转换
- unused:有程序元素未被使用
- fallthrough:switch程序块直接通往下一种情况而没有break
- path:在类路径、源文件路径等中有不存在路径
- serial:在可序列化的类上缺少serialVersionUID定义
- finally:任何finally子句不能真唱完成
- all:所有情况
-
注意:当注解类型里只有一个value成员变量,使用该注解时可以直接在注解后的括号中指定value成员变量的值,而无需使用name=value结构对的形式。在@SuppressWarning注解类型中只有一个value成员变量,所以可以把“value=”省略掉例如:
@SuppressWarning({"unchecked","falthrough"});
如果@SupressWarning注解所声明的被禁止的警告格式个数只有一个时,则可不用大括号,例如:
@SuppressWarning("unchecked");
(2)元注解java.lang.annotation包下提供了4个元注解,它们用来修饰其他的注解定义。这4个元注解分别是@Target注解、@Retention注解、@Documented注解以及@Inherited注解
-
@Target注解
- @Target注解用于指定被其修饰的注解能用于修饰那些程序元素,@Target注解类型有唯一的value作为作为成员变量。这个成员变量时java.lang.annotation.ElementType类型,ElementType类型时可以被标注的程序元素的枚举类型。@Target的成员变量value为如下值时,则可以指定被修饰的注解只能按如下声明进行标注,当value为FIELD时,被修饰的注解只能用来修饰成员变量
- ElementType.ANNOTATION_TYPE:注解声明。
- ElementType.CONSTRUCTOR:构造方法声明。
- ElementType.FIELD:成员变量声明。
- ElementType.LCAL_VARIABLE :局部变量声明。
- ElementType.METHOD:方法声明。
- ElementType.PACKAGE:包声明。
- ElementType.PARAMETER:参数声明。
- ElementType.TYPE:类、接口(包括注解类型)或枚举声明
- @Target注解用于指定被其修饰的注解能用于修饰那些程序元素,@Target注解类型有唯一的value作为作为成员变量。这个成员变量时java.lang.annotation.ElementType类型,ElementType类型时可以被标注的程序元素的枚举类型。@Target的成员变量value为如下值时,则可以指定被修饰的注解只能按如下声明进行标注,当value为FIELD时,被修饰的注解只能用来修饰成员变量
-
@Retention注解
@Retention注解描述了被其修饰的注解是否被编译器丢弃或者保留在class文件中。默认情况下,注解被保存在class文件中,但在运行时并不能被反射访问。
-
@Retention包含一个RetentionPolicy类型的value成员变量,其取值来自java.lang.annotation.RetentionPolicy的枚举类型值,有如下3个取值。
RetentionPolicy.CLASS:@Retention注解中value成员变量的默认值,表示编译器会把修饰的注解记录在class文件中,但当运行Java程序时,Java虚拟机不再保留注解,从而无法通过反射对注解进行访问
RetentionPolicy.RUNTIME:表示编译器将注解揭露在class文件中,当运行Java程序时,Java虚拟机会保留注解,程序可以通过反射获取该注解。
-
RetentionPolicy.SOURCE:表示编译器将直接丢弃被修饰的注解。
//通过将value成员变量的值设为RetentionPolicy.RUNTIME,指定@Retention注解在运行时可以通过反射来进行访问。 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention{ RetentionPolicy value(); }
-
@Documented注解
- @Documented注解用于指定被其修饰的注解将被JavaDoc工具提取成文档,如果在定义某注解是使用了@Documented修饰,则所有使用该注解修饰的程序元素API文档中都将包含该注解说明。另外,@Documented注解类型没有成员变量
-
@Inherited注解
- @Inherited注解用于指定被其修饰的注解将具有继承性。也就是说,如果一个使用了@Inherited注解修饰的注解被用于某个类被用于某个类,则这个注解也将被用于这个类的子类。
(3)自定义注解
-
注解类型是一种接口,但它又不同于接口。定义一个新的注解类型与定义一个接口非常相似,定义新的注解类型要使用@Interface关键字
public @interface AnnotationTest{}
注解类型定义之后,就可以用它来修饰程序中的类、接口、方法和成员变量等程序元素
3.读取注解信息
-
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").getAnnotation();
//遍历所有注解
for(Annotation an:arr){
System.out.println(an);
}
}
需要注意,这里得到的注解,都是被定义为运行时的注解,即都是用@Retention(RetentionPolicy.RUNTIME)修饰的注解。否则,通过反射得不到这个注解信息。因为当一个注解类型被定义为运行时注解,该注解在运行时才是可见的。当class文件被装载时,保存在class文件中的注解才会被Java虚拟机所读取
有时候需要获取某个注解里的元数据,则可以将注解强制类型转换成所需的注解类型,然后通过注解对象的抽象方法来访问这些元数据
4.认识线程
- 计算机的操作系统大多采用多任务和分时设计,多任务是指在一个操作系统中可以同时运行多个程序,例如,可以在使用QQ聊天的同时听音乐,即又多个独立运行的任务,每个任务对应一个进程,每个任务又可产生多个线程。
进程
- 认识进程先从程序开始。程序是对数据描述与操作的代码集合,如office中的word、暴风影音的应用程序。
- 进程时程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程。操作系统同时管理一个计算机系统中的多个继承,让计算机系统中的多个进程轮流使用CPU资源,或者共享操作系统的其他资源。
- 进程又如下特点
- 进程时系统运行程序的基本单位。
- 每一个进程都有自己独立的一块内存空间、一组系统资源。
- 每一个进程的内部数据和状态都是完全独立的。
线程
线程是进程中执行运算的最小单位,一个进程在其执行过程中可以产生多个线程,而线程必须在某个进程中执行。
线程是进程内部的一个执行单元,是可完成一个独立任务的顺序控制流程,如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为都线程。
线程按处理级别可分为核心级线程和用户级线程。
-
核心级线程
- 核心级线程是和系统任务相关的线程,它负责处理不同进程之间的多个线程。允许不同进程中的线程按照同一相对优先调度方法对进程进行调度,使它们有条不紊地工作,可以发挥多处理器地并发优势,以充分利用计算机地软/硬件资源。
-
用户级线程
- 在开发程序时,由于编写程序地需要而编写地线程即用户级线程,这些线程的创建、执行和消亡都是在编写应用程序时进行控制的。对于用户级线程的切换,通常发生在一个应用程序的诸多线程之间,如迅雷中的多线程下载就属于用户级线程。
- 多线程可以改善用户体验。具有多个线程的进程能更好地表达和解决现实世界的具体问题,多线程是计算机应用开发和程序设计的一项重要的实用技术。
- 线程和进程既有联系又有区别,具体如下
- 一个进程中至少要有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程所有资源。
- 处理机分配给线程,即真正在处理机上运行的是线程。
-
多线程的优势
- 多线程程序可以带来更好的用户体验,避免因程序执行过慢而导致出现计算机死机或者白屏的情况。
- 多线程程序可以最大限度地提高计算机系统的利用效率。
编写线程类
- 每个程序至少自动拥有一个线程,称为主线程。当程序加载到内存时启动主线程。Java程序中的public static void main()方法时主线程地入口,运行Java程序时,会先执行这个方法。
- 开发中用户编写的线程一般都是指除了主线程之外的其他线程。
- 使用一个线程的过程可以分为如下4个步骤
- 定义一个线程,同时指明这个线程所要执行的代码,即期望完成的功能。
- 创建线程对象。
- 启动线程。
- 终止线程。
定义一个线程类通常有两种方法,分别是继承java.lang.Thread类和实现java.lang.Runnable接口
-
使用Thread类创建线程。
java提供了java.lang.Thread类支持多线程编程,该类提供了大量的方法来控制和操作线程。
创建线程时继承Tread类并重写Thread类的run()方法。Thread类的run()方法是线程要执行操作任务的方法,所以线程要执行的操作代码需要写在run()方法中,并通过调用start()方法来启动线程
//通过继承Thread类来创建线程
public class MyThread extends Thread{
private int count=0;
public void run(){
while(count<100){
cout++;
System.out.println("count的值是:"+count);
}
}
}
//启动线程
public class Test{
public static void main(String[]args){
MyThread mt= new MyThread();//实例化线程对象
mt.start();//启动线程
}
}
创建线程对象时不会执行线程,必须调用线程对象的start()方法才能使线程开始执行。
-
使用Runnable接口创建线程
使用继承Thread类的方式创建线程简单明了,符合大家的习惯,但它也有一个缺点,如果定义的类已经继承了其他类则无法继承Thread类。使用Runnable接口创建线程的方式可以解决上述问题。
-
Runnable接口中声明一个run()方法,即public void run()。一个类可以通过实现Runnable接口并实现其run()方法完成线程的所有活动,已实现的run()方法称为该对象的线程体。任何实现Runnable接口的对象都可以作为一个线程的目标对象。
//通过实现Runnable接口创建线程 public class MyThread implements Runnable{ private int count=0; public void run(){ while(count<100){ count++; System.out.println("count的值是:"+count); } } } //启动线程 public class Test{ public static void main(String[]args){ Thread thread =new Thread(new MyThread());//创建线程对象 thread.start(); //启动线程 } }
两种创建线程的方式有各自的特点和应用领域:直接继承Thread类的方式编写简单,可以直接操作线程,适用于单继承的情况;实现Runnable接口的方式,当一个线程继承了另一个类时,就只能用实现Runnable接口的方式来创建线程,而且这种方式还可以使多个线程之间使用同一个Runnable对象
线程的状态
-
线程的声明周期可以分为4个阶段,即线程的4种状态,分别为新生状态、可运行状态、阻塞状态和死亡状态。一个具有生命的线程,总是处于这4种状态之一。
img
- 新生状态(New Thread)
- 创建线程对象之后,尚未调用其start()方法之前,这个线程就有了生命,此时线程仅仅是一个空对象,系统没有为其分配资源。此时只能启动和终止线程,任何其他操作都会引发异常。
- 可运行状态(Runnable)
- 当调用start()方法启动线程之后,系统为该线程分配出CPU外的所需资源,这个线程就有了运行的机会,线程处于可运行的状态,在这个状态当中,该线程对象可能正在运行,也可能尚未运行。对于只有一个CPU的机器而言,任何时刻只能有一个处于可运行状态的线程占用处理机,获得CPU资源,此时的系统真正运行线程的run()方法。
-
阻塞状态
一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态。阻塞状态是一种“不可运行”的状态,而处于这种状态的线程在得到一个特定的事件之后会转回可运行状态。
-
导致一个线程被阻塞有以下原因:
调用Thread类的静态方法sleep。
一个线程执行到一个I/O操作时,如果I/O操作尚未完成,则线程将被阻塞。
如果一个线程的执行需要得到一个对象的锁,而这个对象的锁整正被别的线程占用,那么此线程会被阻塞。
线程的suspend()方法被调用而使线程被挂起时,线程进入阻塞状态。但suspend()容易导致死锁,已经被JDK列为过期方法,基本不再使用。
处于阻塞状态的线程可以转回可运行状态,例如,在调用sleep()方法后,这个线程的睡眠时间已经达到了指定的间隔,那么它就有可能重新回到可运行状态。或当一个线程等待的锁变得可用的时候,那么这个线程也会从被阻塞状态转入可运行状态
- 死亡状态
- 一个线程的run()方法运行完毕、stop()方法被调用或者运行过程中出现未捕获的异常时,线程进入死亡状态。
线程调度
- 当同一时刻有多个线程处于可运行状态,它们需要排队等待CPU资源,每个线程会自动获得一个线程的优先级,优先级的高低反应现成的重要或紧急程度。可运行状态的线程按优先级排队,线程调度依据建立在优先级基础上的“先到先服务”原则。
- 线程调度管理器负责线程排队和在线程间分配CPU,并按线程调度算法进行调度。当线程调度管理器选中某个线程时,该线程获得CPU资源进入运行状态。
- 线程调度时抢占式调度,即在当前线程执行过程中如果有一个更高优先级的线程进入可运行状态,则这个更高优先级的线程立即被调度执行。
- 线程优先级
-
相乘的优先级用1~10表示,10表示优先级最高,默认值是5。每个优先级对应一个Thread类的公用静态常量。例如:
public static final int NORM_PRIORITY=5; public static final int NORM_PRIORITY=1; public static final int NORM_PRIORITY=10;
每个线程的优先级都介于Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间
线程的优先级可以通过setPriority(int grade)方法更改,此方法的参数表示要设置的优先级,它必须是一个1~10的整数。例如,myThread.setPriority(3);将线程对象myThread的优先级别设置为3
-
实现线程调度的方法
(1)join()方法使当前线程暂停执行,等待调用该方法的线程结束后再继续执行本线程
//join()的三种重载形式 public final void join(); public fianl void join(long mills); public final void join(long mills,int nanos);
从线程返回数据时也经常使用到join()方法。
public class Test{ public static void main(String[]args)throws Interrupeted Exception{ MyThread thread=new MyThread(); thread.start(); System.out.println("values1:"+thread.value1); System.out.println("values2:"+thread.value2); } } public class MyThread extends Thread{ public String value1; public String value2; public void run(){ value1="value1已赋值"; value2="value2已赋值"; } } //输出结果 value:null value:null
在run()方法中已经对value1和value2赋值,而返回的确实null,出现这种情况的原因是在主线程中调用start()方法后就立刻输出了value1和value2的值,而run()方法可能还没有执行到为value1和value2赋值的语句。要避免这种情况的发生,需要等到run()方法执行完后才执行输出value1和value2的代码,可以使用join()方法来解决这个问题
//在thread.start()后面添加thread.join(); thread.start(); thread.join(); //重新运行输出 value1 value1 已赋值 value2 value2 已赋值
(2)sleep方法
- sleep()方法会让当前线程睡眠(停止执行)millis毫秒,线程由运行中的状态进入不可运行状态,睡眠时间过后线程会再次进入可运行状态
(3)yield()方法
- yield()方法可让当前线程暂停执行,允许其他线程执行,但该线程仍处于可运行状态,并不变为阻塞状态。此时,系统选择其他相同或更高优先级线程执行,若无其他相同或更高优先级,则该线程继续执行。
- 调用yield()方法后,当前线程并不是转入被阻塞状态,它可以与其他等待执行的线程竞争CPU资源,如果此时它由抢占到CPU资源,就会出现连续运行几次的情况
线程同步的必要性
- 前面介绍的线程都是独立的,而且是异步执行,也就是说每个线程都包含了运行时所需要的数据和方法,而不需要外部资源或方法,也不必关心其他线程的状态或行为,否则就不能保证程序运行结果的正确性
实现线程同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源在某一时刻只能被一个线程使用的方式称为线程同步
采用同步来控制线程的执行有两种方式,即同步方法和同步代码块。这两种方式都使用synchronized关键字实现
- 同步方法
- 通过在方法声明加入synchronized关键字来声明同步方法
- 时用synchronized修饰的方法控制对类成员变量的访问。每个类实例对应一把锁,方法一旦执行,就独占该锁,直到该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对应每一个实例,其所有声明为synchronized的方法只能有一个处于可执行状态,从而有效避免了类成员变量的访问冲突
同步方法的缺陷:如果将一个运行时间比较长的方法声明成synchronized将会影响效率
-
同步代码块
- synchronized块中的代码必须获得对象syncObject的锁才能执行,具体实现机制与同步方法一样。由于可针对任意代码块,且可任意指定上锁对象,故灵活性较高
-
死锁
- 多线程在使用同步机制时,存在“死锁”的潜在危险。如果多个线程都处于等待状态而无法唤醒时,就构成了死锁,此时处于等待状态的多个线程占用系统资源,但无法运行,因此不会释放自身的资源。
- 避免死锁的有效方法是:线程因某个条件未满足而受阻,不能让其继续占有资源;如果有多个对象需要互斥访问,应确定获得锁的顺序,并保证整个程序以相反的舒徐释放锁
生产者消费者问题
- 前面的介绍中,了解了多线程中使用同步机制的重要性,并介绍了如何通过同步来正确地访问共享资源。这些线程之间是相互独立地,并不存在任何依赖关系。它们各自竞争CPU资源,互不相让,而且还无条件地组织其他线程对共享资源地异步访问。然而很多现实问题不仅要求同步地访问同一共享资源,而且线程间还彼此牵制,互相通信。
- 使用线程同步可以阻止并发更新同一个共享资源,但是不能用它来实现不同线程之间地消息传递,要解决生产者消费者问题,需要使用线程通信
- 实现线程简通信
- Java提供了如下三种方法实现线程之间的通信
- wait()方法:调用wait()方法会挂起当前程序,并释放共享资源的锁
- notify()方法:调用任意对象的notify()方法会在因调用该对象的wait()方法而阻塞线程中随机选择一个线程解除阻塞,但要等到获得锁后才可真正执行
- notifyAll():调用了notifyAll()方法会将因调用该对象的wait()方法而阻塞的所有线程一次性全部解除阻塞
- Java提供了如下三种方法实现线程之间的通信
wait()、notify()和notifyAll()这三个方法都是Object类中的final方法,被所有类继承且不允许重写。这三个方法只能在同步方法或者同步代码块中使用,否则会抛出异常